RecyclerView - 水平 LinearLayoutManager 创建/绑定方法的调用方式过于频繁
RecyclerView - Horizontal LinearLayoutManager create / bind methods called way too often
目前我对 Android 上 LinearLayoutManagers 和 RecyclerViews 的后续问题的想法已经结束:
我想实现什么场景
一个水平的 RecyclerView,用户可以在上面快速滑动而没有任何限制。全屏大小的项目使它们与 recyclerview 本身一样大。当投掷停止或用户手动停止时,回收器应滚动到一个项目(有点模仿 viewPager)
(我使用的是支持修订版 25.1.0)
代码片段
传呼机-class本身
public class VelocityPager extends RecyclerView {
private int mCurrentItem = 0;
@NonNull
private LinearLayoutManager mLayoutManager;
@Nullable
private OnPageChangeListener mOnPageChangeListener = null;
@NonNull
private Rect mViewRect = new Rect();
@NonNull
private OnScrollListener mOnScrollListener = new OnScrollListener() {
private int mLastItem = 0;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mOnPageChangeListener == null) return;
mCurrentItem = mLayoutManager.findFirstVisibleItemPosition();
final View view = mLayoutManager.findViewByPosition(mCurrentItem);
view.getLocalVisibleRect(mViewRect);
final float offset = (float) mViewRect.left / ((View) view.getParent()).getWidth();
mOnPageChangeListener.onPageScrolled(mCurrentItem, offset, 0);
if (mCurrentItem != mLastItem) {
mOnPageChangeListener.onPageSelected(mCurrentItem);
mLastItem = mCurrentItem;
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (mOnPageChangeListener == null) return;
mOnPageChangeListener.onPageScrollStateChanged(newState);
}
};
public VelocityPager(@NonNull Context context) {
this(context, null);
}
public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutManager = createLayoutManager();
init();
}
@NonNull
private LinearLayoutManager createLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnScrollListener(mOnScrollListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnScrollListener(mOnScrollListener);
}
@Override
public void onScrollStateChanged(int state) {
// If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
// This code fixes this. This code is not strictly necessary but it improves the behaviour.
if (state == SCROLL_STATE_IDLE) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
// views on the screen
int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
// distance we need to scroll
int leftMargin = (screenWidth - lastView.getWidth()) / 2;
int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
int leftEdge = lastView.getLeft();
int rightEdge = firstView.getRight();
int scrollDistanceLeft = leftEdge - leftMargin;
int scrollDistanceRight = rightMargin - rightEdge;
if (leftEdge > screenWidth / 2) {
smoothScrollBy(-scrollDistanceRight, 0);
} else if (rightEdge < screenWidth / 2) {
smoothScrollBy(scrollDistanceLeft, 0);
}
}
}
private void init() {
setLayoutManager(mLayoutManager);
setItemAnimator(new DefaultItemAnimator());
setHasFixedSize(true);
}
public void setCurrentItem(int index, boolean smoothScroll) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(index);
}
if (smoothScroll) smoothScrollToPosition(index);
if (!smoothScroll) scrollToPosition(index);
}
public int getCurrentItem() {
return mCurrentItem;
}
public void setOnPageChangeListener(@Nullable OnPageChangeListener onPageChangeListener) {
mOnPageChangeListener = onPageChangeListener;
}
public interface OnPageChangeListener {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* @param position Position index of the new selected page.
*/
void onPageSelected(int position);
/**
* Called when the scroll state changes. Useful for discovering when the user
* begins dragging, when the pager is automatically settling to the current page,
* or when it is fully stopped/idle.
*
* @param state The new scroll state.
* @see VelocityPager#SCROLL_STATE_IDLE
* @see VelocityPager#SCROLL_STATE_DRAGGING
* @see VelocityPager#SCROLL_STATE_SETTLING
*/
void onPageScrollStateChanged(int state);
}
}
项目的 xml 布局
(注意:为了在应用程序内实现其他目的,根视图必须是可点击的)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true">
<LinearLayout
android:id="@+id/icon_container_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="top|end"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:alpha="0"
android:background="@drawable/info_background"
android:orientation="horizontal"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="@+id/delete"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_delete"
android:padding="12dp"
android:src="@drawable/ic_delete_white_24dp"
android:tint="@color/icons" />
</LinearLayout>
<LinearLayout
android:id="@+id/icon_container_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:alpha="0"
android:background="@drawable/info_background"
android:orientation="vertical"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="@+id/size"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_size"
android:padding="12dp"
android:src="@drawable/ic_straighten_white_24dp"
android:tint="@color/icons" />
<ImageView
android:id="@+id/palette"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_palette"
android:padding="12dp"
android:src="@drawable/ic_palette_white_24dp"
android:tint="@color/icons" />
</LinearLayout>
</RelativeLayout>
寻呼机本身的 xml 布局
(相当嵌套?可能是问题的原因?我不知道...)
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="end">
<SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.my.example.OptionalViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="horizontal"
app:layout_behavior="com.my.example.MoveUpBehavior" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
android:clickable="false"
android:fitsSystemWindows="false"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_menu_white_24dp" />
</android.support.design.widget.CoordinatorLayout>
</SwipeRefreshLayout>
<include layout="@layout/layout_drawer" />
</android.support.v4.widget.DrawerLayout>
我的适配器中与 ViewHolders 相关的部分
@Override
public int getItemCount() {
return dataset.size();
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.v("Adapter", "CreateViewHolder");
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View rootView = layoutInflater.inflate(R.layout.page, parent, false);
return new MyViewHolder(rootView);
}
@Override
public void onBindViewHolder(MyViewHolder page, int position) {
Log.v("Adapter", String.format("BindViewHolder(%d)", position));
final ViewData viewData = dataset.get(position);
page.bind(viewData);
listener.onViewAdded(position, viewData.getData());
}
@Override
public void onViewRecycled(MyViewHolder page) {
if (page.getData() == null) return;
listener.onViewRemoved(page.getData().id);
}
@Override
public int getItemViewType(int position) {
return 0;
}
ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder implements MyListener {
@BindView(R.id.info_container)
ViewGroup mInfoContainer;
@BindView(R.id.icon_container_top)
ViewGroup mIconContainerTop;
@BindView(R.id.icon_container_bottom)
ViewGroup mIconContainerBottom;
@BindView(R.id.info_rows)
ViewGroup mInfoRows;
@BindView(R.id.loading)
View mIcLoading;
@BindView(R.id.sync_status)
View mIcSyncStatus;
@BindView(R.id.delete)
View mIcDelete;
@BindView(R.id.ic_fav)
View mIcFavorite;
@BindView(R.id.size)
View mIcSize;
@BindView(R.id.palette)
View mIcPalette;
@BindView(R.id.name)
TextView mName;
@BindView(R.id.length)
TextView mLength;
@BindView(R.id.threads)
TextView mThreads;
@BindView(R.id.price)
TextView mPrice;
@Nullable
private MyModel mModel = null;
@Nullable
private Activity mActivity;
public MyViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mActivity= (Activity) itemView.getContext();
if (mActivity!= null) mActivity.addMyListener(this);
}
@OnClick(R.id.delete)
protected void clickDeleteBtn() {
if (mActivity == null || mActivity.getMode() != Mode.EDIT) return;
if (mModel == null) return;
Animations.pop(mIcDelete);
final int modelId = mModel.id;
if (mModel.delete()) {
mActivity.delete(modelId);
}
}
@OnClick(R.id.size)
protected void clickSizeBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_SIZE);
Animations.pop(mIcSize);
}
@OnClick(R.id.palette)
protected void clickPaletteBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_LENGTH);
Animations.pop(mIcPalette);
}
private void initModelViews() {
if (mData == null) return;
final Locale locale = Locale.getDefault();
mName.setValue(String.format(locale, "Model#%d", mModel.id));
mLength.setValue(Html.fromHtml(String.format(locale, itemView.getContext().getString(R.string.template_length), mModel.meters)));
}
/**
* set the icon container to be off screen at the beginning
*/
private void prepareViews() {
new ExpectAnim().expect(mIconContainerTop).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
new ExpectAnim().expect(mIconContainerBottom).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
}
@Nullable
public MyModel getData() {
return mModel;
}
private void enableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(outOfScreen(Gravity.END))
.toAnimation()
.start();
}
private void enableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(outOfScreen(Gravity.BOTTOM))
.toAnimation()
.start();
}
private void enableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(atItsOriginalPosition(), visible())
.toAnimation()
.start();
}
private void disableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(outOfScreen(Gravity.END), invisible())
.toAnimation()
.start();
}
public void bind(@NonNull final ViewData viewData) {
mModel = viewData.getData();
prepareViews();
initModelViews();
}
}
所以,这是我的问题!
在初始化适配器时,我通过一个可观察对象插入了大约 15 到 17 个项目。这似乎是正确的:
但是当水平滑动时,recyclerView 的回调似乎完全混乱并产生奇怪的结果:
你看到回收器根本不尝试回收旧的 viewHolder 了吗?该图像仅显示了正在进行的 "spamming" 的一小部分。有时它会在我缓慢滚动回收器时为同一位置创建一个新的 viewHolder 甚至两次以上!
另一个附带问题是:当前的侦听器应该允许我将绑定/回收事件传递给底层游戏引擎,该引擎将在屏幕上创建销毁实体。由于事件的过度垃圾邮件,它目前也会过度创建这些实体!
我希望 Recycler 第一次(假设在我的示例中是 17 次)创建一个新的 ViewHolder,然后按应有的方式重复使用这些项目。
请帮忙,我在这个问题上被困了 2 天了,在搜索有同样问题但没有运气的人后,我感到很沮丧。
谢谢!
When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
- 使用官方
LinearSnapHelper
将子视图的中心对齐到 RecyclerView 的中心。
- 使用
GravitySnapHelper
library 也可以捕捉到 RecyclerView 的开始或结束,就像 Google Play 商店那样。
这两种解决方案的应用相似:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling.
"Without limitations" 转换为 "infinite speed" 意味着投掷会立即跳到目标位置。这可能不是你想要的。
翻阅SnapHelper
资料后发现有个规律:滚动一英寸需要100毫秒。您可以覆盖此行为。
final SnapHelper snapHelper = new LinearSnapHelper() {
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
snapHelper.attachToRecyclerView(recyclerView);
这是默认速度(MILLISECONDS_PER_INCH = 100
)。试验并找出适合您需求的方法,从 "one inch takes 50 ms to scroll" 开始,依此类推。
ViewHolder
回收显然有问题。我猜你 运行 在 MyViewHolder
中的动画可能会阻止 RecyclerView
正确回收持有人。确保在某个时候取消动画,例如在 RecyclerView.Adapter#onViewDetachedFromWindow()
.
解决此问题后,我建议您按照@EugenPechanec 的建议减少在 OnScrollListener
中完成的自定义计算量。最好依靠支持库 类 并稍微调整一下行为。
目前我对 Android 上 LinearLayoutManagers 和 RecyclerViews 的后续问题的想法已经结束:
我想实现什么场景
一个水平的 RecyclerView,用户可以在上面快速滑动而没有任何限制。全屏大小的项目使它们与 recyclerview 本身一样大。当投掷停止或用户手动停止时,回收器应滚动到一个项目(有点模仿 viewPager) (我使用的是支持修订版 25.1.0)
代码片段
传呼机-class本身
public class VelocityPager extends RecyclerView {
private int mCurrentItem = 0;
@NonNull
private LinearLayoutManager mLayoutManager;
@Nullable
private OnPageChangeListener mOnPageChangeListener = null;
@NonNull
private Rect mViewRect = new Rect();
@NonNull
private OnScrollListener mOnScrollListener = new OnScrollListener() {
private int mLastItem = 0;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mOnPageChangeListener == null) return;
mCurrentItem = mLayoutManager.findFirstVisibleItemPosition();
final View view = mLayoutManager.findViewByPosition(mCurrentItem);
view.getLocalVisibleRect(mViewRect);
final float offset = (float) mViewRect.left / ((View) view.getParent()).getWidth();
mOnPageChangeListener.onPageScrolled(mCurrentItem, offset, 0);
if (mCurrentItem != mLastItem) {
mOnPageChangeListener.onPageSelected(mCurrentItem);
mLastItem = mCurrentItem;
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (mOnPageChangeListener == null) return;
mOnPageChangeListener.onPageScrollStateChanged(newState);
}
};
public VelocityPager(@NonNull Context context) {
this(context, null);
}
public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutManager = createLayoutManager();
init();
}
@NonNull
private LinearLayoutManager createLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnScrollListener(mOnScrollListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnScrollListener(mOnScrollListener);
}
@Override
public void onScrollStateChanged(int state) {
// If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
// This code fixes this. This code is not strictly necessary but it improves the behaviour.
if (state == SCROLL_STATE_IDLE) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
// views on the screen
int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
// distance we need to scroll
int leftMargin = (screenWidth - lastView.getWidth()) / 2;
int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
int leftEdge = lastView.getLeft();
int rightEdge = firstView.getRight();
int scrollDistanceLeft = leftEdge - leftMargin;
int scrollDistanceRight = rightMargin - rightEdge;
if (leftEdge > screenWidth / 2) {
smoothScrollBy(-scrollDistanceRight, 0);
} else if (rightEdge < screenWidth / 2) {
smoothScrollBy(scrollDistanceLeft, 0);
}
}
}
private void init() {
setLayoutManager(mLayoutManager);
setItemAnimator(new DefaultItemAnimator());
setHasFixedSize(true);
}
public void setCurrentItem(int index, boolean smoothScroll) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(index);
}
if (smoothScroll) smoothScrollToPosition(index);
if (!smoothScroll) scrollToPosition(index);
}
public int getCurrentItem() {
return mCurrentItem;
}
public void setOnPageChangeListener(@Nullable OnPageChangeListener onPageChangeListener) {
mOnPageChangeListener = onPageChangeListener;
}
public interface OnPageChangeListener {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* @param position Position index of the new selected page.
*/
void onPageSelected(int position);
/**
* Called when the scroll state changes. Useful for discovering when the user
* begins dragging, when the pager is automatically settling to the current page,
* or when it is fully stopped/idle.
*
* @param state The new scroll state.
* @see VelocityPager#SCROLL_STATE_IDLE
* @see VelocityPager#SCROLL_STATE_DRAGGING
* @see VelocityPager#SCROLL_STATE_SETTLING
*/
void onPageScrollStateChanged(int state);
}
}
项目的 xml 布局
(注意:为了在应用程序内实现其他目的,根视图必须是可点击的)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true">
<LinearLayout
android:id="@+id/icon_container_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="top|end"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:alpha="0"
android:background="@drawable/info_background"
android:orientation="horizontal"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="@+id/delete"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_delete"
android:padding="12dp"
android:src="@drawable/ic_delete_white_24dp"
android:tint="@color/icons" />
</LinearLayout>
<LinearLayout
android:id="@+id/icon_container_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:alpha="0"
android:background="@drawable/info_background"
android:orientation="vertical"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="@+id/size"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_size"
android:padding="12dp"
android:src="@drawable/ic_straighten_white_24dp"
android:tint="@color/icons" />
<ImageView
android:id="@+id/palette"
style="@style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/desc_palette"
android:padding="12dp"
android:src="@drawable/ic_palette_white_24dp"
android:tint="@color/icons" />
</LinearLayout>
</RelativeLayout>
寻呼机本身的 xml 布局
(相当嵌套?可能是问题的原因?我不知道...)
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="end">
<SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.my.example.OptionalViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="horizontal"
app:layout_behavior="com.my.example.MoveUpBehavior" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
android:clickable="false"
android:fitsSystemWindows="false"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_menu_white_24dp" />
</android.support.design.widget.CoordinatorLayout>
</SwipeRefreshLayout>
<include layout="@layout/layout_drawer" />
</android.support.v4.widget.DrawerLayout>
我的适配器中与 ViewHolders 相关的部分
@Override
public int getItemCount() {
return dataset.size();
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.v("Adapter", "CreateViewHolder");
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View rootView = layoutInflater.inflate(R.layout.page, parent, false);
return new MyViewHolder(rootView);
}
@Override
public void onBindViewHolder(MyViewHolder page, int position) {
Log.v("Adapter", String.format("BindViewHolder(%d)", position));
final ViewData viewData = dataset.get(position);
page.bind(viewData);
listener.onViewAdded(position, viewData.getData());
}
@Override
public void onViewRecycled(MyViewHolder page) {
if (page.getData() == null) return;
listener.onViewRemoved(page.getData().id);
}
@Override
public int getItemViewType(int position) {
return 0;
}
ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder implements MyListener {
@BindView(R.id.info_container)
ViewGroup mInfoContainer;
@BindView(R.id.icon_container_top)
ViewGroup mIconContainerTop;
@BindView(R.id.icon_container_bottom)
ViewGroup mIconContainerBottom;
@BindView(R.id.info_rows)
ViewGroup mInfoRows;
@BindView(R.id.loading)
View mIcLoading;
@BindView(R.id.sync_status)
View mIcSyncStatus;
@BindView(R.id.delete)
View mIcDelete;
@BindView(R.id.ic_fav)
View mIcFavorite;
@BindView(R.id.size)
View mIcSize;
@BindView(R.id.palette)
View mIcPalette;
@BindView(R.id.name)
TextView mName;
@BindView(R.id.length)
TextView mLength;
@BindView(R.id.threads)
TextView mThreads;
@BindView(R.id.price)
TextView mPrice;
@Nullable
private MyModel mModel = null;
@Nullable
private Activity mActivity;
public MyViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mActivity= (Activity) itemView.getContext();
if (mActivity!= null) mActivity.addMyListener(this);
}
@OnClick(R.id.delete)
protected void clickDeleteBtn() {
if (mActivity == null || mActivity.getMode() != Mode.EDIT) return;
if (mModel == null) return;
Animations.pop(mIcDelete);
final int modelId = mModel.id;
if (mModel.delete()) {
mActivity.delete(modelId);
}
}
@OnClick(R.id.size)
protected void clickSizeBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_SIZE);
Animations.pop(mIcSize);
}
@OnClick(R.id.palette)
protected void clickPaletteBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_LENGTH);
Animations.pop(mIcPalette);
}
private void initModelViews() {
if (mData == null) return;
final Locale locale = Locale.getDefault();
mName.setValue(String.format(locale, "Model#%d", mModel.id));
mLength.setValue(Html.fromHtml(String.format(locale, itemView.getContext().getString(R.string.template_length), mModel.meters)));
}
/**
* set the icon container to be off screen at the beginning
*/
private void prepareViews() {
new ExpectAnim().expect(mIconContainerTop).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
new ExpectAnim().expect(mIconContainerBottom).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
}
@Nullable
public MyModel getData() {
return mModel;
}
private void enableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(outOfScreen(Gravity.END))
.toAnimation()
.start();
}
private void enableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(outOfScreen(Gravity.BOTTOM))
.toAnimation()
.start();
}
private void enableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(atItsOriginalPosition(), visible())
.toAnimation()
.start();
}
private void disableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(outOfScreen(Gravity.END), invisible())
.toAnimation()
.start();
}
public void bind(@NonNull final ViewData viewData) {
mModel = viewData.getData();
prepareViews();
initModelViews();
}
}
所以,这是我的问题!
在初始化适配器时,我通过一个可观察对象插入了大约 15 到 17 个项目。这似乎是正确的:
但是当水平滑动时,recyclerView 的回调似乎完全混乱并产生奇怪的结果:
你看到回收器根本不尝试回收旧的 viewHolder 了吗?该图像仅显示了正在进行的 "spamming" 的一小部分。有时它会在我缓慢滚动回收器时为同一位置创建一个新的 viewHolder 甚至两次以上!
另一个附带问题是:当前的侦听器应该允许我将绑定/回收事件传递给底层游戏引擎,该引擎将在屏幕上创建销毁实体。由于事件的过度垃圾邮件,它目前也会过度创建这些实体!
我希望 Recycler 第一次(假设在我的示例中是 17 次)创建一个新的 ViewHolder,然后按应有的方式重复使用这些项目。
请帮忙,我在这个问题上被困了 2 天了,在搜索有同样问题但没有运气的人后,我感到很沮丧。 谢谢!
When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
- 使用官方
LinearSnapHelper
将子视图的中心对齐到 RecyclerView 的中心。 - 使用
GravitySnapHelper
library 也可以捕捉到 RecyclerView 的开始或结束,就像 Google Play 商店那样。
这两种解决方案的应用相似:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling.
"Without limitations" 转换为 "infinite speed" 意味着投掷会立即跳到目标位置。这可能不是你想要的。
翻阅SnapHelper
资料后发现有个规律:滚动一英寸需要100毫秒。您可以覆盖此行为。
final SnapHelper snapHelper = new LinearSnapHelper() {
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
snapHelper.attachToRecyclerView(recyclerView);
这是默认速度(MILLISECONDS_PER_INCH = 100
)。试验并找出适合您需求的方法,从 "one inch takes 50 ms to scroll" 开始,依此类推。
ViewHolder
回收显然有问题。我猜你 运行 在 MyViewHolder
中的动画可能会阻止 RecyclerView
正确回收持有人。确保在某个时候取消动画,例如在 RecyclerView.Adapter#onViewDetachedFromWindow()
.
解决此问题后,我建议您按照@EugenPechanec 的建议减少在 OnScrollListener
中完成的自定义计算量。最好依靠支持库 类 并稍微调整一下行为。