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<Myadapter.MyHolder>{
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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
item_layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="#A9A9A9"
android:layout_margin="2dp"
android:layout_width="150dp"
android:layout_height="100dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello World!"
android:gravity="center"/>
</FrameLayout>