3.5onDraw流程设计
目录介绍
- 01.Draw绘制过程
- 02.View绘制流程
- 03.onDraw和draw区别
- 04.Draw绘制注意事项
- 05.draw绘制闪烁
- 06.View中draw源码
- 07.ViewGroup中draw源码
- 09.Draw绘制圆环
- 10.绘制圆形ImageView
01.Draw绘制过程
1.1 View的绘制过程遵循步骤
- View的绘制过程遵循如下几步:
- ①绘制背景 background.draw(canvas)
- ②绘制自己(onDraw)
- ③绘制Children(dispatchDraw)
- ④绘制装饰(onDrawScrollBars)
1.2 查看源码
- 从源码中可以清楚地看出绘制的顺序。
- 无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
public void draw(Canvas canvas) { // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法) // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。 // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。 ... int saveCount; if (!dirtyOpaque) { // 步骤1: 绘制本身View背景 drawBackground(canvas); } // 如果有必要,就保存图层(还有一个复原图层) // 优化技巧: // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过 // 因此在绘制的时候,节省 layer 可以提高绘制效率 final int viewFlags = mViewFlags; if (!verticalEdges && !horizontalEdges) { if (!dirtyOpaque) // 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写 onDraw(canvas); ...... // 步骤3:绘制子View 默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法 dispatchDraw(canvas); ........ // 步骤4:绘制滑动条和前景色等等 onDrawScrollBars(canvas); .......... return; } ... }
02.View绘制流程
- View绘制流程:
img
03.onDraw和draw区别
- 大概扫一下源码就可以明白,draw()这个函数本身会做很多事情
* 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance)
- 在第三步的时候,它就会调用onDraw()方法,来绘制view的内容。也就是draw会调用onDraw。
- 一般情况下,直接用onDraw绘制view的content就可以了,如果绘制多一点的内容,可以调用draw(),不过Android官方推荐用只用onDraw就可以了。
- ondraw() 和dispatchdraw()的区别?绘制VIew本身的内容,通过调用View.onDraw(canvas)函数实现,绘制自己的孩子通过dispatchDraw(canvas)实现。
04.Draw绘制注意事项
- 不要在View绘制和做布局操作的时候实例化数据,将创建对象等这些分配内存资源和会引起垃圾回收机制的操作在onDraw/onLayout之前进行。因为在View及其子类的onDraw方法,会实时调用以更新界面,会频繁的创建对象和进行垃圾回收,垃圾回收的GC线程会抢占CPU资源影响UI的显示性能,这样一个显示很顺畅的用户界面就会因对象分配引起的一些垃圾回收机制进行短暂的停滞。
05.draw绘制闪烁
06.draw方法源码
- View类中的draw方法源码,如下所示
/** * 源码分析:draw() * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。 * 绘制过程: * 1. 绘制view背景 * 2. 绘制view内容 * 3. 绘制子View * 4. 绘制装饰(渐变框,滑动条等等) * 注: * a. 在调用该方法之前必须要完成 layout 过程 * b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法) * c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制 * d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制 */ public void draw(Canvas canvas) { ...// 仅贴出关键代码 int saveCount; // 步骤1: 绘制本身View背景 if (!dirtyOpaque) { drawBackground(canvas); } // 若有必要,则保存图层(还有一个复原图层) // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过 // 因此在绘制时,节省 layer 可以提高绘制效率 final int viewFlags = mViewFlags; if (!verticalEdges && !horizontalEdges) { // 步骤2:绘制本身View内容 if (!dirtyOpaque) onDraw(canvas); // View 中:默认为空实现,需复写 // ViewGroup中:需复写 // 步骤3:绘制子View // 由于单一View无子View,故View 中:默认为空实现 // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写 dispatchDraw(canvas); // 步骤4:绘制装饰,如滑动条、前景色等等 onDrawScrollBars(canvas); return; } ... } //下面,我们继续分析在draw()中4个步骤调用的drawBackground()、 // onDraw()、dispatchDraw()、onDrawScrollBars(canvas) /** * 步骤1:drawBackground(canvas) * 作用:绘制View本身的背景 */ private void drawBackground(Canvas canvas) { // 获取背景 drawable final Drawable background = mBackground; if (background == null) { return; } // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界 setBackgroundBounds(); ..... // 获取 mScrollX 和 mScrollY值 final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { // 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移 canvas.translate(scrollX, scrollY); // 调用 Drawable 的 draw 方法绘制背景 background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } /** * 步骤2:onDraw(canvas) * 作用:绘制View本身的内容 * 注: * a. 由于 View 的内容各不相同,所以该方法是一个空实现 * b. 在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容 * c. 谨记:自定义View中 必须 且 只需复写onDraw() */ protected void onDraw(Canvas canvas) { ... // 复写从而实现绘制逻辑 } /** * 步骤3: dispatchDraw(canvas) * 作用:绘制子View * 注:由于单一View中无子View,故为空实现 */ protected void dispatchDraw(Canvas canvas) { ... // 空实现 } /** * 步骤4: onDrawScrollBars(canvas) * 作用:绘制装饰,如 滚动指示器、滚动条、和前景等 */ public void onDrawForeground(Canvas canvas) { onDrawScrollIndicators(canvas); onDrawScrollBars(canvas); final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (foreground != null) { if (mForegroundInfo.mBoundsChanged) { mForegroundInfo.mBoundsChanged = false; final Rect selfBounds = mForegroundInfo.mSelfBounds; final Rect overlayBounds = mForegroundInfo.mOverlayBounds; if (mForegroundInfo.mInsidePadding) { selfBounds.set(0, 0, getWidth(), getHeight()); } else { selfBounds.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); } final int ld = getLayoutDirection(); Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(), foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld); foreground.setBounds(overlayBounds); } foreground.draw(canvas); } }
07.ViewGroup中draw源码
- 跟View的draw一样,只是多了一个dispatchDraw,绘制步骤一样。具体就看一下dispatchDraw方法
/** * 源码分析:dispatchDraw() * 作用:遍历子View & 绘制子View * 注: * a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法 * b. View中默认为空实现(因为没有子View可以去绘制) */ protected void dispatchDraw(Canvas canvas) { ...... // 1. 遍历子View final int childrenCount = mChildrenCount; ...... for (int i = 0; i < childrenCount; i++) { ...... if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { // 2. 绘制子View视图 ->>分析1 more |= drawChild(canvas, transientChild, drawingTime); } .... } } /** * 分析1:drawChild() * 作用:绘制子View */ protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // 最终还是调用了子 View 的 draw ()进行子View的绘制 return child.draw(canvas, this, drawingTime); }
09.Draw绘制圆环
9.1 需求介绍
- 绘制圆环,一个实心中心圆,还有一个外圆环
- 此控件可以设置宽度和高度,可以设置颜色
9.2 思路介绍
- 3.2.1 既然是绘制圆形,可以写一个继承View的自定义view
- 3.2.2 重写onDraw方法,获取控件宽高,然后比较宽高值,取小值的一半作为圆的半径
- 3.2.3 然后分别绘制选中状态和未选中状态的圆
- 3.2.4 创建画笔Paint,并且设置相关属性,比如画笔颜色,类型等
- 3.2.5 利用canvas绘制圆,然后再又用相同方法绘制外边缘
- 3.2.6 自定义一个是否选中状态的方法,传入布尔值是否选中,然后调用view中invalidate方法
9.3 代码介绍
- 具体代码如下所示:
/** * <pre> * @author yangchong * blog : https://github.com/yangchong211 * time : 2016/5/18 * desc : 红点自定义控件 * revise: 建议设置红点宽高一样,否则是椭圆 * </pre> */ public class DotView extends View { private boolean isInit = false; private boolean isSelected = false; private float mViewHeight; private float mViewWidth; private float mRadius; private Paint mPaintBg = new Paint(); private int mBgUnselectedColor = Color.parseColor("#1A000000"); private int mBgSelectedColor = Color.parseColor("#FDE26E"); private static final float mArcWidth = 2.0f; public DotView(Context context) { super(context); } public DotView(Context context, AttributeSet attrs) { super(context, attrs); } public DotView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!isInit) { isInit = true; mViewHeight = getHeight(); mViewWidth = getWidth(); if (mViewHeight >= mViewWidth) { mRadius = mViewWidth / 2.f; } else { mRadius = mViewHeight / 2.f; } } //是否选中 if (isSelected){ drawSelectedDot(canvas); } else{ drawUnSelectedDot(canvas); } } /** * 绘制选中指示器红点 * @param canvas canvas */ private void drawSelectedDot(Canvas canvas) { //设置paint相关属性 mPaintBg.setAntiAlias(true); mPaintBg.setColor(mBgSelectedColor); mPaintBg.setStyle(Style.FILL); //绘制圆 canvas.drawCircle(mViewWidth / 2.f, mViewHeight / 2.f, mRadius - 8.f, mPaintBg); mPaintBg.setStyle(Style.STROKE); float offset = 1.f + mArcWidth; RectF oval = new RectF(mViewWidth / 2.f - mRadius + offset, mViewHeight / 2.f - mRadius + offset, mViewWidth / 2.f + mRadius - offset, mViewHeight / 2.f + mRadius - offset); //绘制指定的弧线,该弧线将被缩放以适应指定的椭圆形。 canvas.drawArc(oval, 0.f, 360.f, false, mPaintBg); } /** * 绘制未选中指示器红点 * @param canvas canvas */ private void drawUnSelectedDot(Canvas canvas) { mPaintBg.setAntiAlias(true); mPaintBg.setColor(mBgUnselectedColor); mPaintBg.setStyle(Style.FILL); canvas.drawCircle(mViewWidth / 2.f, mViewHeight / 2.f, mRadius - 8.f, mPaintBg); } /** * 设置是否选中 * @param isSelected isSelected */ public void setIsSelected(boolean isSelected) { this.isSelected = isSelected; //使整个视图无效。如果视图是可见的,则{@link#onDraw(android.Graphics.Canvas)}将在将来的某个时候被调用。 //调用该方法,会进行重新绘制,也就是调用onDraw方法 this.invalidate(); } }
10.绘制圆形ImageView
10.1 需求分析
- 1.业务需求:可以设置圆角,可以设置圆形,如果是圆角则必须设置半径,默认圆角半径为10dp
- 2.如果设置了圆形,则即使设置圆角也无效;如果设置非圆形,则圆角生效,同时需要判断圆角半径是否大于控件宽高,处理边界逻辑
- 3.当设置圆形的时候,即使设置宽高不一样,那么取宽高中的最小值的一半为圆形半径
10.2 代码介绍
- 代码如下所示
public class ARoundImageView extends AppCompatImageView { /* * Paint:画笔 * Canvas:画布 * Matrix:变换矩阵 * * 业务需求:可以设置圆角,可以设置圆形,如果是圆角则必须设置半径,默认圆角半径为10dp */ /** * 圆形模式 */ private static final int MODE_CIRCLE = 1; /** * 普通模式 */ private static final int MODE_NONE = 0; /** * 圆角模式 */ private static final int MODE_ROUND = 2; /** * 圆角半径 */ private int currRound = dp2px(10); /** * 画笔 */ private Paint mPaint; /** * 默认是普通模式 */ private int currMode = 0; public ARoundImageView(Context context) { this(context,null); } public ARoundImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ARoundImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); obtainStyledAttrs(context, attrs, defStyleAttr); initViews(); } private void obtainStyledAttrs(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ARoundImageView, defStyleAttr, 0); currMode = a.hasValue(R.styleable.ARoundImageView_type) ? a.getInt(R.styleable.ARoundImageView_type, MODE_NONE) : MODE_NONE; currRound = a.hasValue(R.styleable.ARoundImageView_radius) ? a.getDimensionPixelSize(R.styleable.ARoundImageView_radius, currRound) : currRound; a.recycle(); } private void initViews() { //ANTI_ALIAS_FLAG 用于绘制时抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); } /** * 当模式为圆形模式的时候,我们强制让宽高一致 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (currMode == MODE_CIRCLE) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int result = Math.min(getMeasuredHeight(), getMeasuredWidth()); // 此方法必须由{@link#onMeasure(int,int)}调用,以存储已测量的宽度和测量的高度。 // 如果不这样做,将在测量时触发异常。 setMeasuredDimension(result, result); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { //获取ImageView图片资源 Drawable mDrawable = getDrawable(); //获取Matrix对象 Matrix mDrawMatrix = getImageMatrix(); if (mDrawable == null) { return; } if (mDrawable.getIntrinsicWidth() == 0 || mDrawable.getIntrinsicHeight() == 0) { return; } if (mDrawMatrix == null && getPaddingTop() == 0 && getPaddingLeft() == 0) { mDrawable.draw(canvas); } else { final int saveCount = canvas.getSaveCount(); canvas.save(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (getCropToPadding()) { final int scrollX = getScrollX(); final int scrollY = getScrollY(); canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(), scrollX + getRight() - getLeft() - getPaddingRight(), scrollY + getBottom() - getTop() - getPaddingBottom()); } } canvas.translate(getPaddingLeft(), getPaddingTop()); switch (currMode){ case MODE_CIRCLE: Bitmap bitmap1 = drawable2Bitmap(mDrawable); mPaint.setShader(new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint); break; case MODE_ROUND: Bitmap bitmap2 = drawable2Bitmap(mDrawable); mPaint.setShader(new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); canvas.drawRoundRect(new RectF(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()), currRound, currRound, mPaint); break; case MODE_NONE: default: if (mDrawMatrix != null) { canvas.concat(mDrawMatrix); } mDrawable.draw(canvas); break; } canvas.restoreToCount(saveCount); } } /** * drawable转换成bitmap */ private Bitmap drawable2Bitmap(Drawable drawable) { if (drawable == null) { return null; } Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); //根据传递的scaleType获取matrix对象,设置给bitmap Matrix matrix = getImageMatrix(); if (matrix != null) { canvas.concat(matrix); } drawable.draw(canvas); return bitmap; } private int dp2px(float value) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics()); } }
其他介绍
01.关于博客汇总链接
02.关于我的博客
- 我的个人站点:
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 简书:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
- 开源中国:https://my.oschina.net/zbj1618/blog
- 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e