Android- 记一次解决问题的过程:从源码中分析永远是解决问题的最有效方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP&& !getStickyChildren().isEmpty()) {tempViews = getChildren();if (tempViews != null) {// 修改 mChildrensetChildren(sortViews(tempViews.length));}}
super.draw(canvas);
// 兼容 5.0 以下吸顶功能 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP&& !getStickyChildren().isEmpty() && tempViews != null) {// 恢复 mChildrensetChildren(tempViews);}}
// 返回排序后的 children 数组 private View[] sortViews(int size) {View[] views = new View[size];int index = 0;int count = getChildCount();for (int i = 0; i < count; i++) {View child = getChildAt(i);// 普通 viewif (!isStickyChild(child)) {views[index] = child;index++;}}
for (int i = 0; i < count; i++) {View child = getChildAt(i);// 吸顶 viewif (isStickyChild(child)) {views[index] = child;index++;}}return views;}
修改好,运行测试一下,当 view 吸顶时,能正常显示在最上层,不会被下面的 view 覆盖了,好像问题已经完美解决了。可是当我点击界面上的控件时,新的问题出现了,我点击的 view 和响应的 view 不是同一个,事件的传递乱了。因为我们把 view 的绘制顺序改变了,所以我们实际看到的、操作的 view,跟系统判断的可能不是同一个 view 了。显然这种解决方法引发了新的问题,是不可取的。
分析源码
既然通过修改 mChildren 的方法行不通,只能另寻方案。我尝试跟踪 view 的绘制源码,期待能有一些新思路。ViewGroup 绘制子
view 的源码调用路径是:draw()-->dispatchDraw()。ViewGroup 中的 dispatchDraw()方法是绘制子 view 的关键代码,通过阅读源码,我发现了几句关键代码。
@Overrideprotected void dispatchDraw(Canvas canvas) {
// step 1:获取预定义的排序列表 final ArrayList<View> preorderedList = usingRenderNodeProperties? null : buildOrderedChildList();
// step 2:判断是否需要自定义排序 final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {// step 3:根据绘制顺序获取 view 下标 final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);// step 4:根据下标获取子 viewfinal View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {// step 5:绘制子 viewmore |= drawChild(canvas, child, drawingTime);}}}
第一步:获取预定义的排序列表。如果开启了硬件加速 usingRenderNodeProperties 为 true,preorderedList 为 null。否则执行 buildOrderedChildList()方法,这个方法大部分情况下也直接返回 null,所以 preorderedList 一般都是 null 的。buildOrderedChildList()方法只有在没有设置硬件加速,并且子 view 设置了 Z 轴高度的情况下才不会返回 null。我们知道,Android 4.0 后,默认都是开启硬件加速的,而 5.0 前,是不支持 view 的 Z 轴的,所以只有在 5.0 后关闭硬件加速,并且设置了子 view 的 Z 轴,buildOrderedChildList()方法才不会返回 null,这个方法就是处理这种情况的,而且它对 view 的排序处理跟我们下面分析的逻辑基本一样,所以这个方法我们可以忽略不看。
第二步:判断是否需要自定义排序。既然 preorderedList 为 null,那么是否需要自定义排序的判断就是 isChildrenDrawingOrderEnabled()方法,这个方法默认为 false,只有设置为 true,自定义的排序才生效,这是我们需要关注的第一个方法。
第三步:根据绘制顺序获取 view 下标。直接看代码:
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {final int childIndex;if (customOrder) {// 如果自定义排序,根据顺序获取 view 下标 final int childIndex1 = getChildDrawingOrder(childrenCount, i);if (childIndex1 >= childrenCount) {throw new IndexOutOfBoundsException("getChildDrawingOrder() "
"returned invalid index " + childIndex1
" (child count is " + childrenCount + ")");}childIndex = childIndex1;} else {// 不是自定义排序,下标和顺序一致 childIndex = i;}return childIndex;}
在这个方法里,如果不排序,返回的下标和顺序一样,所以默认绘制顺序就是 view 的添加顺序。如果需要排序,通过 getChildDrawingOrder 获取需要绘制的 view 的下标,绘制顺序由这个方法的返回值决定。
protected int getChildDrawingOrder(int childCount, int drawingPosition) {return drawingPosition;}
可以看到,这个方法的返回值依然是顺序本身,所以它的默认绘制顺序也 view 的添加顺序。但是这个方法是 protected,也就是说我们可以覆写这个方法,返回我们想要的 index,改变 view 的绘制顺序。这是我们需要关注的第二个方法。
第四步:根据下标,调用 getAndVerifyPreorderedView 或者需要绘制的子 view。
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,int childIndex) {final View child;if (preorderedList != null) {child = preorderedList.get(childIndex);if (child == null) {throw new RuntimeException("Invalid preorderedList contained null child at index "
childIndex);}} else {child = children[childIndex];}return child;}
这个方法很简单,就是根据下标或者 view,如果有预定义排序,就从 preorderedList 中获取,否则就从 children 数组获取,children 数组就是保存子 view 的数组,按添加顺序排列。
第五步:drawChild,就是调用 child 的 draw 方法绘制子 view。
最终实现
现在我们知道,想要改变 ViewGroup 的子 view 绘制顺序,只有开启自定义排序,并且覆写 getChildDrawingOrder 方法就可以了。
在自定义 ViewGroup 的构造方法中调用:
// 开启自定义排序 setChildrenDrawingOrderEnabled(true);复制代码
预先处理 view 的排序
// 保存预先处理的排序 private final List<View> mViews = new ArrayList<>();
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {
//忽略其他的代码
// 排序 sortViews();}
private void sortViews() {List<View> list = new ArrayList<>();int count = getChildCount();for (int i = 0; i < count; i++) {View child = getChildAt(i);// 添加非吸顶 viewif (!isStickyChild(child)) {list.add(child);}}
for (int i = 0; i < count; i++) {View child = getChildAt(i);// 添加吸顶 viewif (isStickyChild(child)) {list.add(child);}}mViews.clear();
评论