Android TV RecyclerView焦点移动飞框的实现

上一篇文章讲了 Android TV 焦点移动飞框的实现 ,是在ViewGroup中实现的。这一篇文章主要在RecyclerView中实现焦点移动飞框,支持横向和竖向列表。实现效果如下图所示:

RecyclerView视图绑定

在Android TV端使用RecyclerView跟手机上的一样,都是通过绑定ViewHolder来实现视图的绑定,首先需要创建一个基类继承RecyclerView.ViewHolder,里面包含一个setUpView的抽象方法,让实现类在这个方法里面处理RecyclerView中Item视图的绑定:

public abstract  class BaseViewHolder<T> extends RecyclerView.ViewHolder {
    public View itemView;

    public BaseViewHolder(View itemView) {
        super(itemView);
    }

    public abstract void setUpView(T model, int position, RecyclerView.Adapter adapter);
}

然后创建一个MenuViewHolder子类继承BaseViewHolder基类,实现setUpView方法,setUpView方法有3个参数Movie model, final int position, RecyclerView.Adapter adapter,第一个参数是Bean模型,第二个参数是该Item在列表中的位置,第三个参数是与负责处理视图绑定的适配器类,在Adapter适配器类中的onBindViewHolder调用该方法实现视图的绑定和处理。:

public class MenuViewHolder extends BaseViewHolder {
    private TextView title;
    private TextView content;

    public MenuViewHolder(View itemView) {
        super(itemView);
        title= (TextView) itemView.findViewById(R.id.Menu_Title);
        content= (TextView) itemView.findViewById(R.id.Menu_Content);
        this.itemView=itemView;
        itemView.setFocusable(true);
    }

    @Override
    public void setUpView(Movie model, final int position, RecyclerView.Adapter adapter) {
        title.setText(model.getTitle());
        content.setText(model.getContent());
        itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View view, boolean b) {
                if(b){
                    focusStatus(view);
                }else{
                    normalStatus(view);
                }
            }
        });
        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//                Log.e("No"+position,"Click");
            }
        });
    }

    private void focusStatus(View itemView){
        if(itemView!=null){
            if(Build.VERSION.SDK_INT>=21){
//                ViewCompat.animate(itemView).scaleX(1.20f).scaleY(1.20f).translationZ(1).start();

                ObjectAnimator scaleX=ObjectAnimator.ofFloat(itemView,"scaleX",1.0f,1.20f);
                ObjectAnimator scaleY=ObjectAnimator.ofFloat(itemView,"scaleY",1.0f,1.20f);
                ObjectAnimator translationZ=ObjectAnimator.ofFloat(itemView,"translationZ",0f,1.0f);
                AnimatorSet animatorSet=new AnimatorSet();
                animatorSet.play(scaleX).with(scaleY).with(translationZ);
                animatorSet.setDuration(200);
                animatorSet.start();
            }else{
//                ViewCompat.animate(itemView).scaleX(1.20f).scaleY(1.20f).start();
                ObjectAnimator scaleX=ObjectAnimator.ofFloat(itemView,"scaleX",1.0f,1.20f);
                ObjectAnimator scaleY=ObjectAnimator.ofFloat(itemView,"scaleY",1.0f,1.20f);
                ObjectAnimator translationZ=ObjectAnimator.ofFloat(itemView,"translationZ",0f,1.0f);
                AnimatorSet animatorSet=new AnimatorSet();
                animatorSet.play(scaleX).with(scaleY).with(translationZ);
                animatorSet.setDuration(200);
                animatorSet.start();

                ViewGroup parent= (ViewGroup) itemView.getParent();
                parent.requestLayout();
                parent.invalidate();
            }
        }
    }

    private void normalStatus(View itemView){
        if(itemView!=null){
            if(Build.VERSION.SDK_INT>=21){
//                ViewCompat.animate(itemView).scaleX(1.0f).scaleY(1.0f).translationZ(0f).start();

                ObjectAnimator scaleX=ObjectAnimator.ofFloat(itemView,"scaleX",1.20f,1.0f);
                ObjectAnimator scaleY=ObjectAnimator.ofFloat(itemView,"scaleY",1.20f,1.0f);
                ObjectAnimator translationZ=ObjectAnimator.ofFloat(itemView,"translationZ",1.0f,0f);
                AnimatorSet animatorSet=new AnimatorSet();
                animatorSet.setDuration(200);
                animatorSet.play(scaleX).with(scaleY).with(translationZ);
                animatorSet.start();
            }else{
//                ViewCompat.animate(itemView).scaleX(1.0f).scaleY(1.0f).translationZ(0f).start();
                ObjectAnimator scaleX=ObjectAnimator.ofFloat(itemView,"scaleX",1.20f,1.0f);
                ObjectAnimator scaleY=ObjectAnimator.ofFloat(itemView,"scaleY",1.20f,1.0f);
                ObjectAnimator translationZ=ObjectAnimator.ofFloat(itemView,"translationZ",1.0f,0f);
                AnimatorSet animatorSet=new AnimatorSet();
                animatorSet.setDuration(200);
                animatorSet.play(scaleX).with(scaleY).with(translationZ);
                animatorSet.start();

                ViewGroup parent= (ViewGroup) itemView.getParent();
                parent.requestLayout();
                parent.invalidate();
            }

        }
    }
}

首先需要在MenuViewHolder的构造函数中实现视图的绑定,并设置该Item视图为可获得焦点的itemView.setFocusable(true)。然后在setUpView中处理与视图的填充,并在这里面实现得失焦点和点击的监听。Item中有两种状态,一种是获得焦点,一种是默认情况(没获得焦点),在同一时刻只能只有一个Item获得焦点。focusStatus方法是使获得焦点的View做放大并Z轴抬起的处理,normalStatus方法是使View重回默认状态,即做缩小并Z轴下降的处理。在这两个方法中使用ViewCompat.animate和ObjectAnimator都可以。

Adapter适配器实现

由于在onBindViewHolder中我们把与视图处理相关的逻辑都交给了ViewHolder中处理了,所以我们的Adapter很清爽:


public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {
    private List list;

    public MenuAdapter(List list){
        this.list=list;
    }

    @Override
    public MenuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MenuViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.recyclerview_item_menu,parent,false));
    }

    @Override
    public void onBindViewHolder(MenuViewHolder holder, int position) {
        holder.setUpView(list.get(position),position,this);
    }

    @Override
    public int getItemCount() {
        return list.size();
    }
}

Activity处理

由于这种效果类似于瀑布流,我们为RecyclerView使用了StaggeredGridLayoutManager交错网格布局管理器,每个格子的高度和宽度都可以不一样,支持水平方向和垂直方向,StaggeredGridLayoutManager中第一个参数是行数(水平方向)或者列数(垂直方法),第二个参数是方向参数:

        menuAdapter = new MenuAdapter(list);
        recyclerView.setLayoutManager(new StaggeredGridLayoutManager(5, StaggeredGridLayoutManager.VERTICAL));
        recyclerView.setAdapter(menuAdapter);

        recyclerView.post(new Runnable() {
            @Override
            public void run() {
                flyBorder.attachToRecyclerView(recyclerView);
                recyclerView.getChildAt(0).setFocusable(true);
                recyclerView.getChildAt(0).setFocusableInTouchMode(true);
                recyclerView.getChildAt(0).requestFocus();
            }
        });

recyclerView.post底层是使用handler发送message到MainLooper主线程去处理,由于RecyclerView绑定adapter后子视图有可能还未初始化,getChildAt(0)方法可能会为null。所以使用post方法等RecyclerView中的视图都初始化完后再令第一个Item获取焦点,并实现flyBorderView与RecyclerView的绑定。

扩展FlyBorderView 支持RecyclerView

首先FlyBorderView 还是上一节中的那个类,在这个类中做扩展以支持RecyclerView,添加一个attachToRecyclerView方法实现FlyBorderView 与RecyclerView的绑定。

final DisplayMetrics displayMetrics = getContext().getApplicationContext().getResources().getDisplayMetrics();

public void attachToRecyclerView(final RecyclerView recyclerView) {
        View childView = recyclerView.getChildAt(0);
        final int childWidth = childView.getWidth();
        final int childHeight = childView.getHeight();
        startTotalAnim(childWidth + 2 * borderWidth, childHeight + 2 * borderWidth,
                childView.getLeft() - borderWidth, childView.getTop() - borderWidth);
 StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) recyclerView.getLayoutManager();

        switch (layoutManager.getOrientation()) {
            case StaggeredGridLayoutManager.HORIZONTAL:
                recyclerView.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
                    @Override
                    public void onGlobalFocusChanged(View oldFocus, View newFocus) {

                        if (newFocus.getLayoutParams() instanceof RecyclerView.LayoutParams) {
                            int left;
                            if (newFocus.getLeft() >= displayMetrics.widthPixels - childWidth) {
                                left = displayMetrics.widthPixels - childWidth;
                            } else if (newFocus.getLeft() <= 0) {
                                left = 0;
                            } else {
                                left = newFocus.getLeft();
                            }
                            int widthInc = newFocus.getWidth() + 2 * borderWidth - getWidth();
                            int heightInc = newFocus.getHeight() + 2 * borderWidth - getHeight();
                            int translationX = left - borderWidth;
                            int translationY = newFocus.getTop() - borderWidth;
                            startTotalAnim(widthInc, heightInc, translationX, translationY);
                        }

                    }
                });
                break;
            case StaggeredGridLayoutManager.VERTICAL:
                recyclerView.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
                    @Override
                    public void onGlobalFocusChanged(View oldFocus, View newFocus) {

                        if (newFocus.getLayoutParams() instanceof RecyclerView.LayoutParams) {
                            int top;
                            if (newFocus.getTop() >= displayMetrics.heightPixels - childHeight) {
                                top = displayMetrics.heightPixels - childHeight;
                            } else if (newFocus.getTop() <= 0) {
                                top = 0;
                            } else {
                                top = newFocus.getTop();
                            }
                            int widthInc = newFocus.getWidth() + 2 * borderWidth - getWidth();
                            int heightInc = newFocus.getHeight() + 2 * borderWidth - getHeight();
                            int translationX = newFocus.getLeft() - borderWidth;
                            int translationY = top - borderWidth;
                            startTotalAnim(widthInc, heightInc, translationX, translationY);
                        }
                    }
                });
                break;
            default:
                Toast.makeText(getContext(), "Error", Toast.LENGTH_SHORT).show();
        }


    }

attachToRecyclerView方法中前4行主要是令焦点框初始化到RecyclerView中第一个View的位置。然后根据StaggeredGridLayoutManager的getOrientation方法分水平和垂直方法进行处理。实现RecyclerView中监听焦点变化的主要是获取RecyclerView的视图树并设置焦点监听器来监听,跟在ViewGroup中监听焦点的变化差不多,recyclerView.getViewTreeObserver().addOnGlobalFocusChangeListener。

在水平方向上,有两种特殊情况:
第一种情况是最右边的Item并没有完全显示:
Android TV RecyclerView焦点移动飞框的实现_第1张图片

这时候RecyclerView向右滚动,最右边Item完全显示,但是获取的newFocus.getLeft()的左上角x轴位置值并不准确,焦点框并不能与Item的View对齐,位置值有可能会大于等于displayMetrics.widthPixels - childWidth(即最后一个Item完全显示的坐标值),所以需要将最右边的Item完全显示的y轴位置值设置为left = displayMetrics.widthPixels - childWidth。

第二种情况是最左边的Item并没有完全显示:
Android TV RecyclerView焦点移动飞框的实现_第2张图片

这时候RecyclerView向左滚动,最左边Item完全显示,但是获取的newFocus.getLeft()的左上角x轴位置值并不准确,焦点框并不能与Item的View对齐,位置值会小于0,焦点飞框移动后会看不到。所以需要将最左边的Item完全显示的y轴位置值设置为left = 0;

在垂直方向上也同样有两种情况,类似于水平方向上的两种特殊情况,只不过要判断处理的是y轴方向上的便宜量,即view.getTop坐标值。

在ViewGroup和RecyclerView中的焦点飞框的实现已经处理完毕了,下一个坑估计就是多RecyclerView联动焦点移动飞框的实现了,即飞框可以从一个RecyclerView移动到另一个RecyclerView。

本位项目Demo源码:
https://github.com/QQ402164452/FlyBorderViewDemo/branches

你可能感兴趣的:(Android)