0

目前,我对 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 天,在搜索有同样问题但没有运气的人后,我感到很沮丧。谢谢!

4

2 回答 2

2

当投掷停止或用户手动停止时,回收器应该滚动到一个项目(有点模仿 viewPager)

  • 使用将LinearSnapHelper子视图中心捕捉到 RecyclerView 中心的官方。
  • 使用一个也可以捕捉到 RecyclerView 开始或结束的GravitySnapHelper,就像 Google Play 商店一样。

这两种解决方案的应用类似:

new LinearSnapHelper().attachToRecyclerView(recyclerView);

一个水平的 RecyclerView,用户可以在其上快速滑动,而不受任何限制。

“无限制”翻译为“无限速度”,意思是一扔会立即跳到目标位置。这可能不是你想要的。

浏览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)。试验并找出适合您的需求,从“滚动一英寸需要 50 毫秒”等开始。

于 2017-03-28T11:47:47.317 回答
2

ViewHolder回收显然存在问题。我猜你在里面运行的动画MyViewHolder可能会阻止RecyclerView正确回收持有人。确保在某些时候取消动画,例如在RecyclerView.Adapter#onViewDetachedFromWindow().

解决此问题后,我建议您按照@EugenPechanec 的建议减少在OnScrollListeners 中完成的自定义计算量。最好依靠支持库类并稍微调整一下行为。

于 2017-03-28T11:57:54.767 回答