自定义view如何分类
- 自定义View:只需要重写onMeasure()和onDraw(),在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
- 自定义ViewGroup:只需要重写onMeasure()和onLayout(),一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout
view的构造函数:共有4个
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CustomView(Context context) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
LayoutInflater 的基本用法
LayoutInflater 其实就是使用 Android 提供的 pull 解析方式 来解析布局文件的
LayoutInflater.from(this).inflate()
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
inflate()方法一般接收两个参数,第一个参数就是要加载的布局 id,第二个参数
是指给该布局的外部再嵌套一层父布局,如果不需要就直接传 null。接收三个参数时:
- 如果 root 为 null,attachToRoot 将失去作用,设置任何值都没有意义。
- 如果 root 不为 null,attachToRoot 设为 true,则会给加载的布局文件的指定一个父布局,即 root。
- 如果 root 不为 null,attachToRoot 设为 false,则会将布局文件最外层的所有 layout 属性进行设置,当该 view 被添加到父 view 当中时,这些 layout 属性会自动生效。
- 在不设置 attachToRoot 参数的情况下,如果 root 不为 null,attachToRoot 参数默认为 true。
Android两种坐标系
- View的静态坐标方法
View的静态坐标方法 | 解释 |
getLeft() | 返回View自身左边到父布局左边的距离 |
getTop() | 返回View自身顶边到父布局顶边的距离 |
getRight() | 返回View自身右边到父布局左边的距离 |
getBottom() | 返回View自身底边到父布局顶边的距离 |
getX() | 返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变。 |
getY() | 返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变。 |
- 手指触摸屏幕时MotionEvent
MotionEvent坐标方法 | 解释 |
getX() | 当前触摸事件距离当前View左边的距离 |
getY() | 当前触摸事件距离当前View顶边的距离 |
getRawX() | 当前触摸事件距离整个屏幕左边的距离 |
getRawY() | 当前触摸事件距离整个屏幕顶边的距离 |
- 获取宽高
View宽高方法 | 解释 |
getWidth() | layout后有效,返回值是mRight-mLeft,一般会参考measure的宽度(measure可能没用),但不是必须的。 |
getHeight() | layout后有效,返回值是mBottom-mTop,一般会参考measure的高度(measure可能没用),但不是必须的。 |
getMeasuredWidth() | 返回measure过程得到的mMeasuredWidth值,供layout参考,或许没用。 |
getMeasuredHeight() | 返回measure过程得到的mMeasuredHeight值,供layout参考,或许没用。 |
- 获取view位置
View的方法 | 结论描述 |
getLocalVisibleRect() | 获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。 |
getGlobalVisibleRect() | 获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。 |
getLocationOnScreen() | 坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。 |
getLocationInWindow() | 如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。 |
- View滑动相关坐标系
View的scrollTo()和scrollBy()是用于滑动View中的内容,而不是改变View的位置;改变View在屏幕中的位置可以使用offsetLeftAndRight()和offsetTopAndBottom()方法,他会导致getLeft()等值改变
View的滑动方法 | 效果及描述 |
offsetLeftAndRight(int offset) | 水平方向挪动View,offset为正则x轴正向移动,移动的是整个View,getLeft()会变的,自定义View很有用。 |
offsetTopAndBottom(int offset) | 垂直方向挪动View,offset为正则y轴正向移动,移动的是整个View,getTop()会变的,自定义View很有用。 |
scrollTo(int x, int y) | 将**View中内容(不是整个View)**滑动到相应的位置,参考坐标原点为ParentView左上角,x,y为正则向xy轴反方向移动,反之同理。 |
scrollBy(int x, int y) | 在scrollTo()的基础上继续滑动xy。 |
setScrollX(int value) | 实质为scrollTo(),只是只改变Y轴滑动。 |
setScrollY(int value) | 实质为scrollTo(),只是只改变X轴滑动。 |
getScrollX()/getScrollY() | 获取当前滑动位置偏移量。 |
Android 视图绘制流程
view的层级结构
一. onMeasure() 决定View的大小;
源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
用于测量视图的大小。View 系统的绘制流程会从 ViewRoot 的 performTraversals()方法中开始,在其内部调用 View 的 measure()方法。measure()方法接收两个参数,widthMeasureSpec 和 heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec 的值由 specSize 和 specMode 共同组成的,其中 specSize 记
录的是大小,specMode 记录的是规格。specMode 一共有三种类型,如下所
示:
1. EXACTLY(确切的大小,如:100dp)
表示父视图希望子视图的大小应该是由 specSize 的值来决定的,系统默认会按
照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任
意的大小。
2. AT_MOST(大小不可超过某数值,如:matchParent, 最大不能超过父布局大小)
表示子视图最多只能是 specSize 中指定的大小,开发人员应该尽可能小得去设
置这个视图,并且保证不会超过 specSize。系统默认会按照这个规则来设置子
视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
3. UNSPECIFIED(不对View大小做限制,系统使用)
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这
种情况比较少见,不太会用到。
public class MyView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}
上面代码中把 View 默认的测量流程覆盖掉了,不管在布局文件中定义 MyView 这个视图的大小是多少,最终在界面上显示的大小都将会是 200*200。
注意:在 setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和 getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是 0。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。
getMeasureWidth与getWidth的区别
- getMeasureWidth:在measure()过程结束后就可以获取到对应的值; 通过setMeasuredDimension()方法来进行设置的.
- getWidth:在layout()过程结束后才能获取到; 通过视图右边的坐标减去左边的坐标计算出来的.
二. onLayout() 决定View在ViewGroup中的位置;
源码:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot 的 performTraversals()方法会在 measure 结束后继续执行,并调用 View 的 layout()方法来执行此过程。
View 中的 onLayout()方法就是一个空方法,因为 onLayout()过程是为
了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图 决定子视图的显示位置。既然如此,我们来看下 ViewGroup 中的 onLayout() 方法是怎么写的吧,代码如下:
ViewGroup中的源码:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
可以看到,ViewGroup 中的 onLayout()方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。
在 onLayout()过程结束后,我们就可以调用 getWidth()方法和 getHeight()方 法来获取视图的宽高了。
三. onDraw() 决定绘制这个View。
在这里才真正地开始对视图进行绘制。ViewRoot 中的代码会继续执行并创建出一个 Canvas 对象,然后调用 View 的 draw()方法来 执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代
码如下所示:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
第一步的作用是对视图的背景进行绘制。这里会先得到一个 mBGDrawable 对象,然后根据 layout 过程确定的视图位置来设置背景的绘制区域,之后再调用 Drawable 的draw()方法来完成背景的绘制工作。那么这个 mBGDrawable 对象是从哪里来的呢?其实就是在 XML中通过 android:background 属性设置的图片或颜色。当然你也可以在代码中通过 setBackgroundColor()、setBackgroundResource()等方法进行赋值。
第三步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么 onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。
第四步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现 View 中的 dispatchDraw()方法又是一个空方法,而 ViewGroup 的 dispatchDraw()方法中就会有具体的绘制代码。
第六步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是 ListView 或者ScrollView,为什么要绘制滚动条呢?其实不管是 Button 也好,TextView 也 好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。
自定义绘制API
Canvas常用方法
- 绘制图形(点、线、矩形、椭圆、圆等)
- 绘制文本(文本的居中问题,需要Paint知识)
- 画布的基本变化(平移、缩放、旋转、倾斜)
- 画布的裁剪
- 画布的保存
Paint类主要用于设置绘制风格:包括画笔的颜色画笔触笔粗细、填充风格及文字的特征
- Paint常用方法
- 颜色
- 类型(填充、描边)
- 字体大小
- 宽度
- 对齐方式
- 文字位置属性测量
- 文字宽度测量
3.Path常用方法
- 添加路径
- 移动起点
- 贝塞尔(二阶、三阶)
- 逻辑运算
- 重置路径
- PathEffect
- Matrix
- PathMeasure
- PorterDuffXfermode
- Matrix
- 平移矩阵
- 缩放矩阵
- 旋转矩阵
- ColorMatrix
类别 | API | 描述 |
旋转 | setRotate | 设置(非输入轴颜色的)色调 |
饱和度 | setSaturation | 设置饱和度 |
缩放 | setScale | 三原色的取值的比例 |
设置 | set、setConcat | 设置颜色矩阵、两个颜色矩阵的乘积 |
重置 | reset | 重置颜色矩阵为初始状态 |
矩阵运算 | preConcat、postConcat | 颜色矩阵的前乘、后乘 |
4.动画
- ObjectAnimator
- ValueAnimator
- AnimatorSet
- 差值器
- 估值器
自定义View示例:实现类似水波扩散效果一共分为六步
1.
public SpreadView(Context context) {
this(context,null,0);
}
public SpreadView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public SpreadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaints();
}
private void initPaints() {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
2.实现画笔paint类
本文一共两只画笔
private void initPaints() {
//画笔1:
centerPaint = new Paint();
centerPaint.setColor(Color.YELLOW);
centerPaint.setAntiAlias(true);//抗锯齿效果
//最开始不透明且扩散距离为0
alphas.add(255);
spreadRadius.add(0);
//画笔2:
spreadPaint = new Paint();
spreadPaint.setAntiAlias(true);
spreadPaint.setAlpha(255);
spreadPaint.setColor(Color.RED);
}
3.覆写onMeasure(…)方法
实现这个方法告诉了母容器如何放弃自定义View,可以通过提供的measureSpecs来决定你的View的高和宽,以下是一个正方形,确认它的宽和高是一样的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = MeasureSpec.getSize(widthMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(w, h);
setMeasuredDimension(size, size);
}
注意:
这个方法需要至少保证一个setMeasuredDimension(..)调用,否则会报IllegalStateException错误。
4.实现onSizeChanged(…)方法
这个方法是你获取View现在的宽和高. 这里我们计算的是中心和半径
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//圆心位置
centerX = w / 2;
centerY = h / 2;
}
5.实现onDraw(…)方法
这个方法提供了如何绘制view,它提供的Canvas类可以进行绘制。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < spreadRadius.size(); i++) {
int alpha = alphas.get(i);
spreadPaint.setAlpha(alpha);
int width = spreadRadius.get(i);
//绘制扩散的圆
canvas.drawCircle(centerX, centerY, radius + width, spreadPaint);
//每次扩散圆半径递增,圆透明度递减
if (alpha > 0 && width < 300) {
alpha = alpha - distance > 0 ? alpha - distance : 1;
alphas.set(i, alpha);
spreadRadius.set(i, width + distance);
}
}
//当最外层扩散圆半径达到最大半径时添加新扩散圆
if (spreadRadius.get(spreadRadius.size() - 1) > maxRadius) {
spreadRadius.add(0);
alphas.add(255);
}
//超过8个扩散圆,删除最先绘制的圆,即最外层的圆
if (spreadRadius.size() >= 8) {
alphas.remove(0);
spreadRadius.remove(0);
}
//中间的圆
canvas.drawCircle(centerX, centerY, radius, centerPaint);
//TODO 可以在中间圆绘制文字或者图片
//延迟更新,达到扩散视觉差效果
postInvalidateDelayed(delayMilliseconds);
}
6.添加你的View
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<com.qinqu.spreadviewdemo.SpreadView
android:id="@+id/spreadView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:spread_center_color="@color/colorAccent"
app:spread_delay_milliseconds="35"
app:spread_distance="5"
app:spread_max_radius="90"
app:spread_radius="100"
app:spread_spread_color="@color/colorAccent" />
</LinearLayout>
问题
自定义 View 执行 invalidate() 方法 , 为什么有时候不会回调 onDraw()?
自定义一个view时,重写onDraw。调用view.invalidate(),会触发onDraw和computeScroll()。前提
是该view被附加在当前窗口.
view.postInvalidate(); //是在非UI线程上调用的
自定义一个ViewGroup,重写onDraw。onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。
表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。因此,一般直接重写dispatchDraw来绘制viewGroup.自定义一个ViewGroup,dispatchDraw会调用drawChild.