Android TV RecyclerView实现无限居中滑动并上下带渐隐效果
先上实现效果图
一、TV开发中自定义RecyclerView解决若干问题
1、快速滑动焦点乱跑问题
在Android TV端使用原生RecyclerView在快速滑动的过程会发现焦点不见了很莫名奇怪,通过阅读相关源码发现,我们只需要自定义的查找焦点的逻辑即可
可以看看这篇大佬写的焦点分析Android TV开发总结【焦点】
我在查阅其他大神的解决方案中总结了两种,供大家参考
1、重写RecyclerView的focusSearch方法
@Override
public View focusSearch(View focused, int direction) {
View realNextFocus = super.focusSearch(focused, direction);
View nextFocus = FocusFinder.getInstance().findNextFocus(this, focused, direction);
switch (direction) {
case FOCUS_RIGHT:
case FOCUS_LEFT:
// 调用移出的监听
if (nextFocus == null && !canScrollHorizontally(-1)) {
if (mCanFocusOutHorizontal) {
if (mFocusLostListener != null) {
mFocusLostListener.onFocusLost(focused, direction);
}
return realNextFocus;
} else {
return focused;
}
}
break;
case FOCUS_UP:
case FOCUS_DOWN:
if (nextFocus == null && !canScrollVertically(1)) {
if (mCanFocusOutVertical) {
return realNextFocus;
} else {
return focused;
}
}
break;
}
return realNextFocus;
}
2、重写dispatchKeyEvent方法
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
boolean result = super.dispatchKeyEvent(event);
View focusView = this.getFocusedChild();
if (focusView == null) {
return result;
}
int dy = 0;
int dx = 0;
if (getChildCount() > 0) {
View firstView = this.getChildAt(0);
dy = firstView.getHeight();
dx = firstView.getWidth();
}
if (event.getAction() == KeyEvent.ACTION_UP) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return super.dispatchKeyEvent(event);
}
return true;
} else {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
View rightView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_RIGHT);
Log.i(TAG, "rightView is null:" + (rightView == null));
if (rightView != null) {
rightView.requestFocus();
return true;
} else {
this.smoothScrollBy(dx, 0);
return true;
}
case KeyEvent.KEYCODE_DPAD_LEFT:
View leftView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_LEFT);
Log.i(TAG, "leftView is null:" + (leftView == null));
if (leftView != null) {
leftView.requestFocus();
return true;
} else {
this.smoothScrollBy(-dx, 0);
return true;
}
case KeyEvent.KEYCODE_DPAD_DOWN:
View downView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_DOWN);
Log.i(TAG, " downView is null:" + (downView == null));
if (downView != null) {
downView.requestFocus();
return true;
} else {
this.smoothScrollBy(0, dy);
return true;
}
case KeyEvent.KEYCODE_DPAD_UP:
View upView = FocusFinder.getInstance().findNextFocus(this, focusView, View.FOCUS_UP);
Log.i(TAG, "upView is null:" + (upView == null));
if (upView != null) {
upView.requestFocus();
return true;
} else {
this.smoothScrollBy(0, -dy);
return true;
}
}
}
return result;
}
2、实现焦点记忆功能
如果想实现焦点记忆功能的话可以重写如下RecyclerView函数
private View mLastFocusView = null;
// 最后一次聚焦的位置
private int mLastFocusPosition = 0;
@Override
public void requestChildFocus(View child, View focused) {
Log.i(TAG, "requestChildFocus nextchild= " + child + ",focused = " + focused);
Log.i(TAG, "requestChildFocus focusPos = " + mLastFocusPosition);
super.requestChildFocus(child, focused);
mLastFocusView = focused;
//执行过super.requestChildFocus之后hasFocus会变成true
mLastFocusPosition = getChildViewHolder(child).getBindingAdapterPosition();
Log.i(TAG, "requestChildFocus focusPos = " + mLastFocusPosition);
}
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
Log.i(TAG, "addFocusables--focusPos = " + mLastFocusPosition);
if (this.hasFocus() || mLastFocusView == null) {
//在recyclerview内部焦点切换
super.addFocusables(views, direction, focusableMode);
} else {
//将当前的view放到Focusable views列表中,再次移入焦点时会取到该view,实现焦点记忆功能
views.add(getLayoutManager().findViewByPosition(mLastFocusPosition));
}
}
3、实现Item焦点放大不被遮挡
如果想实现Item放大不被遮挡的话,需要重写getChildDrawingOrder函数
@Override
protected int getChildDrawingOrder(int childCount, int position) {
View focusedView = getFocusedChild();
if (null != focusedView) {
int pos = indexOfChild(focusedView);
/* 这是最后一个需要刷新的item */
if (position == childCount - 1) {
if (pos > position) {
pos = position;
}
return pos;
}
else if (pos == position) {
/* 这是原本要在最后一个刷新的item */
return childCount - 1;
}
}
return position;
}
4、实现Item居中滑动效果
如果想实现居中滑动效果,有两种方式
1、重写RecyclerView的requestChildFocus和requestChildRectangleOnScreen方法
//焦点是否居中
private boolean mSelectedItemCentered = true;
private int mSelectedItemOffsetStart = 0;
private int mSelectedItemOffsetEnd = 0;
@Override
public void requestChildFocus(View child, View focused) {
Log.i(TAG, "nextchild= " + child + ",focused = " + focused);
//计算控制recyclerview 选中item的居中从参数
if (mSelectedItemCentered && child != null) {
mSelectedItemOffsetStart = !isVertical() ? (getFreeWidth() - child.getWidth()) : (getFreeHeight() - child.getHeight());
mSelectedItemOffsetStart /= 2;
mSelectedItemOffsetEnd = mSelectedItemOffsetStart;
}
Log.i(TAG, "requestChildFocus focusPos = " + mCurrentFocusPosition);
super.requestChildFocus(child, focused);
//执行过super.requestChildFocus之后hasFocus会变成true
mCurrentFocusPosition = getChildViewHolder(child).getBindingAdapterPosition();
Log.i(TAG, "requestChildFocus focusPos = " + mCurrentFocusPosition);
}
/**
* 通过该方法设置选中的item居中
* <p>
* 该方法能够确定在布局中滚动或者滑动时候,子item和parent之间的位置
* dy,dx的实际意义就是在滚动中下滑和左右滑动的距离
* 而这个值的确定会严重影响滑动的流畅程度
*/
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
Log.i(TAG, "requestChildRectangleOnScreen= " + child + ",pos = " + immediate);
final int parentLeft = getPaddingLeft();
final int parentRight = getWidth() - getPaddingRight();
final int parentTop = getPaddingTop();
final int parentBottom = getHeight() - getPaddingBottom();
final int childLeft = child.getLeft() + rect.left;
final int childTop = child.getTop() + rect.top;
final int childRight = childLeft + rect.width();
final int childBottom = childTop + rect.height();
final int offScreenLeft = Math.min(0, childLeft - parentLeft - mSelectedItemOffsetStart);
final int offScreenRight = Math.max(0, childRight - parentRight + mSelectedItemOffsetEnd);
final int offScreenTop = Math.min(0, childTop - parentTop - mSelectedItemOffsetStart);
final int offScreenBottom = Math.max(0, childBottom - parentBottom + mSelectedItemOffsetEnd);
final boolean canScrollHorizontal = getLayoutManager().canScrollHorizontally();
final boolean canScrollVertical = getLayoutManager().canScrollVertically();
// Favor the "start" layout direction over the end when bringing one side or the other
// of a large rect into view. If we decide to bring in end because start is already
// visible, limit the scroll such that start won't go out of bounds.
final int dx;
if (canScrollHorizontal) {
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
dx = offScreenRight != 0 ? offScreenRight
: Math.max(offScreenLeft, childRight - parentRight);
} else {
dx = offScreenLeft != 0 ? offScreenLeft
: Math.min(childLeft - parentLeft, offScreenRight);
}
} else {
dx = 0;
}
// Favor bringing the top into view over the bottom. If top is already visible and
// we should scroll to make bottom visible, make sure top does not go out of bounds.
final int dy;
if (canScrollVertical) {
dy = offScreenTop != 0 ? offScreenTop : Math.min(childTop - parentTop, offScreenBottom);
} else {
dy = 0;
}
if (dx != 0 || dy != 0) {
scrollBy(dx, dy);
if (immediate) {
scrollBy(dx, dy);
} else {
smoothScrollBy(dx, dy);
}
// 重绘是为了选中item置顶,具体请参考getChildDrawingOrder方法
postInvalidate();
return true;
}
return false;
}
2、自定义LayoutManager,来实现居中滑动
public class CenterLayoutManager extends LinearLayoutManager {
public CenterLayoutManager(Context context) {
super(context);
}
@Override
public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state,final int position) {
CenterSmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext(),recyclerView) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return computeVectorForPosition(targetPosition);
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
public PointF computeVectorForPosition(int targetPosition) {
return super.computeScrollVectorForPosition(targetPosition);
}
abstract class CenterSmoothScroller extends LinearSmoothScroller {
RecyclerView recyclerView;
CenterSmoothScroller(Context context,RecyclerView recyclerView) {
super(context);
this.recyclerView = recyclerView;
}
@Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
Log.i("du","calculateDtToFit viewStart:" + viewStart + "---viewEnd:" + viewEnd + "---boxStart:" + boxStart + "---boxEnd:" + boxEnd + "----snapPreference:" + snapPreference );
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
}
/**
* 滑动完成后,让该targetPosition 处的item获取焦点
*/
@Override
protected void onStop() {
Log.i("du","onStop-Position" + getTargetPosition());
super.onStop();
final View itemView = findViewByPosition(getTargetPosition());
if (null != itemView) {
itemView.requestFocus();
}
}
}
}
//recyclerview中调用
@Override
public void onBindViewHolder(final @NonNull BaseViewHolder viewHolder, final int i) {
viewHolder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
Log.i("du","onBindViewHolder-Position"+ i + "---hasFocus:" + hasFocus);
if (hasFocus) {
ViewCompat.animate(viewHolder.itemView).scaleX(1.5f).scaleY(1.5f).start();
mCenterLayoutManager.smoothScrollToPosition(mRecyclerView,new RecyclerView.State(), i);
} else {
ViewCompat.animate(viewHolder.itemView).scaleX(1f).scaleY(1f).start();
}
}
});
二、实现垂直列表的无线循环滑动
在这里参照了网上的方法
在RecyclerView.Adapter的方法中:
@Override
public int getItemCount() {
return Integer.MAX_VALUE;
}
由于我们需要时需要无线居中滑动而且默认定位,所以我们需要调整下逻辑重写setAdapter
@Override
public void setAdapter(@Nullable Adapter adapter) {
super.setAdapter(adapter);
//定位到对应位置
scrollToPosition(((Integer.MAX_VALUE / 2) - ((Integer.MAX_VALUE / 2) % realCount)) + mCurPos);
postDelayed(new Runnable() {
@Override
public void run() {
View targetView = getLayoutManager().findViewByPosition(((Integer.MAX_VALUE/2)-((Integer.MAX_VALUE/2)%realCount)) + mCurPos);
if (targetView != null) {
targetView.requestFocus();
}
}
},100);
}
三、实现垂直列表的渐隐效果
自定义RecyclerView并重写对应方法即可
private Paint paint;
private int height;
private int width;
private int spanPixel = 100;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height = h;
width = w;
float spanFactor = spanPixel / (height / 2f);
// 设置渐隐效果,起始0为0x00000000,中间spanFactor为0xff000000,末尾为0xff000000
LinearGradient linearGradient = new LinearGradient(0, 0, 0, height / 2,
new int[]{0x00000000, 0xff000000, 0xff000000}, new float[]{0, spanFactor, 1f}, Shader.TileMode.MIRROR);
paint.setShader(linearGradient);
}
@Override
public void draw(Canvas c) {
c.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
super.draw(c);
c.drawRect(0, 0, width, height, paint);
c.restore();
}
四、限制RecyclerView滑动速度
可以使用通过调整dispatchKeyEvent 的输入间隔时间
private long mLastKeyDownTime;
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
long current = System.currentTimeMillis();
if (event.getAction() != KeyEvent.ACTION_DOWN || getChildCount() == 0) {
return super.dispatchKeyEvent(event);
}
// 限制两个KEY_DOWN事件的最低间隔为120ms
if (isComputingLayout() || current - mLastKeyDownTime <= 120) {
return true;
}
mLastKeyDownTime = current;
return super.dispatchKeyEvent(event);
}