学习笔记,备忘。完整学习资源:hencoder
Path的相关方法,细分为两类:
addXxx - 添加子图形,如addCirclexxxTo - 画线(直线或者曲线 - 贝塞尔曲线),如lineTo等。与1的区别是:1添加完整的封闭图形,而2只是添加一条线。moveTo(不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但是你可以通过moveTo或者rMoveTo来改变当前位置,从而间接地设置这些方法的起点。moveTo 和rMoveTo的区别在于rMoveTo是相对当前位置坐标)等。Path.setFillType设置填充方式。 Paint类的几个常用的方法,例如: Paint.setStyle(Style style)设置绘制模式。比如Style.Fill、Style.Stroke等Paint.setColor(int color) 设置颜色Paint.setStrokeWidth(float width)Paint.setTextSize(float textSize)Paint.setAniAlias(boolean aa) 设置抗锯齿开关Paint.setStrokeCap(cap)设置线条端点形状的方法。端点有圆头(ROUND)、平头(BUTT)和方头(SQUARE)关于Android中的坐标系:
在Android中,每个View都有自己的视图坐标系。这个坐标系的原点是View左上角的那个点。水平方向是x轴,右正左负。竖直方向是Y轴,下正上负。对于旋转的弧度,顺时针是正,逆时针是负。
Paint Api大致分为四类:
效果如下:
paint.setColorFilter为绘制设置颜色过滤。颜色过滤的意思是为绘制的内容设置一个统一的过滤策略,然后Canvas.drawXXX()方法会对每个像素都进行过滤后再绘制出来。比如胶卷效果、有色光照射效果等。
setXfermodeXfermode指的是你要绘制的内容和Canvas的目标位置的内容应该怎样结合计算出最终的颜色。通俗的说就是要你以绘制的内容作为源图像,以View中已有的内容作为目标图像,选取一个PorterDuff.mode作为绘制内容的颜色处理方案。
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); ... canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方 paint.setXfermode(xfermode); // 设置 Xfermode canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆 paint.setXfermode(null); // 用完及时清除 XfermodePorterDuff.Mode在Paint一共有三处API,它们的工作原理都一样,只是用途不同:
另外,设置Xfermode的时候其实是创建的它的子类PorterDuffXfermode。事实上Xfermode也只有这一个子类。
注意:使用Xfermode的时候,必须使用离屏缓冲,即把内容绘制在额外的层上,再把绘制好的内容贴回View中。
离屏缓存有两种方式:
1、Canvas.saveLayer - 可以做短时间的离屏缓冲。使用方式简单,在绘制代码前后各加一行代码即可。
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG); canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方 paint.setXfermode(xfermode); // 设置 Xfermode canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆 paint.setXfermode(null); // 用完及时清除 Xfermode canvas.restoreToCount(saved);2、View.setLayerType
View.setLayerType() 是直接把整个 View 都绘制在离屏缓冲中。 setLayerType(LAYER_TYPE_HARDWARE) 是使用 GPU 来缓冲, setLayerType(LAYER_TYPE_SOFTWARE) 是直接直接用一个 Bitmap 来缓冲。如无特殊需求,可以选择第一种方法,以此获取更高的性能。
效果类Api,指的是
抗锯齿(paint.setAntiAlias);填充/轮廓(paint.setStyle);线条宽度(paint.setStrokeWidth);setStrokeCap(设置线头形状,BUTT平头、ROUND圆头、SQUARE方头。默认为BUTT);setStrokeJoin(Paint.Join join)设置拐角的形状,有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER;setStrokeMiter是对setStrokeJoin的一个补充,用于设置MITER型拐角的延长线的最大值,即如果拐角的角度太小,有可能由于出现连接点过长的情况,如果过长,则尖角转为平角。Paint的色彩优化有两个方法:setDither(设置图像的抖动,即通过在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法)和setFilterBitmap(图像在放大绘制的时候,如果开启了双线性过滤,就可以让结果图像显得更加平滑),作用是让画面颜色变得更加顺眼。setPathEffect(PathEffect effect)给图形的轮廓设置效果 - 比如虚线的圆、把拐角变平角等,对Canvas的所有图形绘制有效,也就是drawLine、drawCircle等setShaowLayer:在此方法之后绘制的内容,会加一层阴影。清除阴影使用clearShadowLayer。注:硬件加速的情况下,只支持文字的绘制,文字之外的绘制必须关闭硬件加速才可以正常绘制。setMaskFilter:setShadowLayer() 是设置的在绘制层下方的附加效果;而这个 MaskFilter 和它相反,设置的是在绘制层上方的附加效果。等等这些。
drawText(String text, float x, float y , Paint paint)
x,y是文字的坐标,但是这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。
因此,在绘制文字的时候把坐标填成(0,0),文字并不会显示在View的左上角,而几乎完全显示在View的上方,到了View外部看不到的位置。如下图:
注:其他的drawXXX都是以左上角作为基准点的,而drawText却是文字左下方。drawText中的y指的是文字的基线。
沿着一条Path来绘制文字。
Canvas.drawText只能绘制单行文字,不能换行。要想实现绘制多行文字,可以使用StaticLayout。
用法与clipRect完全一样,只是把参数换成了Path。可以裁切的形状更多一些。
Canvas .translate、Canvas .scale、Canvas .rotate、skew(错切)
注:变换前后注意保存与恢复绘制范围。另外,Canvas的几何变换顺序反的,比如你想先平移再缩放,就需要把缩放的代码写在平移的代码前面。
canvas.save(); canvas.skew(0, 0.5f); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore(); 使用 Matrix 来做常见和不常见的二维变换;Matrix 做常见变换的方式:
创建 Matrix 对象;调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。上面已经说了,Canvas的几何变换顺序是反的,但是Matrix可以自己来定义顺序。preXXX往前插入变换操作,postXXX就是往后面插入平移。
Matrix matrix = new Matrix(); ... matrix.reset(); matrix.postTranslate(); matrix.postRotate(); canvas.save(); canvas.concat(matrix); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore();效果和Canvas是一样的。把Matrix应用到Canvas有两个方法:Canvas.setMatrix和Canvas.concat:
Canvas.setMatrix(matrix):用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换(注:根据下面评论里以及我在微信公众号中收到的反馈,不同的系统中 setMatrix(matrix) 的行为可能不一致,所以还是尽量用 concat(matrix) 吧);Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。可以使用Matrix来做自定义变换。自定义变换使用的是Matrix.setPolyToPoly方法。
使用 Camera 来做三维变换。Camera三维变换有三类:旋转、平移、移动相机。
Camera的工作原理:
与二维的View坐标系不同,x轴 - 右正左负,y轴 - 上正下负,z轴 - 外负里正
相机的位置位于图中的小黄点,可以通过setLocation来改变相机在z轴的位置。
1、三维旋转:rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z);
canvas.save(); // Camera和Canvas一样,也需要保存和恢复状态才能正常绘制,不然在界面刷新之后,会出现问题 camera.save(); // 保存 Camera 的状态 camera.rotateX(30); // 旋转 Camera 的三维空间 camera.applyToCanvas(canvas); // 把旋转投影到 Canvas camera.restore(); // 恢复 Camera 的状态 canvas.drawBitmap(bitmap, point1.x, point1.y, paint); canvas.restore();2.translate/setLocation
一个完整的绘制过程会依次绘制以下几个内容:
背景主体(onDraw())子 View(dispatchDraw())滑动边缘渐变和滑动条前景一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。
这其中的第 2、3 两步,前面已经讲过了;第 1 步——背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法,这个每个人都用得很 6 了),而不能自定义绘制;而第 4、5 两步——滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。
draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。前面讲到的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。 // View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦): public void draw(Canvas canvas) { ... drawBackground(Canvas); // 绘制背景(不能重写) onDraw(Canvas); // 绘制主体 dispatchDraw(Canvas); // 绘制子 View onDrawForeground(Canvas); // 绘制滑动相关和前景 ... }
关于绘制方法,有两点需要注意一下:
出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false)这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。上述伪代码将涉及事件拦截的三个方法的关系表现的淋漓尽致。
dispatchTouchEvent:如果事件能够传递给当前View,则此方法一定会被调用;onInterceptTouchEvent:如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用;View无该事件;onTouchEvent:如果此方法返回false,则在同一事件序列中,当前View不会再次接收到该事件。 ------ View默认的事件处理流程 ------ public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && enable && mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } // 不同的子View可能需要复写onTouchEvent,比如TextView public boolean onTouchEvent(MotionEvent event) { if (!enable) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } if (clickable) { ----------事件处理开始--------- switch(event.getAction()){ case MtionEvent.ACTION_UP: // 在UP事件中处理onClick if (mOnClickListener != null) { mOnClickListener.onClick(this); result = true; } else { result = false; } break; ...... } ----------事件处理完成--------- return true;// clickable为true,则直接返回true } return false; }优先级:OnTouchListener > onTouchEvent > OnClickListener,即当一个View进行事件处理时,如果设置了OnTouchListener,那么OnTouchListener中的onTouch方法将会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法将不会被调用。在onTouchEvent方法中,如果当前设置了OnClickListener,那么它的onClick方法会被调用。
当一个点击事件产生后,传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件之后,就会按照事件分发机制去分发事件。极端情况:如果所有的元素都不处理这个事件,那么这个事件将会最终再传递给Activity处理,即Activity的onTouchEvent方法会被调用。
onTouchEvent是否消费一组事件,是需要在DOWN事件中决定的,如果你在DOWN事件发过来的时候返回了false,以后你就跟这组事件无缘了,没有第二次机会。
而onInterceptTouchEvent则是你在整个过程中对事件流中的每个事件进行监听,你可以选择先行观望,给子View一个处理事件的机会,而一旦事件流的发展达到了你的触发条件,比如你发现用户是在滑动了,这个时候你再返回true,立刻就可以实现事件流的接管。这样就做到了两不耽误,既让子View有机会去处理事件,又可以在需要的时候把处理事件的工作接管过来。
当onInterceptTouchEvent返回true的时候,除了完成事件的接管,这个View还会做一件事,就是它会对它的子View发送一个额外的取消事件ACTION_CANCEL。因为你在接管事件的时候子View可能正处在一个中间的状态,比如用户先按下了一个按钮,然后手指一滑,你就知道用户是要滑动,这个时候你就把事件拦截接管了,但是现在上面的那个按钮它是按下状态,你需要让它恢复,因此,在onInterceptTouchEvent返回true的时候,子View会接收到一个CANCEL事件,告诉他通知他,这个事件序列你不要接管了,把状态恢复吧。
由上可以看出,事件流的结束有两种情况:
ACTION_DOWN -> ACTION_MOVE -> ...... -> ACTION_UPACTION_DOWN -> ACTION_MOVE -> ...... -> ACTION_CANCEL(非人为的情况)父容器决定是否拦截事件。
// onInterceptTouchEvent 一旦完成了拦截,该方法不再调用 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean isIntercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: isIntercepted = false; break; case MotionEvent.ACTION_MOVE: // 拦截后的父View可以处理MOVE和UP事件,但是没有DOWN事件了,因为同一事件序列的DOWN事件触发完成,子View已经消费了该事件 if(父容器需要当前点击事件){ isIntercepted = true; }else{ isIntercepted = false; } break; case MotionEvent.ACTION_UP: isIntercepted = false; break; default: break; } mLastX = x; mLastY = y; return isIntercepted; }ACTION_DOWN必须返回false,否则一旦父容器拦截了ACTION_DOWN,那么后续的事件都会直接交由父容器处理了。其次,ACTION_UP事件这里也必须返回false,因为如果返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发。父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都交给它处理了。即父容器是Boss,可以随时接管。
内部拦截法 - 子View的dispatchTouchEvent父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则,交给父容器进行处理。这种处理方案需配合requestDisallowInterceptTouchEvent完成。
父:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; }else{ return true; } }除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用requestDisallowInterceptTouchEvent(false)的时候,父元素才能继续拦截所需的事件。
子:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int dx = x - mLastX; int dy = y - mLastY; if(父View需要当前点击事件){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); }父容器为啥不能拦截ACTION_DOWN呢?因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的影响,所以一旦父容器拦截了ACTION_DOWN,那么所有的事件都无法传递给子元素了。
