产品需求,点击标签变成选中态,且被选中标签 自动滑到屏幕中间,如图所示: 1.如何实现自动滑动到屏幕中间?
2.如何避免闪动?
3.滑动速度如何控制?
一,自动滑动到屏幕中间:
RecyclerView中最容易想到的方法是smoothScrollToPosition(int position),可是position该是多少呢?显然这个方法行不通。
设置滑动还要从LinearLayoutManager入手,重写之。
/** * Created by iblade.Wang on 2019/5/22 17:08 */ public class CenterLayoutManager extends LinearLayoutManager { public CenterLayoutManager(Context context) { super(context); } public CenterLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public CenterLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext()); smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); } private static class CenterSmoothScroller extends LinearSmoothScroller { CenterSmoothScroller(Context context) { super(context); } @Override public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) { return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2); } } }点击Item调用
centerLayoutManager.smoothScrollToPosition(recyclerView1, new RecyclerView.State(), position);OK,第一个问题搞定;现在解决闪动的问题。
二,item闪动问题:
UI说“初版”的切换时 闪一下体验太不好,说实话我是真心看不出来,不是王婆卖瓜自卖自夸,而是该Demo场景不够显眼,因为它确实闪屏了。在另外一个RecyclerView 点赞功能 时候,想护犊子都不行,犊子确实太闪眼。场景是Item中一个ImageView有共四张图Loading,ErrorImg,GIF和CoverImg的切换,图片下方是点赞按钮,一点赞,图片快速闪一下。明白人一听就知道问题出在哪:adapter1.notifyItemChanged(position);
现在就要引入一个经常听,我却很少用的操作:Item的局部刷新。
/** * Notify any registered observers that the item at <code>position</code> has changed with * an optional payload object. * * <p>This is an item change event, not a structural change event. It indicates that any * reflection of the data at <code>position</code> is out of date and should be updated. * The item at <code>position</code> retains the same identity. * </p> * * <p> * Client can optionally pass a payload for partial change. These payloads will be merged * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the * item is already represented by a ViewHolder and it will be rebound to the same * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing * payloads on that item and prevent future payload until * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not * attached, the payload will be simply dropped. * * @param position Position of the item that has changed * @param payload Optional parameter, use null to identify a "full" update * * @see #notifyItemRangeChanged(int, int) */ public final void notifyItemChanged(int position, @Nullable Object payload) { mObservable.notifyItemRangeChanged(position, 1, payload); }没错。就是带上这个payLoad。
当然在Adapter中也要对应重写一个带payLoads的方法如下:
/** * Called by RecyclerView to display the data at the specified position. This method * should update the contents of the {@link ViewHolder#itemView} to reflect the item at * the given position. * <p> * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method * again if the position of the item changes in the data set unless the item itself is * invalidated or the new position cannot be determined. For this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. If you need the position of an item later * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will * have the updated adapter position. * <p> * Partial bind vs full bind: * <p> * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, * the ViewHolder is currently bound to old data and Adapter may run an efficient partial * update using the payload info. If the payload is empty, Adapter must run a full bind. * Adapter should not assume that the payload passed in notify methods will be received by * onBindViewHolder(). For example when the view is not attached to the screen, the * payload in notifyItemChange() will be simply dropped. * * @param holder The ViewHolder which should be updated to represent the contents of the * item at the given position in the data set. * @param position The position of the item within the adapter's data set. * @param payloads A non-null list of merged payloads. Can be empty list if requires full * update. */ public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) { onBindViewHolder(holder, position); }有人问了,为何传参是Object,接收却是List payloads,哪位路过大神 求评论区 解读。
测试结果是 传参后list的长度总为1.当然不传参长度为0.
代码如下:
public static final int UPDATE_STATE = 101; public static final int UPDATE_NAME = 102; @Override public void onBindViewHolder(@NonNull LabelHolder holder, int position, @NonNull List<Object> payloads) { //list为空时,必须调用两个参数的onBindViewHolder(@NonNull LabelHolder holder, int position) if (payloads.isEmpty()) { onBindViewHolder(holder, position); } else if (payloads.get(0) instanceof Integer) { int payLoad = (int) payloads.get(0); switch (payLoad) { case UPDATE_STATE: holder.textView.setSelected(list.get(position).isSelected()); break; case UPDATE_NAME: holder.textView.setText(list.get(position).getName()); break; default: break; } } }这样就能顺利解决闪动的问题。下面说说滑动速度控制。
三,如何做到真正的smooth:
smooth纵享丝滑方法名的都是骗人的,使用时往往都是“噔”一下就到position了,搞得UI和我们鸡飞狗跳。
废话少说。控制滑动速度,其实关键关键还在LinearLayoutManager中的LinearSmoothScroller,重写一个方法:
private static final float MILLISECONDS_PER_INCH = 25f; /** * Calculates the scroll speed. * * @param displayMetrics DisplayMetrics to be used for real dimension calculations * @return The time (in ms) it should take for each pixel. For instance, if returned value is * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds. */ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; }默认是25f,上图中是我已经改为100f后效果,不多解释,看变量名就知道咋回事。
private static class CenterSmoothScroller extends LinearSmoothScroller { CenterSmoothScroller(Context context) { super(context); } @Override public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) { return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2); } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return 100f / displayMetrics.densityDpi; } }完整代码如下:
Activity:
public class CenterItemActivity extends AppCompatActivity { private RecyclerView recyclerView; private LabelAdapter adapter; private List<FilterBean> list = new ArrayList<>(); private int lastLabelIndex; private CenterLayoutManager centerLayoutManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_center_item); init(); } private void init() { recyclerView = findViewById(R.id.label_recycler_view); adapter = new LabelAdapter(list, this); centerLayoutManager = new CenterLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); recyclerView.setLayoutManager(centerLayoutManager); for (int i = 0; i < 20; i++) { FilterBean bean = new FilterBean(); bean.setName("Label-" + i); list.add(bean); } recyclerView.setAdapter(adapter); adapter.setOnLabelClickListener(new LabelAdapter.OnLabelClickListener() { @Override public void onClick(FilterBean bean, int position) { if (position != lastLabelIndex) { ToastUtil.show(CenterItemActivity.this, bean.getName()); FilterBean lastBean = list.get(lastLabelIndex); lastBean.setSelected(false); adapter.notifyItemChanged(lastLabelIndex, LabelAdapter.UPDATE_STATE); centerLayoutManager.smoothScrollToPosition(recyclerView, new RecyclerView.State(), position); bean.setSelected(true); adapter.notifyItemChanged(position, LabelAdapter.UPDATE_STATE); } lastLabelIndex = position; } }); } } /** * Created by iblade.Wang on 2019/5/22 17:08 */ public class CenterLayoutManager extends LinearLayoutManager { public CenterLayoutManager(Context context) { super(context); } public CenterLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public CenterLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext()); smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); } private static class CenterSmoothScroller extends LinearSmoothScroller { public CenterSmoothScroller(Context context) { super(context); } @Override public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) { return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2); } @Override protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { return 100f / displayMetrics.densityDpi; } } } /** * Created by iblade.Wang on 2019/5/22 17:16 */ public class LabelAdapter extends RecyclerView.Adapter<LabelAdapter.LabelHolder> { private List<FilterBean> list; private Activity activity; private LayoutInflater inflater; public LabelAdapter(List<FilterBean> list, Activity activity) { this.list = list; this.activity = activity; inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @NonNull @Override public LabelHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new LabelHolder(inflater.inflate(R.layout.item_pos_type, parent, false)); } @Override public void onBindViewHolder(@NonNull LabelHolder holder, int position1) { final int position = holder.getAdapterPosition(); if (list != null && null != list.get(position)) { FilterBean bean = list.get(position); holder.textView.setSelected(bean.isSelected()); holder.textView.setText(bean.getName()); holder.textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (onLabelClickListener != null) { onLabelClickListener.onClick(bean, position); } } }); } } public static final int UPDATE_STATE = 101; public static final int UPDATE_NAME = 102; @Override public void onBindViewHolder(@NonNull LabelHolder holder, int position, @NonNull List<Object> payloads) { //list为空时,必须调用两个参数的onBindViewHolder(@NonNull LabelHolder holder, int position) if (payloads.isEmpty()) { onBindViewHolder(holder, position); } else if (payloads.get(0) instanceof Integer) { int payLoad = (int) payloads.get(0); switch (payLoad) { case UPDATE_STATE: holder.textView.setSelected(list.get(position).isSelected()); break; case UPDATE_NAME: holder.textView.setText(list.get(position).getName()); break; default: break; } } } public interface OnLabelClickListener { /** * 点击label * * @param bean 点击label的对象 * @param position 点击位置 */ void onClick(FilterBean bean, int position); } private OnLabelClickListener onLabelClickListener; public void setOnLabelClickListener(OnLabelClickListener onLabelClickListener) { this.onLabelClickListener = onLabelClickListener; } @Override public int getItemCount() { return null == list ? 0 : list.size(); } final class LabelHolder extends RecyclerView.ViewHolder { private TextView textView; public LabelHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv_type); } } }