自定义View系列的总结

在自定义View系列中以上9篇都是”谷歌的小弟”的原创博文,在这个系列教程中对大部分知识点都做了详细的阐述。在我通读了以上文章后受益匪浅啊,原理明白了就算不强记以后很容易能想得到但是一些用法之类的查找确实麻烦,所以再来一篇总结,结合自己的理解和应用加深理解且以后回忆起来也提供一条思路。顺序就按照小弟的来。

常用的工具类

  • Configuration.class:设备信息类,用来描述设备的配置信息,比如用户的配置信息,设备的相关信息(输入模式、屏幕方向)。
//获取对象
Configuration configuration = getResources().getConfiguration();
//用户Locale
Locale locale = configuration.locale;
//信号的国家码
int mcc = configuration.mcc;
//信号的网络码
int mnc = configuration.mnc;
//横竖屏
int screen = configuration.orientation;
  • ViewConfiguration.class:它提供了自定义View可能用到的一些标准常量,比如尺寸大小、灵敏度。举个例子,获取距离至少为多大系统才会认为是滑动而不是点击。它提供了一系列的方法供我们查询这些标准常量,既然是常量便是不可修改的。
//获取对象
ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
//对象方法-系统识别滑动的最小距离
int touchSlop = viewConfiguration.getScaledTouchSlop();
//对象方法-是否有物理按键
boolean flag = viewConfiguration.hasPermanentMenuKey();
//静态方法-双击间隔时间,在时间内判定为双击,超出为两次单击
int doubleTimeout = ViewConfiguration.getDoubleTapTimeout();
//静态方法-按住变成长按动作需要的时间
int longPressTimeout = ViewConfiguration.getLongPressTimeout();
  • GestureDetector.class:这个类是用来简化Touch处理,可以实现一些常用的操作,比如拖动,滑动等。通过设置GestureDetector.OnGestureListener来监听手势,我们通过实现接口代码处理相应手势动作下的逻辑。
/**
 * step1:实现GestureDetector.OnGestureListener
 */ 

//触摸屏幕时均会调用该方法  
boolean onDown(MotionEvent e);
//手指在屏幕上拖动时会调用该方法   
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
//手指长按屏幕时均会调用该方法
public void onLongPress(MotionEvent e);
//手指在屏幕上滚动时会调用该方法
public boolean onScroll(MotionEvent e1,MotionEvent e2, float distanceX,float distanceY);
//手指在屏幕上按下,且未移动和松开时调用该方法
public void onShowPress(MotionEvent e);
//轻击屏幕时调用该方法
public boolean onSingleTapUp(MotionEvent e);

/**
 * step2:生成GestureDetector对象
 */ 
 GestureDetector gestureDetector = new GestureDetector(context,new
GestureListenerImpl());

/**
 * step3:将View的onTouch时间交由GestureDetector处理
 */ 
 @Override  
public boolean onTouchEvent(MotionEvent event) {  
     return mGestureDetector.onTouchEvent(event);  
} 
  • VelocityTracker.class:速度追踪器。用于跟踪触摸屏事件的速率。
private void startVelocityTracker(MotionEvent event) {

    /**
     * step1-开始追踪,追踪谁?
     */
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);

    /**
     * step2-追踪处理,获取具体的数值
     */
    //设置VelocityTracker单位.1000表示1秒时间内运动的像素
    velocityTracker.computeCurrentVelocity(1000);
    //获取在1秒内X方向所滑动像素值
    int xVelocity = (int) velocityTracker.getXVelocity();
    //获取追踪到的速度
    int velocity_x = Math.abs(xVelocity);

    /**
     * step3-释放
     */
    if (velocityTracker != null) {
        velocityTracker.recycle();
        velocityTracker = null;
    }

}
  • Scroller.class:下节总结。

  • ViewDragHelper.class:处理拖拽动作的类。

mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {});

创建实例,三个参数基本就是这三个。对应的内部方法。

/**
 * 唯一的抽象接口
 * 1. 返回true表示可以捕获这个View的动作
 * 2. 我们可以通过它的参数来判断哪些
 */
public abstract boolean tryCaptureView(View child, int pointerId);

/**
 * 处理水平方向的越界
 * 1. 返回值是我们最终拖拽的距离
 * 2. 参数left是手势拖拽的距离
 * 3. 判定X是否越界,
 *    最小位移min: paddingLeft
 *    最大位移max:parent.width-paddingRight-childView.width
 *    a. left<min return min;
 *    b. left>max return max;
 *    c. else     return left;
 */
public int clampViewPositionHorizontal(View child, int left, int dx);

/**
 * 处理垂直方向的越界(参照水平方向)
 */
 public int clampViewPositionVertical(View child, int top, int dy);

/**
 * 捕获子View动作
 */
 public void onViewCaptured(View capturedChild, int activePointerId);

/**
 * 释放子View动作
 */
 public void onViewReleased(View releasedChild, float xvel, float yvel);

提供一个图给大家脑补…

这里写图片描述

/**
* ViewGroup的事件分发交给ViewDragHelper处理
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return mDragHelper.shouldInterceptTouchEvent(ev);
}

/**
* ViewGroup的事件消费交给ViewDragHelper处理
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    return true;
}

onMeasure()测量过程

MeasureSpec.class

  • 封装了parentView对childView布局的要求;
  • 32位,高2位是mode,低30位是size;
  • EXACTLY,精确模式,已知大小
  • AT_MOST,未检测出大小,但最大不超过其size
  • UNSPECIFIED,不做考虑

View的测量过程,我们通过源码可以知道,View的大小不仅仅它本身的布局有关,还和parentView的MeasureSpec相关,view的size由parentView的MeasureSpec.mode和其本身的布局共同决定,对源码的分析可以得出以下结论。

  • ① 当View的宽高为具体值value时,不管其parentView的MeasureSpec.mode。
    size = value;
    mode = EXACTLY;
  • ② 当View的宽高布局为match_parent,parentView.mode为EXACTLY。
    size = parentLeftSize;
    mode = EXACTLY;
  • ③ 当View的宽高布局为match_parent,parentView.mode为AT_MOST。
    size = parentLeftSize;
    mode = AT_MOST;
  • ④ 当View的宽高布局为wrap_content,不管其parentView的MeasureSpec.mode。
    size = parentLeftSize;
    mode = AT_MOST;
    这里写图片描述

  • parentLeftSiz表示parentView剩下的空间;

onMeasure()分析
在了解了MeasureSpec后,我们具体分析View如何获取大小的。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  • protected int getSuggestedMinimumWidth()。看是否有background,没有的话size=minWidth(或minHeight),有的话取两者中较大的值为size;
  • public static int getDefaultSize(int size, int measureSpec)。可知size=MeasureSpec.getSize(measureSpec);就是说onMeasure()过程中View的size是由ViewGroup()绘制
    protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed);的时候设置的size决定的,也就是这个图。

这里写图片描述

  • 我们看到情况④,当childView的布局为wrap_content时,childView的size一直都是parentLeftSize,即父控件剩余空间,也可以说是match_parent,就是填满空间了…,所以我们在自定义View时如果不对这种情况进行处理的话就会造成我们的自定义View布局为wrap_content时,实际效果是match_parent;
  • 为什么TextView等等控件在上述情况表现正常呢,这是因为在源码中已经对上述情况作了处理。
  • 那么我们应该如何处理?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = 100;
    int height = 200;
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if( widthMode==MeasureSpec.AT_MOST && heightMode==MeasureSpec.AT_MOST )
        setMeasuredDimension(width, height);
    else if (widthMode==MeasureSpec.AT_MOST)
        setMeasuredDimension(width, heightSize);
    else if ( heightMode==MeasureSpec.AT_MOST )
        setMeasuredDimension(widthSize, height);

}
  • 我们在重写onMeasure()方法时,利用判断childView.mode来甄别布局是否为wrap_content。当mode为AT_MOST时我们就认为是wrap_content。
  • 但是我们查看图表可知,情况③也是AT_MOST模式,但是它的属性是match_parent,Why?其实我们通过反证法,可以得出结论此种情况是不存在(不合理)的,所以正常情况不会出现,所以可以忽略。

onLayout()摆放过程

  • View经过onMeasure()阶段之后,会进入到onLayout()确定View的位置过程。先要确定View应该放在哪里我们需要理清思路。
    protected void onLayout(boolean changed, int left, int top, int
    right, int bottom) {};方法
  • 该方法在源码中为空,且源码给出的注释说明,我们调用该方法去确定子View的位置,也就是说子View的位置是由其父View中的onLayout()方法确定的,通过查看父View的onLayout()源码,我们知道该方法为抽象方法,即继承ViewGroup的View(如LinearLayout等)需要按照自己的规则去实现onLayout()方法。
  • 所以自定义ViewGroup时,我们在onLayout()方法中我们根据parentView的参数等去计算所有chiilView的位置,并且在得到具体位置的值时调用childView的layout()确定位置(即第一步)。

有两个方法有必要讨论一下,getMeasuredWidth()和getWidth()方法。它们有何区别。

  • getMeasuredWidth()方法查看源码,return mMeasuredWidth变量,查找源码可以看出此变量在
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法中给赋值的,在网上查发现,’setMeasuredDimension()’方法调用了setMeasuredDimensionRaw(),而onMeasure()调用了setMeasuredDimension()。
  • getWidth()方法查看源码发现,返回值是”mRight - mLeft”,且它在onLayout()过程后才有的值。
  • 那么,getMeasuredWidth()方法的值是在onMeasure()测量过程后获取的大小。并且它的值和我们在setMeasuredDimension()设置的值相关。
  • 那么,getWidth()方法的值是在onLayout()摆放过程之后获取的大小,并且它的值是坐标相减的结果。

总结完onDraw()之后,会有例子体现。

onDraw()绘画过程

绘制的过程从以下几个方面来简述。

  • 源码draw(canvas)绘制过程
    - 从源码注释可看出View的绘制大体上分为6步。
    - 1. 绘制背景drawBackground(canvas);
    - 2. 保存当前画布的堆栈状态并在该画布上创建Layer用于绘制View在滑动时的边框渐变效果(可忽略);
    - 3. 绘制View的内容,protected void onDraw(Canvas canvas) {};我们需要实现的方法:
    - 4. 画子View,dispatchDraw(canvas);
    - 5. 绘制当前视图在滑动时的边框渐变效果(可忽略);
    - 6. 绘制View的滚动条;

  • Canvas、Bitmap、Paint的关系
    - 源码中看出我们绘制View时重写onDraw()方法,而方法中只有参数Canvas,查看官方文档可以得到绘制4要素
    - 1. 用什么工具画? Paint类 。
    - 2. 把画画在哪里?Bitmap上,Bitmap承载和呈现了画的各种图形。
    - 3. 画的内容?根据自己的需求画圆,画直线,画路径。
    - 4. 怎么画? canvas各种操作。

  • Canvas类的常用操作
    - canvas.translate(x, y); 移动坐标系
    - canvas.rotate(angle); 旋转坐标系
    - canvas.clipXxx(); 裁剪某个形状,就是把坐标系放入到裁剪区域
    - canvas.save()生成一个透明的图层Layer;
    - canvas.restore()Layer操作的东西覆盖到原来的图形上;

  • PorterDuffXfermode图形合成的规则
    - PorterDuffXfermode,图形的合成规则,有一个图来表现它的规则
    这里写图片描述

来个例子,默认为SRC_IN:

/**
* 测试模式
* @param bitmap 图片
* @param pixels 圆角半径
* @return
*/
private Bitmap testPorterDuffXfermode(Bitmap bitmap, float pixels){
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    Bitmap roundBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(roundBitmap);
    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    paint.setAntiAlias(true);
    Rect rect = new Rect(0, 0, width, height);
    RectF rectF = new RectF(rect);
    canvas.drawRoundRect(rectF, pixels, pixels, paint);
    PorterDuffXfermode xfermode=new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    paint.setXfermode(xfermode);
    canvas.drawBitmap(bitmap, rect, rect, paint);
    return roundBitmap;
}

其对应的图片,我们先画了一个圆角矩形,然后设置了PorterDuff.Mode为SRC_IN,最后绘制了原图。 所以,它会取圆角矩形和原图相交的部分但只显示原图部分;这样就形成了圆角的Bitmap。DST目标图像是画布初始图像椭圆,而SRC源图像则是要呈现的图片girl.png:
这里写图片描述

其所有模式对应解释PorterDuff.Mode.XXX,
- 01. CLEAR
绘制不会提交到画布上,可以理解为清空画布上所有像素
- 02. SRC
只显示绘制源图像
- 03.DST
只显示目标图像,即已在画布上的初始图像
- 04.SRC_OVER
正常绘制显示,即后绘制的叠加在原来绘制的图上
- 05.DST_OVER
上下两层都显示但是下层(DST)居上显示
- 06.SRC_IN
取两层绘制的交集且只显示上层(SRC)
- 07.DST_IN
取两层绘制的交集且只显示下层(DST)
- 08.SRC_OUT
取两层绘制的不相交的部分且只显示上层(SRC)
- 09.DST_OUT
取两层绘制的不相交的部分且只显示下层(DST)
- 10.SRC_ATOP
两层相交,取下层(DST)的非相交部分和上层(SRC)的相交部分
- 11.DST_ATOP
两层相交,取上层(SRC)的非相交部分和下层(DST)的相交部分
- 12.XOR
挖去两图层相交的部分
- 13.DARKEN
显示两图层全部区域且加深交集部分的颜色
- 14.LIGHTEN
显示两图层全部区域且点亮交集部分的颜色
- 15.MULTIPLY
显示两图层相交部分且加深该部分的颜色
- 16.SCREEN
显示两图层全部区域且将该部分颜色变为透明色

  • Bitmap和Matrix矩阵处理图像
    - 我们通过PorterDuffXfermode来对图形合成进行一些操作,可以通过Matrix对图形进行位移、缩放等等操作。
    - 通过Matrix对图形进行处理时,我们可以把处理事件集当做一个队列来考虑,我们需要把所有事件添加到队列当中,然后再执行。将事件添加到队列的方式有三种,set、post、pre;
    - set,会将队列中所有指令清空,然后将当前指令放入到指令队列中,此时指令集中只有一个事件;
    - post,会将当前指令放入到指令集的队列末尾,当前指令最后执行;
    - pre,会将当前指令放入到指令集的队列头部,当前指令最先执行;
m.postTranslate(x1, y1);
m.setScale(0.2f, 0.5f);
m.preTranslate(x2, y2); 
canvas.drawBitmap(bitmap, matrix, paint);

先执行位移(x2, y2),再执行缩放。

  • Shader渲染图像
    - BitmapShader———图像渲染
    - LinearGradient——–线性渲染
    - RadialGradient——–环形渲染
    - SweepGradient——–扫描渲染
    - ComposeShader——组合渲染
    - 具体使用可以查阅相关文档。

  • PathEffect画路径时样式效果
    - CornerPathEffect,用平滑的方式衔接Path的各部分;
    - DashPathEffect,将Path的线段虚线化;
    - PathDashPathEffect,与DashPathEffect效果类似但需要自定义路径虚线的样式;
    - DiscretePathEffect,离散路径效果;
    - ComposePathEffect,两种样式的组合。先使用第一种效果然后在此基础上应用第二种效果;
    - SumPathEffect,两种样式的叠加。先将两种路径效果叠加起来再作用于Path;

自定义View小结

  • 直接继承自View
    - 在使用该方式实现自定义View时通常的核心操作都在onDraw( )当中进行。
    - 在分析measure部分源码的时候,我们提到如果直接继承自View在onMeasure( )中要处理view大小为wrap_content的情况,否则这种情况下的大小和match_parent一样。
    - 还需要注意对于padding的处理。

  • 继承自系统已有的View
    - 比如常见的TextView,Button等等。如果采用该方式,我们只需要在系统控件的基础上做出一些调整和扩展即可,而且也不需要去自己支持wrap_content和padding。

  • 直接继承自ViewGroup
    - 在onMeasure( )实现wrap_content的支持。这点和直接继承自View是一样的。
    - 在onMeasure( )和onLayout中需要处理自身的padding以及子View的margin

  • 继承自系统已有的ViewGroup
    - 比如LinearLayout,RelativeLayout等等。如果采用该方式,那么在上面提到的两个问题就不用再过多考虑了,简便了许多。

搜索历史标签可参照:MyFlowLayout.class

View的Touch事件处理

View的touch时间处理流程用小弟的流程图表示

这里写图片描述

  • View处理Touch事件的总体流程
    - dispatchTouchEvent()->onTouch()->onTouchEvent()->onClick();
    - touch事件最先传入到dispatchTouchEvent()中去;
    - 如果View存在onTouchListener()那么会调用该监听器的onTouch()函数,在此函数中,如果touch事件被消费掉了,则不会再往下执行任何方法,即onTouch()返回true,那么touch事件处理到此为止。如果touch事件未被消费则会继续调用View的onTouchEvent()函数处理touch事件。
    - 如果View不存在onTouchListener(),那么会执行调用View的onTouchEvent()函数处理touch事件,在该方法中处理ACTION_UP事件时如果设置了onClickListener监听则调用onClick()函数。

  • onTouch()与onTouchEvent()以及click三者的区别和联系
    - onTouch()、onTouchEvent()函数都是处理触摸事件的API;
    - onTouch()事件是TouchListener接口中的方法, 是暴露给用户的接口便于处理触摸事件,而onTouchEvent()方法是android系统自身处理touch事件的实现;
    - 先调用onTouch()方法,只有当onTouch()方法没有消费掉touch事件时才会继续调用onTouchEvent()方法。即onTouch()方法的优先级高于onTouchEvent()方法;
    - 在onTouchEvent()方法中处理ACTION_UP手势时会调用onClick()方法,所以touch的处理是优先于onClick的;
    - 执行顺序onTouch()–>onTouchEvent()–>onClick()

  • View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有

ViewGroup的事件分发处理

事件的分发与处理可以参照小弟的流程图。

这里写图片描述

  • 在Touch事件的传递过程中,如果上一级拦截了Touch那么其下一级就无法在收到Touch事件。
  • 在Touch事件的消费过程中,如果下一级消费Touch事件那么其上一级就无法处理Touch事件。

源码中如何对touch事件进行分发的,我们同样参照小弟给的流程图来分析。

这里写图片描述

从流程图中我们可以看出。可以分成以下几个流程探讨。

  • 1. touch事件的开端,ACTION_DOWN
    - 做一些初始化、还原状态的操作;
    - 比较重要的一点就是将mFirstTouchEventTarget变量置为null;
    - mFirstTouchEventTarget==null,表明ViewGroup拦截touch事件,或者拦截了touch事件但子View无法消费事件,此时touch事件需要ViewGroup本身自己来处理;
    - mFirstTouchEventTarget !=null,表明ViewGroup不拦截touch事件并且子View可以消费掉touch事件,此时touch事件交给再下层ViewGroup或者子View处理;

  • 2. 是否拦截touch事件
    - 分为三种情况来赋值。
    - 当down事件来临,我们需要看是否设置了禁止拦截标志,如果表明不可拦截,则intercepted为false(情况①),如果表明可以设置拦截,则调用onInterceptTouchEvent()方法来得到具体要不要拦截touch事件(情况②),此方法默认为false不拦截事件,但我们可以重写此方法。
    - 如果第一次Down事件处理时已经得出结论子View无法消费事件,那么当其他touch事件来临时可以直接拦截了,不必再往下分发了(情况③)。
    - 如果拦截了touch事件,调用dispatchTransformedTouchEvent(null);

  • 3. touch事件没有取消和拦截,寻找子View
    - 在这个过程中就是寻找是否存在可以消费touch事件的子View;
    - 找到了可以消费touch事件的子View,调用dispatchTransformedTouchEvent(chileView);
    - 除去上面那种情况,其余的情况,调用dispatchTransformedTouchEvent(null);

  • 4. 总结
    - 以上这就是事件的分发,是从Activity->外部ViewGroup->内部ViewGroup->View顺序派发的。
    - 至于事件的处理,我们通过查看dispatchTransformedTouchEvent()的源码发现;
    - 传入参数为null时,说明ViewGroup拦截了touch事件,或者没有拦截touch事件但是没有任何字View可以消费touch事件,此时我们就把ViewGroup当做成普通的View处理,调用View的Touch事件处理过程;
    - 传入参数不为null时,如果childView为ViewGroup则继续重复ViewGroup的事件分发机制,一直找到子View,调用子View的Touch事件处理机制。

这里写图片描述

滑动冲突

  • 为什么有滑动冲突?
    - 子View和父View都有滑动的需求
    - 滑动事件不能准确地传递给相应的View

  • 如何解决滑动冲突?
    - 子View禁止父View拦截Touch事件,在子View的onTouch()方法中调用
    childView.getParent.requestDisallowInterceptTouchEvent(true);设置禁止拦截touch事件
    - 在父View中准确地进行事件分发和拦截,重写onInterceptTouchEvent()方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值