产品需求,点击标签变成选中态,且被选中标签 自动滑到屏幕中间,如图所示:
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);
}
}
}