简体   繁体   English

RecyclerView-Horizo​​ntal LinearLayoutManager创建/绑定方法的方式经常被调用

[英]RecyclerView - Horizontal LinearLayoutManager create / bind methods called way too often

Currently I'm at the end of my ideas on following issue with LinearLayoutManagers and RecyclerViews on Android: 目前,我对关于Android上的LinearLayoutManagers和RecyclerViews的以下问题的想法已经结束:

What scenario I wanted to achieve 我想实现什么方案

A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling. 用户可以在其上快速滑动的水平RecyclerView,而不会受到任何限制。 The items being fullscreen sized making them as big as the recyclerview itself. 全屏尺寸的物品使它们与recyclerview本身一样大。 When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit) (I'm using support revision 25.1.0) 当逃逸停止或用户手动停止时,回收站应滚动到一项(有点模仿viewPager)(我使用的是支持版本25.1.0)

code snippets 代码段

The Pager-class itself 传呼机类本身

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);

    }

}

The item's xml layout 项目的xml布局

(Note: the root view has to be clickable for other purposes inside the app) (注意:根视图必须可在应用程序内用于其他目的单击)

<?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>

The xml layout with the pager itself 带有分页器本身的xml布局

(Quite nested? Might be a cause of the problem? I don't know... ) (非常嵌套吗?可能是问题的原因吗?我不知道...)

<?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>

part of my adapter that is relevant for ViewHolders 与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;
    }

The ViewHolder 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();
    }

}

So, here's my issue with these! 所以,这是我的问题!

When intializing the adapter I insert about 15 to 17 items via an observable. 初始化适配器时,我通过观察插入约15到17项。 This seems to be correct: 这似乎是正确的:

记录初始化即可

but when swiping horizontally the recyclerView's callbacks seem to be totally messed up and produce weird results: 但是在水平滑动时,recyclerView的回调似乎完全混乱了,并产生了奇怪的结果:

混乱的伐木

Do you see that the recycler does not try to recycle old viewHolders at all? 您是否看到回收商根本不尝试回收旧的viewHolders? The image just shows a small portion of the "spamming" that is going on. 该图像仅显示了正在进行的“垃圾邮件”的一小部分。 Sometimes it will create a new viewHolder even more than two times for the same position while I scroll the recycler slowly! 有时,当我缓慢滚动回收器时,它会为同一位置创建两次以上的新viewHolder!

垃圾邮件

Another side problem is: The listener currently should allow me to pass the bind / recycle events to an underlying game engine which will create destroy entities on the screen. 另一个问题是:侦听器当前应允许我将绑定/回收事件传递给底层的游戏引擎,该引擎将在屏幕上创建销毁实体。 Due the excessive spamming of the events it will currently create those entities also excessively! 由于事件的垃圾邮件过多,因此当前将过多地创建这些实体!

I excpected the Recycler to create a new ViewHolder for the first (let's say in my example 17) times and then just reuse the items how it should. 我期望Recycler可以第一次创建一个新的ViewHolder(在示例17中为例),然后再按需重复使用这些项。

Please help, I'm stuck on this problem for 2 days now and I'm frustrated after searching people with same issues but without luck. 请帮忙,我在这个问题上停留了2天,在搜寻有同样问题但没有运气的人后感到沮丧。 Thank you! 谢谢!

When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit) 当逃逸停止或用户手动停止时,回收站应滚动到一项(有点模仿viewPager)

  • Use the official LinearSnapHelper which snaps center of child view to center of RecyclerView. 使用官方的LinearSnapHelper ,它将子视图的中心对齐到RecyclerView的中心。
  • Use a GravitySnapHelper library which can also snap to start of or end of RecyclerView, just like Google Play store does. 使用GravitySnapHelper ,该也可以像Google Play商店一样捕捉到RecyclerView的开始或结束。

Both of these solutions are applied similarly: 这两种解决方案的应用类似:

new LinearSnapHelper().attachToRecyclerView(recyclerView);

A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling. 用户可以在其上快速滑动的水平RecyclerView,而不会受到任何限制。

"Without limitations" translates to "infinite speed" meaning a fling would instantly jump to target position. “无限制”转换为“无限速度”,意味着猛击会立即跳到目标位置。 That's probably not what you want. 那可能不是您想要的。

After going through SnapHelper source I found out that there is a rule: one inch takes 100 milliseconds to scroll. 经过SnapHelper源代码后,我发现有一条规则:一英寸滚动需要100毫秒。 You can override this behavior. 您可以覆盖此行为。

final SnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

That's the default speed (where MILLISECONDS_PER_INCH = 100 ). 这是默认速度(其中MILLISECONDS_PER_INCH = 100 )。 Experiment and find out what fits your needs, start with "one inch takes 50 ms to scroll" and so on. 实验并找出适合您需求的内容,从“一英寸滚动需要50毫秒”开始,依此类推。

There's obviously a problem with ViewHolder recycling. ViewHolder回收显然存在问题。 I'm guessing the animations you're running inside MyViewHolder might prevent RecyclerView from recycling holders properly. 我猜想您在MyViewHolder运行的动画可能会阻止RecyclerView正确回收持有人。 Make sure you cancel animations at some point, eg in RecyclerView.Adapter#onViewDetachedFromWindow() . 确保在某些时候取消动画,例如在RecyclerView.Adapter#onViewDetachedFromWindow()

After you've fixed this, I suggest you follow @EugenPechanec's suggestion to reduce the amount of custom calculations done in the OnScrollListener s. 解决此问题后,建议您遵循@EugenPechanec的建议,以减少在OnScrollListener完成的自定义计算量。 It's better to rely on support library classes and tweak the behavior a little. 最好依靠支持库类并稍微调整一下行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM