RecyclerView 没有按预期滚动

Posted

技术标签:

【中文标题】RecyclerView 没有按预期滚动【英文标题】:RecyclerView does not scroll as expected 【发布时间】:2014-10-30 07:21:00 【问题描述】:

我有一个项目,我使用水平回收器视图,我想将一个元素居中。我的实现有效,但并非在所有情况下都在此处查看此 GIF:

您可能会注意到,如果我从左边来,它会正确滚动。如果我从右边来,它会过度滚动很多,我不知道如何停止,也不知道如何解决。

我在这里将我的代码条带化到这个例子中:

public class DemoActivity extends ActionBarActivity implements View.OnClickListener 
    private static final int JUMP_TO_LEFT = MyAdapter.NON_VISIBLE_ITEMS + MyAdapter.VISIBLE_ITEMS - 1;
    private static final int JUMP_TO_RIGHT = MyAdapter.NON_VISIBLE_ITEMS;
    private LinearLayoutManager mLayoutManager;
    private RecyclerView mRecycler;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        findViewById(android.R.id.button1).setOnClickListener(this);
        mRecycler = (RecyclerView)findViewById(R.id.recycler);
        MyAdapter mAdapter = new MyAdapter();
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        mRecycler.setLayoutManager(mLayoutManager);
        mRecycler.setHasFixedSize(true);
        mRecycler.scrollToPosition(MyAdapter.NON_VISIBLE_ITEMS);
        mRecycler.setAdapter(mAdapter);
    

    @Override
    public void onClick(View v) 
        int pos = mLayoutManager.findFirstVisibleItemPosition();
        int outer = (MyAdapter.VISIBLE_ITEMS - 1) / 2;
        if(pos + outer >= MyAdapter.ITEM_IN_CENTER) 
            mRecycler.smoothScrollToPosition(JUMP_TO_RIGHT);
         else 
            mRecycler.smoothScrollToPosition(JUMP_TO_LEFT);
        
    

这是我的适配器:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.Holder> implements View.OnClickListener 
    public static final int VISIBLE_ITEMS = 7;
    public static final int NON_VISIBLE_ITEMS = 150;
    private static final int TOTAL_ITEMS = VISIBLE_ITEMS + NON_VISIBLE_ITEMS * 2;
    public static final int ITEM_IN_CENTER = (int)Math.ceil(VISIBLE_ITEMS / 2f) + NON_VISIBLE_ITEMS;

    private Calendar mCalendar;

    public MyAdapter() 
        mCalendar = GregorianCalendar.getInstance();
        setHasStableIds(true);
    

    private int getToday() 
        return (int)TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
    

    @Override
    public int getItemCount() 
        return TOTAL_ITEMS;
    

    @Override
    public Holder onCreateViewHolder(ViewGroup parent, int viewType) 
        final TextView tv = new TextView(parent.getContext());
        int width = parent.getWidth() / VISIBLE_ITEMS;
        tv.setLayoutParams(new TableRow.LayoutParams(width, ViewGroup.LayoutParams.MATCH_PARENT, 1));
        tv.setGravity(Gravity.CENTER);
        tv.setBackgroundColor(Color.TRANSPARENT);
        DisplayMetrics metrics = tv.getContext().getResources().getDisplayMetrics();
        float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, metrics);
        tv.setLineSpacing(padding, 1f);
        tv.setPadding(0, (int)padding, 0, 0);
        tv.setOnClickListener(this);
        return new Holder(tv);
    

    @Override
    public void onBindViewHolder(Holder holder, int position) 
        int today = getToday();
        mCalendar.setTimeInMillis(System.currentTimeMillis());
        mCalendar.set(Calendar.HOUR_OF_DAY, 12); // set to noon to avoid energy saver time problems
        mCalendar.add(Calendar.DAY_OF_YEAR, position - ITEM_IN_CENTER + 1);
        DateFormat format = new SimpleDateFormat("E\nd");
        String label = format.format(mCalendar.getTime()).replace(".\n", "\n");
        int day = (int)TimeUnit.MILLISECONDS.toDays(mCalendar.getTimeInMillis());
        holder.update(day, today, label);
    

    @Override
    public long getItemId(int position) 
        mCalendar.setTimeInMillis(System.currentTimeMillis());
        mCalendar.set(Calendar.HOUR_OF_DAY, 12); // set to noon to avoid energy saver time problems
        mCalendar.add(Calendar.DAY_OF_YEAR, position - ITEM_IN_CENTER + 1);
        DateFormat format = new SimpleDateFormat("dMMyyyy");
        return Long.parseLong(format.format(mCalendar.getTime()));
    

    @Override
    public void onClick(View v) 
        String day = ((TextView)v).getText().toString().replace("\n", " ");
        Toast.makeText(v.getContext(), "You clicked on " + day, Toast.LENGTH_SHORT).show();
    

    public class Holder extends RecyclerView.ViewHolder 
        private final Typeface font;

        private Holder(TextView v) 
            super(v);
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) 
                font = Typeface.create("sans-serif-light", Typeface.NORMAL);
             else 
                font = null;
            
        

        public void update(int day, int today, String label) 
            TextView tv = (TextView)itemView;
            tv.setText(label);

            if(day == today) 
                tv.setTextSize(18);
                tv.setTypeface(null, Typeface.BOLD);
             else 
                tv.setTextSize(16);
                tv.setTypeface(font, Typeface.NORMAL);
            

            tv.setBackgroundColor(0xff8dc380);
        
    

你觉得有什么原因吗?为了让您更简单,我还将这段代码放在了 GitHub 上。 https://github.com/rekire/RecylcerViewBug

【问题讨论】:

你可以试试 FancyCoverFlow 作为水平选择器。它像 viewpager 一样工作 @berserk lib 使用了我不想使用的已弃用的GalleryView 请把那些 SimpleDateFormat 作为字段,创建两个每个视图回收太多了。 【参考方案1】:

我发现了一个令人惊讶的简单解决方法:

@Override
public void onClick(View v) 
    int pos = mLayoutManager.findFirstVisibleItemPosition();
    int outer = (MyAdapter.VISIBLE_ITEMS + 1) / 2;
    int delta = pos + outer - ForecastAdapter.ITEM_IN_CENTER;
    //Log.d("Scroll", "delta=" + delta);
    View firstChild = mForecast.getChildAt(0);
    if(firstChild != null) 
        mForecast.smoothScrollBy(firstChild.getWidth() * -delta, 0);
    

我在这里计算自己跳跃的宽度,这正是我想要的。

【讨论】:

嗨 @rekire:这里的 ForecastAdapter.ITEM_IN_CENTER 是什么? 在我的例子中是 151,因为我有 301 个元素。【参考方案2】:

在具有垂直方向的 LinearLayoutManager 的情况下,您可以创建自己的 SmoothScroller 并覆盖 calculateDyToMakeVisible() 方法,您可以在其中设置所需的视图位置。例如,要在 smoothScroll() 之后让目标视图始终出现在 RecyclerView 的顶部,请这样写:

class CustomLinearSmoothScroller extends LinearSmoothScroller 

    public CustomLinearSmoothScroller(Context context) 
        super(context);
    

    @Override
    public int calculateDyToMakeVisible(View view, int snapPreference) 
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (!layoutManager.canScrollVertically()) 
            return 0;
        
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)view.getLayoutParams();
        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
        final int viewHeight = bottom - top;
        final int start = layoutManager.getPaddingTop();
        final int end = start + viewHeight;
        return calculateDtToFit(top, bottom, start, end, snapPreference);
    

"top" 和 "bottom" - 目标视图的边界

“start”和“end” - 在smoothScroll期间视图应该放置在它们之间的点

【讨论】:

不错的答案.... 没有完全解决问题,但使滚动好多了。我通过将 +1000 添加到 int top 来修复它。不推荐它,因为它是一个丑陋的黑客,但对于那些有同样问题的人来说,这将是 C 或 D 计划。 对于垂直,这是为我做的:linearLayoutManager.scrollToPositionWithOffset(position, 0);【参考方案3】:

这对我有用:

itemsView.smoothScrollBy(-recyclerView.computeHorizontalScrollOffset(), 0)

【讨论】:

【参考方案4】:

要支持平滑滚动,您必须覆盖 smoothScrollToPosition(RecyclerView, State, int) 并创建一个 RecyclerView.SmoothScroller。

RecyclerView.LayoutManager 负责 用于创建实际的滚动动作。如果您想提供自定义 平滑滚动逻辑,覆盖 smoothScrollToPosition(RecyclerView, State, int) 在你的 LayoutManager 中。

https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html#smoothScrollToPosition(int)

在您的情况下,使用 smoothScrollBy 可能是一种解决方法(不需要此覆盖)。

【讨论】:

以上是关于RecyclerView 没有按预期滚动的主要内容,如果未能解决你的问题,请参考以下文章

当 RecyclerView 滚动时,从 ThreadPool 中杀死正在运行的线程

Android Recycer 流式布局

滚动视图没有按预期滚动

滚动视图中的 tableview,tableview didselect 没有按预期工作

可过滤在 Recyclerview 中无法按预期工作

为啥 recyclerview 的 searchview 不起作用?