关于Android(安卓)draw中的画布的说明
网络上关于Android绘图的原理和流程已经说明的很到位了,这里就不再过多的做解释。这篇文章主要想谈一下关于draw方法中的画布的来源问题
有兴趣的大佬们可以继续看下去…
首先说明几点(一些基础还是要有的),view在经过measure后会获得自己的测量宽高,在经过layout之后, 父ViewGroup会通过onlayout为子View制定布局的上下左右坐标。所以说measure之后的测量高度不一定是实际高度。而view的实际高度是layout所指,我们可以通过layout方法中的setFrame的onSizeChanged来获取实际宽高。在一切的一切都完成,只剩绘图的时候,我们知道要调用draw(Canvas canvas)方法了。可是不知道大家对这个canvas这么来的有没有疑问。前几天看zxt的一个项目,里面在recyclerView中添加ItemDecoration,在自定义的ItemDecoration中使用了inflate一个布局,然后调用这个布局的绘制流程,最终画出来的东西会发现,和layout中设置的位置不匹配,永远都是画在最上面。
@Override public void onDrawOver(Canvas c, final RecyclerView parent, RecyclerView.State state) { View toDrawView = mInflater.inflate(R.layout.header_complex, parent, false); int toDrawWidthSpec;//用于测量的widthMeasureSpec int toDrawHeightSpec;//用于测量的heightMeasureSpec //拿到复杂布局的LayoutParams,如果为空,就new一个。 // 后面需要根据这个lp 构建toDrawWidthSpec,toDrawHeightSpec ViewGroup.LayoutParams lp = toDrawView.getLayoutParams(); Log.e("sss", "onDrawOver: "+lp.height+" "+lp.width,null ); if (lp == null) { lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);//这里是根据复杂布局layout的width height,new一个Lp toDrawView.setLayoutParams(lp); } if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { //如果是MATCH_PARENT,则用父控件能分配的最大宽度和EXACTLY构建MeasureSpec。 toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.EXACTLY); } else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { //如果是WRAP_CONTENT,则用父控件能分配的最大宽度和AT_MOST构建MeasureSpec。 toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST); } else { //否则则是具体的宽度数值,则用这个宽度和EXACTLY构建MeasureSpec。 toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY); } //高度同理 if (lp.height == ViewGroup.LayoutParams.MATCH_PARENT) { toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.EXACTLY); } else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.AT_MOST); } else { toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY); } //依次调用 measure,layout,draw方法,将复杂头部显示在屏幕上。 toDrawView.measure(toDrawWidthSpec, toDrawHeightSpec);// toDrawView.layout(parent.getPaddingLeft(), parent.getPaddingTop(),// parent.getPaddingLeft() + toDrawView.getMeasuredWidth(), parent.getPaddingTop() + toDrawView.getMeasuredHeight()); toDrawView.layout(0,100,1000,300); toDrawView.draw(c); }
以上是大佬zxt的源码。我们可以看见的是,虽然layout设置了距离上100,但是仍然没有留有空隙,如下图
因为我们使用的draw方法传递的canvas是整个recycleView的canvas,而子视图draw并不知道这是谁的,只知道传进来就是自己的,所以他就直接在上面绘制我们的图像。而不去管layout中定义的四边距,因此总是以左上角为原点。
我发现这个bug之后,就在想,子view既然能正确绘制的话,而且它的draw方法还只管画不管位置,那肯定在draw之前有人提前为子View设置好了画布,比如translate或者scale或者clip,总之通过处理将画布区域变成子View绘制区域。那么这么重要的责任是谁来承担的呢。想到这些F3进Viewgroup中看一下它的draw方法。draw的源码太长就不贴了,发现他的里面通过dispatchDraw()来向下分发子view的draw,那么跟踪进去
...final int childrenCount = mChildrenCount;final View[] children = mChildren;...//这里有个cliptoPadding属性也很有意思,通过在xml中设置Android:clipchildren属性可//以使child的视图画出限定的布局。这个不作介绍了。大家有兴趣可以百度clipToPadding if (clipToPadding) { clipSaveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); }...//从这里就能看出来了,开始遍历子view的集合,对每个子View执行drawChild(canvas, //transientChild, drawingTime)这个方法,所以说这个方法是关键。在dispatchDraw中//并没有我们想象中的对画布的剪裁。那么我们就跟进dispatchDraw中看一下。for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } }
drawChild方法源码:
/** * Draw one child of this View Group. This method is responsible for getting * the canvas in the right state. This includes clipping, translating so * that the child's scrolled origin is at 0, 0, and applying any animation * transformations. * * @param canvas The canvas on which to draw the child * @param child Who to draw * @param drawingTime The time at which draw is occurring * @return True if an invalidate() was issued */ /** * 短短这么几句话,是不是一下就轻松了。他返回了子View的draw方法。有人会说那不直 * 接调用了draw(canvas)么,父canvas直接传到子view里了,哪有什么剪裁的地方。 * 这就错了,仔细看的话这里draw的参数是3个而不是一个,也就是draw的一个3参数重载 * 我们先不管这些,看看上面官方给的注释。英语不好可以直接有道去,这里我说一下大概 * 意思: * 这个方法主要负责的是获得画布的正确状态,包括平移,缩放等 * 看到这句话,终于知道了就是在这里对画布进行了变换,跟进去。看一下 * */ protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
那么下面是draw的3参数实现:
/** * This method is called by ViewGroup.drawChild() to have each child view draw itself. * * This is where the View specializes rendering behavior based on layer type, * and hardware acceleration. */ boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ... ... int sx = 0; int sy = 0; if (!drawingWithRenderNode) { computeScroll(); // 如果滑动的话,保存一下滑动的X,Y这里向左是正,向上是正 sx = mScrollX; sy = mScrollY; } final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode; final boolean offsetForScroll = cache == null && !drawingWithRenderNode; int restoreTo = -1; if (!drawingWithRenderNode || transformToApply != null) { //保存了画布之前的状态,便于以后恢复 restoreTo = canvas.save(); } if (offsetForScroll) { //如果view滑动了,那么将画布平移到view的左边界-滑动的x坐标(向左为正),还有上边界-滑动的Y坐标() //终于找到了。大功告成! canvas.translate(mLeft - sx, mTop - sy); } else { //如果没有滑动呢,就直接将画布移动到子view的左上角,后面呢如果有缩放设置缩放,反正每一步变换都要save一下,可能自有道理吧! if (!drawingWithRenderNode) { canvas.translate(mLeft, mTop); } if (scalingRequired) { if (drawingWithRenderNode) { // TODO: Might not need this if we put everything inside the DL restoreTo = canvas.save(); } // mAttachInfo cannot be null, otherwise scalingRequired == false final float scale = 1.0f / mAttachInfo.mApplicationScale; canvas.scale(scale, scale); } } ... ... the more }
通过以上的方式呢,我们看到了,如果直接draw的话是不会考虑我们view的layout布局位置的呢,那么如何在给定一个view1的画布canvas,然后在正确的位置上画出一个不属于他的children的一个view2(也就是说如何在能再这个view的layout所保存的位置上绘制呢)我们就可以通过view1.drawChild(canvas,view2,0)的方法就可以了。那有人还说,最后不是调用3参数的draw么,那么我们也调用呗! 只可惜源码中定义的3参数draw方法是boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
是个非public方法我们没有办法去调用了。除非你反射!
我们修改一下之前的代码,再来看看效果!
@Override public void onDrawOver(Canvas c, final RecyclerView parent, RecyclerView.State state) { ... //这里的代码和文章开篇的是一样的,就省略了.. ... toDrawView.layout(0,100,1000,300); //只有这里更改了一下 由draw变为drawchild!!!! parent.drawChild(c,toDrawView,0) ;}
效果在这:的确满足之前layout的位置了。
更多相关文章
- Android中保存图片的两种方式
- 在mac上无法使用Android(安卓)Studio的解决方法
- Android开发:APP引导页启动页小Demo(实例)
- Android(安卓)项目中常用的页面切换TableLayout+Fragment+ViewPa
- Android事件分发机制以及滑动冲突处理
- android app两种调试方法
- (转)android AppWidgetProvider 定时刷新问题
- android调用JS失败时可能的原因
- 【Android进阶学习】设置透明效果的三种方法