RecyclerView选中Item滚动到屏幕中间 / 指定位置

产品需求,点击标签变成选中态,且被选中标签 自动滑到屏幕中间,如图所示:

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 position has changed with
 * an optional payload object.
 *
 * 

This is an item change event, not a structural change event. It indicates that any * reflection of the data at position is out of date and should be updated. * The item at position retains the same identity. *

* *

* 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.
         * 

* 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 position 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. *

* Partial bind vs full bind: *

* 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);
        }
    }
}

你可能感兴趣的:(Android)