TV端开发之选中放大效果实现

Android TV端开发从0到1的一点经验

Posted by lx8421bcd on December 7, 2018

选中控件高亮/放大是TV端中极其常见的交互效果
选中放大效果
我们知道Android 5.0之后在系统级渲染上引入了Z轴的概念,那么想要选中放大直接scale + 抬高Z轴不就好了嘛

    cardView.setScaleX(1.2f);
    cardView.setScaleY(1.2f);
    cardView.setElevation(ScreenUtil.dp2px(6));

此题结束,下一题!

……

当然没这么简单,除非你的app设定 minSdkVersion=21 ,但这目前是不可能的,之前我们提到过,AndroidTV端pre5.0的用户占比仍然是很高的,做任何效果都必须考虑Android 4.4及以下版本的兼容,elevation和translationZ在pre5.0上是不会生效的,所以我们要另寻他路。

为什么前面要提Z轴的事,因为选中放大直接scale会出现遮挡的问题,即View的渲染是有先后顺序的,后渲染的会将先渲染的控件遮挡,这个顺序基本按照xml布局中从上到下,列表中从前到后来推断。
TV端的控件选中是很有规律的,只要是选中的目标,必然拥有焦点,我们要实现选中放大也必然要在焦点上做文章,就是一点:

拥有焦点的控件最后渲染

有了方向,就该找方法了,很幸运的是Android ViewGroup给我们提供了决定View渲染顺序的方法:getChildDrawingOrder()

// code in ViewGroup.java

    /**
    * Returns the index of the child to draw for this iteration. Override this
    * if you want to change the drawing order of children. By default, it
    * returns i.
    * <p>
    * NOTE: In order for this method to be called, you must enable child ordering
    * first by calling {@link #setChildrenDrawingOrderEnabled(boolean)}.
    *
    * @param i The current iteration.
    * @return The index of the child to draw this iteration.
    *
    * @see #setChildrenDrawingOrderEnabled(boolean)
    * @see #isChildrenDrawingOrderEnabled()
    */
    protected int getChildDrawingOrder(int childCount, int i) {
        return i;
    }

我们要做的事,就是在需要实现选中放大效果的ViewGroup(LinearLayout, RelativeLayout, RecyclerView…etc.)内部重写此方法,并且按照源码注释说明,调用setChildrenDrawingOrderEnabled(true)开启自定义渲染顺序。

/* 以RelativeLayout为例 */
public class TVRelativeLayout extends RelativeLayout {
    public TVRelativeLayout(Context context) {
        super(context);
        init();
    }

    public TVRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TVRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setChildrenDrawingOrderEnabled(true);

    }

    /* 重写此方法,记录焦点控件在ViewGroup中的position,并将其与本该最后渲染的View对调 */
    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        View focusedChild = getFocusedChild();
        int focusViewIndex = indexOfChild(focusedChild);
        if (focusViewIndex == -1) {
            return i;
        }

        if (focusViewIndex == i) {
            return childCount - 1;
        } else if (i == childCount - 1) {
            return focusViewIndex;
        } else {
            return i;
        }
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, focused);
        invalidate();
    }
}

在经过以上操作之后,这个自定义的RelativeLayout会自动将获取焦点的child放到最后一个渲染,这样在焦点Child被放大时,就不会被其它控件遮住,其它ViewGroup实现方式原理相同。