android TV常见需求,焦点item保持居中 —— RecyclerView自定义焦点滑动位置和滑动速度。

Iona ·
更新时间:2024-09-20
· 518 次阅读

  android tv开发和移动端开发最大的不同就是多了一个焦点处理的逻辑。尤其是类似Recyclerview这样本身带有滑动效果,为了醒目的显示当前焦点在什么位置,需要滑动的时候回添加大量的动画、高亮、阴影等效果。
图片来自网络
  同样,让焦点位置不变而列表主动滑动也是一种常见的提醒焦点的手段。demo效果图如下,结尾放出全部代码:
效果图

一、准备工作

先导入recyclerview

dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' }

  我用的demo是androidx的recyclerview。低版本的同学可以使用android.support支持库。
  在布局文件中添加recyclerview的布局,并添加一个item的布局。findviewbyid找到recyclerview的控件,并setLayoutManager(我用的是LinearLayoutManager)和setAdapter。一个粗糙的recyclerview效果就出来了。这是最简单的recyclerview,除了能滑动,什么效果也没有。

二、突出焦点,添加放大动画和阴影

  允许item获得焦点,并为item设置焦点监听。这一步可以放到onBindViewHolder或者ViewHolder初始化的地方。
  为了能看出当前焦点的位置,还需要对获得焦点的item进行高亮处理。下面代码中,用setTranslationZ添加了阴影,ofFloatAnimator方法中还设置了放大动画。

class MyHolder extends RecyclerView.ViewHolder{ public MyHolder(@NonNull final View itemView) { super(itemView); itemView.setFocusable(true); itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean b) { if(b){ int[] amount = getScrollAmount(recyclerView, view);//计算需要滑动的距离 //滑动到指定距离 scrollToAmount(recyclerView, amount[0], amount[1]); itemView.setTranslationZ(20);//阴影 ofFloatAnimator(itemView,1f,1.3f);//放大 }else { itemView.setTranslationZ(0); ofFloatAnimator(itemView,1.3f,1f); } } }); }

//放大动画

//放大动画 private void ofFloatAnimator(View view,float start,float end){ AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(700);//动画时间 ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", start, end); ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", start, end); animatorSet.setInterpolator(new DecelerateInterpolator());//插值器 animatorSet.play(scaleX).with(scaleY);//组合动画,同时基于x和y轴放大 animatorSet.start(); }

  因为item放大,体积超过了recyclerview的边界。为了使这部分正常显示,需要在布局文件中recyclerview的父布局添加clipChildren和clipToPadding属性。

android:clipChildren="false" android:clipToPadding="false" 三、计算滑动距离,使焦点始终居中

  首先我们看看recyclerview源码是怎么控制滑动的距离的,什么时候需要滑动,什么时候不用滑动。在源码RecyclerView/的LayoutManager中有这样一段代码:

public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate, boolean focusedChildVisible) { int[] scrollAmount = getChildRectangleOnScreenScrollAmount(child, rect ); int dx = scrollAmount[0]; int dy = scrollAmount[1]; if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) { if (dx != 0 || dy != 0) { if (immediate) { parent.scrollBy(dx, dy); } else { parent.smoothScrollBy(dx, dy); } return true; } } return false; }

  可以看出recyclerview是调用smoothScrollBy(dx, dy)方法滑动的,而滑动的距离由getChildRectangleOnScreenScrollAmount方法计算得出。所以我重写这个方法,改变的dx和dy大小,或者,我也可以在其他地方主动调用smoothScrollBy方法,只要item能移动到对应位置就可以了。这里,我决定在item焦点监听中主动调用smoothScrollBy方法。计算dx和dy我参考了getChildRectangleOnScreenScrollAmount的计算方式,把获得焦点的item放在最中间。

/** * 计算需要滑动的距离,使焦点在滑动中始终居中 * @param recyclerView * @param view */ private int[] getScrollAmount(RecyclerView recyclerView, View view) { int[] out = new int[2]; final int parentLeft = recyclerView.getPaddingLeft(); final int parentTop = recyclerView.getPaddingTop(); final int parentRight = recyclerView.getWidth() - recyclerView.getPaddingRight(); final int childLeft = view.getLeft() + 0 - view.getScrollX(); final int childTop = view.getTop() + 0 - view.getScrollY(); final int dx =childLeft - parentLeft - ((parentRight - view.getWidth()) / 2);//item左边距减去Recyclerview不在屏幕内的部分,加当前Recyclerview一半的宽度就是居中 final int dy = childTop - parentTop - (parentTop - view.getHeight()) / 2;//同上 out[0] = dx; out[1] = dy; return out; } }

getScrollAmount是源码getChildRectangleOnScreenScrollAmount的简化版本。
然后再item的焦点监听中,调用滑动就可以了

itemView.setFocusable(true); itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean b) { if(b){ int[] amount = getScrollAmount(recyclerView, view);//计算需要滑动的距离 recyclerView.smoothScrollBy(amount[0], amount[1]); itemView.setTranslationZ(20);//阴影 ofFloatAnimator(itemView,1f,1.3f);//放大 }else { itemView.setTranslationZ(0); ofFloatAnimator(itemView,1.3f,1f); } } }); 四、修改smoothScrollBy的滑动速度并添加动画插值器

  到上一步,效果就基本完成了。为了更精细的完成界面,还可以对滑动的速度和滑动效果进行修改。
  我们已经知道recyclerview源码中使用的是smoothScrollBy(dx, dy)来进行滑动,那么跟踪smoothScrollBy(dx, dy)源码,看看在哪里可以设置滑动速度。
源码中有这一段:

void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator, int duration, boolean withNestedScrolling) { if (mLayout == null) { Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + "Call setLayoutManager with a non-null argument."); return; } if (mLayoutSuppressed) { return; } if (!mLayout.canScrollHorizontally()) { dx = 0; } if (!mLayout.canScrollVertically()) { dy = 0; } if (dx != 0 || dy != 0) { boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0; if (durationSuggestsAnimation) { if (withNestedScrolling) { int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (dx != 0) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (dy != 0) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); } mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator); } else { scrollBy(dx, dy); } } }

看注释描述,interpolator是动画插值器,duration是动画时间,withNestedScrolling是嵌套滚动平滑滚动。在上一层传入参数的时候interpolator是null,withNestedScrolling是false。那么我调用这个方法代替之前的smoothScrollBy(dx, dy)方法就可以控制滑动速度了。但是这个方法不是public的,不能直接调用,我通过反射取出这个方法:

//根据坐标滑动到指定距离 private void scrollToAmount(RecyclerView recyclerView, int dx, int dy) { //如果没有滑动速度等需求,可以直接调用这个方法,使用默认的速度 // recyclerView.smoothScrollBy(dx,dy); //以下对滑动速度提出定制 try { Class recClass = recyclerView.getClass(); Method smoothMethod = recClass.getDeclaredMethod("smoothScrollBy", int.class, int.class, Interpolator.class, int.class); smoothMethod.invoke(recyclerView, dx, dy, new AccelerateDecelerateInterpolator(), 700);//时间设置为700毫秒, } catch (Exception e) { e.printStackTrace(); } }

之后在item的焦点监听中在调用scrollToAmount就可以了。

全代码:
MainActivity

public class MainActivity extends AppCompatActivity { private String[] permissions = new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.CAMERA}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerview = findViewById(R.id.recyclerview); recyclerview.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); Myadapter myadapter = new Myadapter(recyclerview); recyclerview.setAdapter(myadapter); } class Myadapter extends RecyclerView.Adapter{ private RecyclerView recyclerView; public Myadapter(RecyclerView recyclerView){ this.recyclerView = recyclerView; } @Override public MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View item = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout , parent ,false); return new MyHolder(item); } @Override public void onBindViewHolder(@NonNull MyHolder holder, int position) { } @Override public int getItemCount() { return 30; } class MyHolder extends RecyclerView.ViewHolder{ public MyHolder(@NonNull final View itemView) { super(itemView); itemView.setFocusable(true); itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean b) { if(b){ int[] amount = getScrollAmount(recyclerView, view);//计算需要滑动的距离 //滑动到指定距离 scrollToAmount(recyclerView, amount[0], amount[1]); itemView.setTranslationZ(20);//阴影 ofFloatAnimator(itemView,1f,1.3f);//放大 }else { itemView.setTranslationZ(0); ofFloatAnimator(itemView,1.3f,1f); } } }); } //根据坐标滑动到指定距离 private void scrollToAmount(RecyclerView recyclerView, int dx, int dy) { //如果没有滑动速度等需求,可以直接调用这个方法,使用默认的速度 // recyclerView.smoothScrollBy(dx,dy); //以下对滑动速度提出定制 try { Class recClass = recyclerView.getClass(); Method smoothMethod = recClass.getDeclaredMethod("smoothScrollBy", int.class, int.class, Interpolator.class, int.class); smoothMethod.invoke(recyclerView, dx, dy, new AccelerateDecelerateInterpolator(), 700);//时间设置为700毫秒, } catch (Exception e) { e.printStackTrace(); } } /** * 计算需要滑动的距离,使焦点在滑动中始终居中 * @param recyclerView * @param view */ private int[] getScrollAmount(RecyclerView recyclerView, View view) { int[] out = new int[2]; final int parentLeft = recyclerView.getPaddingLeft(); final int parentTop = recyclerView.getPaddingTop(); final int parentRight = recyclerView.getWidth() - recyclerView.getPaddingRight(); final int childLeft = view.getLeft() + 0 - view.getScrollX(); final int childTop = view.getTop() + 0 - view.getScrollY(); final int dx =childLeft - parentLeft - ((parentRight - view.getWidth()) / 2);//item左边距减去Recyclerview不在屏幕内的部分,加当前Recyclerview一半的宽度就是居中 final int dy = childTop - parentTop - (parentTop - view.getHeight()) / 2;//同上 out[0] = dx; out[1] = dy; return out; } } //放大动画 private void ofFloatAnimator(View view,float start,float end){ AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(700);//动画时间 ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", start, end); ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", start, end); animatorSet.setInterpolator(new DecelerateInterpolator());//插值器 animatorSet.play(scaleX).with(scaleY);//组合动画,同时基于x和y轴放大 animatorSet.start(); } } }

activity_main.xml

item_layout

炭烤葫芦娃 原创文章 11获赞 3访问量 2044 关注 私信 展开阅读全文
作者:炭烤葫芦娃



居中 recyclerview Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号