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所保存的位置上绘制呢)我们就可以通过<font color=#0099ff size=5 face="黑体">view1.drawChild(canvas,view2,0)</font>的方法就可以了。那有人还说,最后不是调用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的位置了。


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容