目前,我对 Android 上的 LinearLayoutManagers 和 RecyclerViews 的以下问题的想法已经结束:
我想达到什么场景
一个水平的 RecyclerView,用户可以在其上快速滑动,而不受任何限制。全屏大小的项目使它们与recyclerview本身一样大。当投掷停止或用户手动停止时,回收器应该滚动到一个项目(有点模仿 viewPager)(我正在使用支持修订版 25.1.0)
代码片段
寻呼机类本身
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;
}
视图持有者
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();
}
}
所以,这是我的问题!
初始化适配器时,我通过 observable 插入大约 15 到 17 个项目。这似乎是正确的:
但是当水平滑动时,recyclerView 的回调似乎完全搞砸了,并产生了奇怪的结果:
你看到回收者根本没有尝试回收旧的 viewHolders 吗?该图像仅显示了正在发生的“垃圾邮件”的一小部分。有时,当我慢慢滚动回收器时,它会为同一位置创建一个新的 viewHolder 甚至超过两次!
另一个问题是:监听器当前应该允许我将绑定/回收事件传递给底层游戏引擎,该引擎将在屏幕上创建销毁实体。由于事件的过度垃圾邮件,它目前也会过度创建这些实体!
我期望 Recycler 第一次(比如说在我的示例 17 中)创建一个新的 ViewHolder,然后按照它应该的方式重用这些项目。
请帮忙,我在这个问题上停留了 2 天,在搜索有同样问题但没有运气的人后,我感到很沮丧。谢谢!