在自定义View系列中以上9篇都是”谷歌的小弟”的原创博文,在这个系列教程中对大部分知识点都做了详细的阐述。在我通读了以上文章后受益匪浅啊,原理明白了就算不强记以后很容易能想得到但是一些用法之类的查找确实麻烦,所以再来一篇总结,结合自己的理解和应用加深理解且以后回忆起来也提供一条思路。顺序就按照小弟的来。
//获取对象
Configuration configuration = getResources().getConfiguration();
//用户Locale
Locale locale = configuration.locale;
//信号的国家码
int mcc = configuration.mcc;
//信号的网络码
int mnc = configuration.mnc;
//横竖屏
int screen = configuration.orientation;
//获取对象
ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
//对象方法-系统识别滑动的最小距离
int touchSlop = viewConfiguration.getScaledTouchSlop();
//对象方法-是否有物理按键
boolean flag = viewConfiguration.hasPermanentMenuKey();
//静态方法-双击间隔时间,在时间内判定为双击,超出为两次单击
int doubleTimeout = ViewConfiguration.getDoubleTapTimeout();
//静态方法-按住变成长按动作需要的时间
int longPressTimeout = ViewConfiguration.getLongPressTimeout();
/**
* 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);
}
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. leftmax 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;
}
MeasureSpec.class
View的测量过程,我们通过源码可以知道,View的大小不仅仅它本身的布局有关,还和parentView的MeasureSpec相关,view的size由parentView的MeasureSpec.mode和其本身的布局共同决定,对源码的分析可以得出以下结论。
④ 当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));
}
@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);
}
有两个方法有必要讨论一下,getMeasuredWidth()和getWidth()方法。它们有何区别。
总结完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
显示两图层全部区域且将该部分颜色变为透明色
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时通常的核心操作都在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事件的总体流程
- 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才有
事件的分发与处理可以参照小弟的流程图。
源码中如何对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()方法。