首页 > 学院 > 开发设计 > 正文

自定义View系列的总结

2019-11-09 16:17:47
字体:
来源:转载
供稿:网友

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

常用的工具类

Configuration.class:设备信息类,用来描述设备的配置信息,比如用户的配置信息,设备的相关信息(输入模式、屏幕方向)。//获取对象Configuration configuration = getResources().getConfiguration();//用户LocaleLocale 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,newGestureListenerImpl());/** * 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处理*/@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { return mDragHelper.shouldInterceptTouchEvent(ev);}/*** ViewGroup的事件消费交给ViewDragHelper处理*/@Overridepublic boolean onTouchEvent(MotionEvent event) { mDragHelper.processTouchEvent(event); return true;}

onMeasure()测量过程

MeasureSpec.class

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

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等等控件在上述情况表现正常呢,这是因为在源码中已经对上述情况作了处理。那么我们应该如何处理?@Overrideprotected 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图形合成的规则 -

Bitmap和Matrix矩阵处理图像 -

Shader渲染图像 -

PathEffect画路径时样式效果 -


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表