產品需求,點擊標籤變成選中態,且被選中標籤 自動滑到屏幕中間,如圖所示:
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);
}
}
}