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 <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);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章