上一篇文章讲了 Android TV 焦点移动飞框的实现 ,是在ViewGroup中实现的。这一篇文章主要在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都可以。
由于在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();
}
}
由于这种效果类似于瀑布流,我们为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,添加一个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并没有完全显示:
这时候RecyclerView向右滚动,最右边Item完全显示,但是获取的newFocus.getLeft()的左上角x轴位置值并不准确,焦点框并不能与Item的View对齐,位置值有可能会大于等于displayMetrics.widthPixels - childWidth(即最后一个Item完全显示的坐标值),所以需要将最右边的Item完全显示的y轴位置值设置为left = displayMetrics.widthPixels - childWidth。
这时候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