专栏首页码上积木原来可以这么操作—修改子View绘制顺序

原来可以这么操作—修改子View绘制顺序

前言

周一的早上,由于项目的开发做的差不多了,正在等待测试结果的我就开始发呆,思考端午节的去处~

“理塘?稻城?还是...解决bug?”

不行不行?‍♂️,可不能让BUG耽误了我的假期,于是我打开公司的项目管理工具,看看测试有没有反馈问题。果然,出现了一个BUG。

大概就是一个RecycleView,需要把其中某一项做放大效果,类似焦点放大的效果。

但是现在的APP中显示效果是会被下一个View遮挡住,我简单写了个Demo说明:

正常的效果应该是Item4的View做放大效果,处在item3item5的上一层。

但是现在的效果是Item4Item3的上面,Item5又在Item4的上面,所以放大的Item4被遮挡住了。

这是什么问题呢?

写个Demo

首先,我们写个Demo复制下BUG出现的页面:

//初始化RecycleView
var adapter = TestAdapter()
rv.layoutManager = LinearLayoutManager(this)
rv.adapter = adapter

//修改数据源
var list = mutableListOf<String>()
for (number in 0..10) {
   list.add("item$number")
}
adapter.addData(list)

//Adapter类
class TestAdapter() : RecyclerView.Adapter<TestAdapter.ViewHolder>(){
    var dataList = mutableListOf<String>()

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
        var tvName = itemView.findViewById<TextView>(R.id.item_name)

        fun bind(str: String,position: Int){
            tvName.text=str

            if (position==4){
                //Item4放大两倍
                itemView.scaleX=2f
                itemView.scaleY=2f
                itemView.setBackgroundColor(Color.RED)
            }else{
                itemView.scaleX=1f
                itemView.scaleY=1f
                itemView.setBackgroundColor(Color.GREEN)
            }
        }
    }

思考原因

把放假的事情放到一边,我慢慢理清了头绪:

由于RecycleView是一个ViewGroup,所以也会按顺序一个个绘制子View,也就是按照顺序调用childView的draw方法。

所以在这个案例中,正常的绘制顺序就是:

Item0 -> Item1 ..Item3 -> Item4 -> Item5 ...

所以被放大的Item4自然也就处在Item3的上层,但会被Item5遮挡。

那怎么解决呢?

“如果能修改RecycleView的子View绘制顺序就好了~” 脑中突然浮现出这样的一句话。

对哦,如果能修改子View绘制顺序,让Item4Item3Item5之后进行绘制,那么就不会被遮挡了。

但是,真的能修改吗?

再看draw方法

这个问题本质上还是涉及到ViewGroup子View 绘制问题,所以我们再次回顾View的draw方法:

//View.java
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        // Step 1, draw the background, if needed
        drawBackground(canvas);

        // Step 2, save the canvas' layers
        canvas.saveUnclippedLayer..

        // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        canvas.drawRect..

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

    }

其中第三步大家都很熟悉,就是绘制View本身的onDraw方法,而在此之后,就是dispatchDraw方法,根据注释我们得知,这个方法就是用来绘制子View的。

//View.java
    protected void dispatchDraw(Canvas canvas) {

    }

在View中是一个空实现,既然是绘制子View,那么肯定会发生在ViewGroup中,所以我们大胆的猜测,在ViewGroup中应该对这个方法进行了重写:

//ViewGroup.java
    @Override
    protected void dispatchDraw(Canvas canvas) {
        //1、是否使用渲染节点
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        //2、预排序列表
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        //3、是否自定义顺序
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        //4、遍历子View
        for (int i = 0; i < childrenCount; i++) {
            //5、获取当前需要绘制的View序号
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            //根据序号获取View
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                //6、绘制子view
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    }

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

代码进行了精简,我们一步步来看:

  • 1、这里有一个属性叫做usingRenderNodeProperties,我们暂且不用管它是什么,只知道默认为true即可。
  • 2、初始化preorderedList,这个我们今天也用不到,由于usingRenderNodeProperties为ture,所以这个列表为null
  • 3、这个属性就很重要了,customOrder——是否自定义顺序,看似和我们的需求对应上了。它的值由两者决定:preorderedList == null 并且 isChildrenDrawingOrderEnabled。前者我们知道为true,所以后者isChildrenDrawingOrderEnabled就是我们待会需要关注的。
  • 4、开始遍历子View,注意这里的i还是正常的顺序,会从0一直遍历到childrenCount-1。
  • 5、获取View对应的序号,所以获取子View并没有直接用遍历中的i,而是通过getAndVerifyPreorderedIndex方法再次获取子View的Index,然后再获取子View。
  • 6、最后获取子View后,就开始调用drawChild也就是child.draw方法进行子View的绘制,这样绘制就传递到子View了。

通过上面的讲解,我们知道了重点就在于获取当前需要绘制View的对应Index方法中,也就是方法getAndVerifyPreorderedIndex

//获取子View的序号Index
    private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
        final int childIndex;
        if (customOrder) {
            final int childIndex1 = getChildDrawingOrder(childrenCount, i);
            childIndex = childIndex1;
        } else {
            childIndex = i;
        }
        return childIndex;
    }

    protected int getChildDrawingOrder(int childCount, int drawingPosition) {
        return drawingPosition;
    }

//根据子View的序号Index获取子View
    private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
            int childIndex) {
        final View child;
        if (preorderedList != null) {
            child = preorderedList.get(childIndex);
        } else {
            child = children[childIndex];
        }
        return child;
    }
  • getAndVerifyPreorderedIndex方法负责返回子View的序号。
  • getAndVerifyPreorderedView方法按照序号childIndex,从children中取出序号对应的View。

customOrder为true的时候,返回的view序号会被设置为getChildDrawingOrder方法的结果,否则就是按照正常的顺序序号,也就是i作为返回结果。

getChildDrawingOrder方法默认情况下也是返回的参数drawingPosition,也就是正常顺序序号i

但是,我们可以通过重写改变getChildDrawingOrder方法的返回结果,比如说:

  • 当传入的正常View顺序是0,然后我们重写返回View序号为childCount-1
  • 当传入的正常View顺序是childCount-1,然后我们重写返回View序号为0
  • 其他情况正常返回

这样就能让原本在第一个绘制的View最后一个绘制的View进行了顺序调换。

当然,getChildDrawingOrder方法能运行的前提是customOrder为true,而customOrder为true的前提是isChildrenDrawingOrderEnabled方法返回true。

final boolean customOrder = isChildrenDrawingOrderEnabled();

protected boolean isChildrenDrawingOrderEnabled() {
    return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER;
}

所以,我们就能得出修改ViewGroup子View绘制顺序的基本方法了,主要有两步:

  • 1、重写isChildrenDrawingOrderEnabled方法,返回true。(可以直接通过调用setChildrenDrawingOrderEnabled(true)方法来完成)
  • 2、重写getChildDrawingOrder方法,返回当前顺序下需要进行绘制的View序号。
    setChildrenDrawingOrderEnabled(true)

    override fun getChildDrawingOrder(childCount: Int, drawingPosition: Int): Int {
        if (drawingPosition == 0) {
            return childCount - 1
        } else if (drawingPosition == childCount - 1) {
            return 0
        } else {
            return drawingPosition
        }
    }

RecycleView 中的优化?

回到我们的需求,根据上述的分析,我们是不是需要自定义一个RecycleView,然后重写isChildrenDrawingOrderEnabledgetChildDrawingOrder 两个方法呢?

并不需要,RecycleView已经为我们提供了API,那就是setChildDrawingOrderCallback方法:

    public void setChildDrawingOrderCallback(@Nullable ChildDrawingOrderCallback childDrawingOrderCallback) {
        mChildDrawingOrderCallback = childDrawingOrderCallback;
        setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null);
    }

    public interface ChildDrawingOrderCallback {
        int onGetChildDrawingOrder(int childCount, int i);
    }

可以看到,如果我们调用setChildDrawingOrderCallback方法,并且传入一个不为空的ChildDrawingOrderCallback,那么就会调用setChildrenDrawingOrderEnabled(true)来完成修改绘制顺序的第一步了,也就是保证customOrder为true。

那么getChildDrawingOrder方法是怎么和ChildDrawingOrderCallback回调方法产生联系的呢?

很明显,RecycleView肯定是重写了getChildDrawingOrder方法:

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (mChildDrawingOrderCallback == null) {
            return super.getChildDrawingOrder(childCount, i);
        } else {
            return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i);
        }
    }

所以getChildDrawingOrder方法的结果就等于回调方法ChildDrawingOrderCallback.onGetChildDrawingOrder

至此,修改子View绘制顺序的两步都完成了,通过RecycleView的setChildDrawingOrderCallback即可完成。

验证时刻

终于,一切可以尘埃落定了,接下来就是见证我们分析结果的时刻,来修改Demo:

        rv.setChildDrawingOrderCallback(object : RecyclerView.ChildDrawingOrderCallback {
            override fun onGetChildDrawingOrder(childCount: Int, i: Int): Int {
                if (i < 4) {
                    return i
                } else if (i < childCount - 1) {
                    return i + 1
                } else {
                    //最后绘制Item4
                    return 4
                }
            }
        })

运行:

结束了?并没有

到此,我们的BUG是解决了,但是,关于绘制顺序的知识点我们可以再做下延伸。

在搜索getAndVerifyPreorderedIndex方法的过程中,我发现了另外一处也用到了getAndVerifyPreorderedIndex方法:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                
                //和绘制顺序几乎一样的代码,获取子View的index,然后获取子View
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                final View[] children = mChildren;
                //遍历子View
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);
                    //事件向下传递给子View        
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        break;
                    }
                }
            }
        }
    }

惊不惊喜,意不意外,在负责事件分发的dispatchTouchEvent方法中,我们找到了和绘制子View几乎一样的代码。

同样的通过getAndVerifyPreorderedIndex方法获取子View的序号,然后获取序号对应的子View。

有的朋友可能会疑惑,这里的preorderedList好像不为null了呢?直接赋值的buildTouchDispatchChildList方法?那我们就进去看看这个方法:

    public ArrayList<View> buildTouchDispatchChildList() {
        return buildOrderedChildList();
    }

    ArrayList<View> buildOrderedChildList() {
        final int childrenCount = mChildrenCount;
        if (childrenCount <= 1 || !hasChildWithZ()) return null;

        final boolean customOrder = isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            // add next child (in child order) to end of list
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View nextChild = mChildren[childIndex];
            final float currentZ = nextChild.getZ();

            // insert ahead of any Views with greater Z
            int insertIndex = i;
            while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                insertIndex--;
            }
            mPreSortedChildren.add(insertIndex, nextChild);
        }
        return mPreSortedChildren;
    }

可以看到,第二句有一个判断:

    if (childrenCount <= 1 || !hasChildWithZ()) return null;

    private boolean hasChildWithZ() {
        for (int i = 0; i < mChildrenCount; i++) {
            if (mChildren[i].getZ() != 0) return true;
        }
        return false;
    }
  • childrenCount <= 1 肯定是不成立的。
  • !hasChildWithZ() 这个一般情况下都是为true的,因为一般View是没有Z轴位置的(需要setZ方法设置z轴坐标)。

所以在事件分发的子View遍历中,preorderedList还是为null,所以和上述的子View绘制逻辑是一模一样的,还是靠isChildrenDrawingOrderEnabled方法和getChildDrawingOrder方法来完成修改事件分发的子View遍历顺序。

总结

  • 1、ViewGroup可以通过调用setChildrenDrawingOrderEnabled(true)方法,以及重写getChildDrawingOrder方法修改子View绘制顺序。
  • 2、RecycleView中将两者进行了封装,只需要调用setChildDrawingOrderCallback方法即可完成修改子View绘制顺序的需求。
  • 3、事件分发的过程中,遍历子View的顺序和绘制子View的顺序获取机制是相同的。

(这里要注意,只是两者的顺序获取机制是相同的,都是通过getChildDrawingOrder方法获取,但是两者顺序并不是完全相同的。因为事件分发中遍历子View是倒序的,也就是从最后一个View开始遍历。而绘制子View的顺序是正序,也就是从第一个View开始遍历)

  • 4、所以在我们修改子View绘制顺序的同时,其实也修改了事件分发的子View遍历顺序

参考

https://www.wanandroid.com/wenda/show/8852

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日一个知识点,建立完整体系架构。

本文分享自微信公众号 - 码上积木(Lzjimu),作者:积木zz

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 附详尽答案,新版精选Android中高级面试题--二

    链接:https://juejin.im/post/5c8211fee51d453a136e36b0

    陈宇明
  • Android组件View绘制流程原理分析

    如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是Dec...

    Anymarvel
  • Android面试常问基础知识点(附详细解答)

    1)Activity:用户可操作的可视化界面,为用户提供一个完成操作指令的窗口。一个Activity通常是一个单独的屏幕,Activity通过Intent来进行...

    Demo_Yang
  • Android编程实现3D立体旋转效果的实例代码

    说明:之前在网上到处搜寻类似的旋转效果 但搜到的结果都不是十分满意 原因不多追述(如果有人找到过相关 比较好的效果 可以发一下连接 一起共同进步)

    砸漏
  • SVG的动态之美-搜狗地铁图重构散记

    寒月十八
  • 这可能是2020大小厂问的最经典的Android面试题了——事件分发机制、View渲染过程

    Activity和View只有两个方法控制事件传递:dispatchTouchEvent(),onTouchEvent ();

    Android技术干货分享
  • Android 性能优化典范

    2015年伊始,Google发布了关于Android性能优化典范的专题, 一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App。课...

    Android架构
  • iOS面试题-UI篇

    面试题持续整理更新中,如果你正在面试或者想一起进阶,不妨添加一下交流群1012951431一起交流。

    会写bug的程序员
  • android之View绘制

    Android系统的视图结构的设计也采用了组合模式,即View作为所有图形的基类,Viewgroup对View继承扩展为视图容器类,由此就得到了视图部分的基本...

    xiangzhihong
  • Android自定义控件总结

    六月的雨
  • Android性能优化典范(1)

    2015年伊始,Google发布了关于Android性能优化典范的专题,一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App。课程...

    WeTest质量开放平台团队
  • Android应用优化之流畅度实操

    用户1269200
  • HenCoder Android 自定义 View 1-8 硬件加速

    硬件加速这个词每当被提及,很多人都会感兴趣。这个词给大部分人的概念大致有两个:快速、不稳定。对很多人来说,硬件加速似乎是一个只可远观而不可亵玩的高端科技:是,我...

    扔物线
  • 解决 APP启动白屏黑屏问题

    闪屏页,我们手机上的每个 APP 几乎都有自己的闪屏页,就是在真正进入程序前,会有一个页面停顿几秒钟。其实我们完全可以充分利用好这几秒钟做很多的程序初始化了启动...

    开发者
  • android自定义控件一站式入门

    Android系统提供了一系列UI相关的类来帮助我们构造app的界面,以及完成交互的处理。 一般的,所有可以在窗口中被展示的UI对象类型,最终都是继承自Vie...

    用户1172465
  • android自定义控件一站式入门

    TODO: 待整理 自定义控件 Android系统提供了一系列UI相关的类来帮助我们构造app的界面,以及完成交互的处理。 一般的,所有可以在窗口中被展示的U...

    用户1172465
  • Android 开发艺术探索笔记一

    ViewRoot对应于ViewRootImpl类,它是连接windowmanager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成...

    Yif
  • Android Heroes Reading Notes 3

    《Android群英传》读书笔记 (3) 第六章 Android绘图机制与处理技巧 + 第七章 Android动画机制与使用技巧

    宅男潇涧
  • HenCoder Android 自定义 View 1-5: 绘制顺序

    之前的内容在这里: HenCoder Android 开发进阶 自定义 View 1-1 绘制基础 HenCoder Android 开发进阶 自定义 View...

    扔物线

扫码关注云+社区

领取腾讯云代金券