RecyclerView 适合屏幕时不要折叠 Toolbar

Posted

技术标签:

【中文标题】RecyclerView 适合屏幕时不要折叠 Toolbar【英文标题】:Don't collapse Toolbar when RecyclerView fits the screen 【发布时间】:2015-12-01 00:30:01 【问题描述】:

我使用 Android 设计库 制作了一个应用程序,带有一个工具栏和 TabLayout。 实际上存在 2 个选项卡,都带有 2 个 RecyclerView,滚动时会自动折叠工具栏。

我的问题是:当 RecyclerView 的项目很少且完全适合屏幕时(如 TAB 2 中),我可以禁用工具栏折叠吗?

我见过很多类似CheeseSquare 的例子,由 Google 员工制作,但问题仍然存在:即使 RecyclerView 只有 1 个项目,工具栏仍会在滚动时隐藏。

我想我可以找出 RecyclerView 的第一项是否在屏幕上可见,如果是,则禁用工具栏折叠。前者容易实现,后者呢?

这是我的布局:

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinator_layout"
    android:layout_
    android:layout_>

        <android.support.design.widget.AppBarLayout
            android:layout_
            android:layout_
            android:fitsSystemWindows="true"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_
            android:layout_
            app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
            android:background="?attr/colorPrimary"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

        <android.support.design.widget.TabLayout
            android:id="@+id/tab_layout"
            android:layout_
            android:layout_
            android:background="@color/glucosio_pink"
            app:tabSelectedTextColor="@android:color/white"
            app:tabIndicatorColor="@color/glucosio_accent"
            app:tabTextColor="#80ffffff"/>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            android:layout_
            android:layout_/>

    <android.support.design.widget.FloatingActionButton
        android:layout_
        android:layout_
        android:id="@+id/main_fab"
        android:layout_margin="16dp"
        android:onClick="onFabClicked"
        app:backgroundTint="@color/glucosio_accent"
        android:src="@drawable/ic_add_black_24dp"
        android:layout_gravity="bottom|right"
        />
    </android.support.design.widget.CoordinatorLayout>

【问题讨论】:

你能告诉我你是如何制作这个 gif 的吗? 嗨@PratikButani!我在互联网上找到了 gif (mzgreen.github.io/2015/06/23/…)。顺便说一句,您可以使用 gifs.com 之类的服务从 YouTube 视频中创建 gif。 用你的链接发给那个人 :) 愿他能帮助你。谢谢 :) twitter.com/pratik13butani/status/644355243174526976 期望它有什么作用?假设您在第一页折叠工具栏,然后滑动到第二页。应该发生什么?工具栏已隐藏,您无法显示它,因为项目太少。并且为了禁用工具栏滚动 - 有可能,我稍后会玩它。也许自定义行为将成为解决方案。 请将您的解决方案写成正确的答案,以便于查找。 【参考方案1】:

最终解决方案(感谢 Michał Z.) 关闭/打开工具栏滚动的方法:

public void turnOffToolbarScrolling() 
    Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
    AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);

    //turn off scrolling
    AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
    toolbarLayoutParams.setScrollFlags(0);
    mToolbar.setLayoutParams(toolbarLayoutParams);

    CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
    appBarLayoutParams.setBehavior(null);
    appBarLayout.setLayoutParams(appBarLayoutParams);


public void turnOnToolbarScrolling() 
    Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
    AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);

    //turn on scrolling
    AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
    toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
    mToolbar.setLayoutParams(toolbarLayoutParams);

    CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
    appBarLayoutParams.setBehavior(new AppBarLayout.Behavior());
    appBarLayout.setLayoutParams(appBarLayoutParams);

找出 RecyclerView 的最后一项是否在我的 Fragment 中可见。 如果是,请禁用滚动:

public void updateToolbarBehaviour()
    if (mLayoutManager.findLastCompletelyVisibleItemPosition() == items.size()-1) 
        ((MainActivity) getActivity()).turnOffToolbarScrolling();
     else 
        ((MainActivity)getActivity()).turnOnToolbarScrolling();
    

【讨论】:

第一次需要延迟调用updateToolbarBehaviour()(使用Handler-Runnable)。否则,findLastCompletelyVisibleItemPosition() 方法最初返回“-1”。 这个解决方案仍然有效。并用于折叠工具栏。 一个简单的逻辑东西的大量代码。在带有工具栏的活动中使用片段时,它变得更加复杂。【参考方案2】:

RecyclerView 现在(从 23.2 版开始)支持wrap_content。只需使用wrap_content 作为高度。

【讨论】:

不幸的是,当我用 RecyclerView 包装 SwipeRefreshLayout 时它对我不起作用 这对我使用 NestedScrollView 有效。谢谢! 这看起来是最好的解决方案。我不确定这是 coordinator/recyclerView 的一个完全假定的功能,但它绝对可以在没有开发人员的情况下使用。在 wrap_content 中,如果它的视口没有被元素填充,回收器不会触发滚动事件。 也为我工作!重要的是要注意 RecyclerView 必须是 CoordinatorLayout 的第一个孩子(我的首先是在嵌套滚动视图中,这阻止了 android 理解滚动不是必需的)。【参考方案3】:

您可以检查RecyclerView 中的最后一项是否可见。如果不是,则使用此方法以编程方式关闭滚动:

            //turn off scrolling
            AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
            toolbarLayoutParams.setScrollFlags(0);
            mToolbar.setLayoutParams(toolbarLayoutParams);

            CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
            appBarLayoutParams.setBehavior(null);
            appBarLayout.setLayoutParams(appBarLayoutParams);

【讨论】:

效果很好,谢谢!我已经用 off 和 on 方法更新了我的问题。 效果很好,但是如果视图的内容具有 app:layout_behavior="@string/appbar_scrolling_view_behavior" 并且还具有 layout_ 关闭滚动后内容不会居中 这只是为了禁用滚动,但如果为不同的片段重新启用相同的功能将不起作用,请以编程方式设置滚动标志。【参考方案4】:

我采取了稍微不同的方法来解决这个问题。

我创建了一个自定义 AppBarBehavior,它根据单元格禁用其自身。

public class CustomAppBarBehavior extends AppBarLayout.Behavior 

    private RecyclerView recyclerView;
    private boolean enabled;

    public CustomAppBarBehavior() 
    

    public CustomAppBarBehavior(Context context, AttributeSet attrs) 
        super(context, attrs);
    

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) 
        updatedEnabled();
        return enabled && super.onInterceptTouchEvent(parent, child, ev);
    

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) 
        return enabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) 
        return enabled && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    

    private void updatedEnabled() 
        enabled = false;
        if(recyclerView != null) 
            RecyclerView.Adapter adapter = recyclerView.getAdapter();
            if (adapter != null) 
                int count = adapter.getItemCount();
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager != null) 
                    int lastItem = 0;
                    if (layoutManager instanceof LinearLayoutManager) 
                        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                        lastItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());
                     else if (layoutManager instanceof StaggeredGridLayoutManager) 
                        StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                        int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);
                        lastItem = Math.abs(lastItems[lastItems.length - 1]);
                    
                    enabled = lastItem < count - 1;
                
            
        
    

    public void setRecyclerView(RecyclerView recyclerView) 
        this.recyclerView = recyclerView;
    

然后在应用栏布局上设置自定义行为

appBarBehavior = new CustomAppBarBehavior();
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(appBarBehavior);
appBarLayout.setLayoutParams(appBarLayoutParams);

View pager 的 Last on page change 更新了 RecyclerView 上的行为

private ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() 
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)  

        @Override
        public void onPageSelected(final int position)              
            appBarLayout.setExpanded(true, true);
            appBarLayout.post(new Runnable() 
                @Override
                public void run() 
                    appBarBehavior.setRecyclerView(childFragments.get(position).getRecyclerView());
                
            );
        

        @Override
        public void onPageScrollStateChanged(int state)  
    ;

这应该适用于更改数据集。

【讨论】:

真的很有趣。感谢分享。 哦,您还需要在旋转时重置 recyclerview,记住视图寻呼机在恢复后会给出索引 0,即使它不在该页面上。碎片是一种快乐。【参考方案5】:

在您更改适配器中的数据后添加此代码:

recyclerView.afterMeasured 
    val isTurnedOff = recyclerView.turnOffNestedScrollingIfEnoughItems()
    if (isTurnedOff) appBarLayout.setExpanded(true)

这是功能:

inline fun <T: View> T.afterMeasured(crossinline action: T.() -> Unit) 
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener 
        override fun onGlobalLayout()              
            viewTreeObserver.removeOnGlobalLayoutListener(this)
            action()
        
    )



fun RecyclerView.turnOffNestedScrollingIfEnoughItems(): Boolean 
    val lm = (layoutManager as LinearLayoutManager)
    val count = if (lm.itemCount <= 0) 0 else lm.itemCount - 1
    val isFirstVisible = lm.findFirstCompletelyVisibleItemPosition() == 0
    val isLastItemVisible = lm.findLastCompletelyVisibleItemPosition() == count

    isNestedScrollingEnabled = !(isLastItemVisible && isFirstVisible)
    return isNestedScrollingEnabled.not()

【讨论】:

【参考方案6】:

我想这是最好的解决方案。 您需要定义您的自定义 AppBarLayout 行为:

class CustomScrollingViewBehavior : AppBarLayout.Behavior 

    constructor() : super()
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    var shouldScroll = true

    override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, axes: Int, type: Int): Boolean 
        return shouldScroll && when (target) 
            is NestedScrollView, is ScrollView, is RecyclerView -> 
                return target.canScrollVertically(1) || target.canScrollVertically(-1)
            
            else -> super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
        
    

然后你只需要在你的布局中使用它作为 AppBarLayout 的属性:

...

 <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar"
            android:layout_
            android:layout_
            app:layout_behavior=".CustomScrollingViewBehavior">

...

就是这样。

注意:自定义行为还支持完全禁用滚动 - 您只需将 shouldScroll 标志设置为 false

customScrollingViewBehavior.shouldScroll = false

【讨论】:

【参考方案7】:

如果根据正确答案帮助 Kotlin 中的任何人,我为 Kotlin 做了这个:

fun changeToolbarScroll(isToScrolling: Boolean)

    val params = toolbar.layoutParams as AppBarLayout.LayoutParams
    val appBarLayoutParams = appBarLayout.layoutParams as 
                             CoordinatorLayout.LayoutParams
    params.scrollFlags = 0
    toolbar.layoutParams = params
    appBarLayoutParams.behavior = null
    appBarLayout.layoutParams = appBarLayoutParams

    if(isToScrolling)
        params.scrollFlags =
            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or 
            AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
        toolbar.layoutParams = params

        appBarLayoutParams.behavior = AppBarLayout.Behavior()
        appBarLayout.layoutParams = appBarLayoutParams
    

在我的例子中,我有一个 MainActivity 的问题,它由 2 个 Fragment 管理导航、工具栏和其他共享的东西,第一个 Framgent 使用 RecyclerView,第二个用于显示细节。 问题是当我设置一个菜单并从 MainAcitivity 更改 MenuItem 时

这听起来很傻而且完全合乎逻辑,但请记住在调用 supportFragmentManager.beginTransaction() 之前总是对 Menu 或 MenuItems 进行更改,否则更改片段不起作用或无法正确更新,无论如何.add、.replace()、show() 的变化...

fun showDetailImageFragment(searchImage: SearchImage) 
    val searchFragment = 
    supportFragmentManager.findFragmentByTag(SEARCH_IMAGES)

    changeToolbarScroll(false)

    if (supportActionBar != null) 
        supportActionBar!!.collapseActionView()
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
        supportActionBar!!.title = getString(R.string.detail_image_title)
    

    actionSearch.isVisible = false
    actionNighMOde.isVisible = false
    actionAppSetings.isVisible = false
    actionAbout.isVisible = false

    supportFragmentManager.beginTransaction()
        .setCustomAnimations(
            R.animator.fade_in,
            R.animator.fade_out,
            R.animator.fade_in,
            R.animator.fade_out
        )
        .hide(searchFragment!!)
        .add(
            R.id.frameLayout,
            DetailImageFragment().newInstance(searchImage)
        ).addToBackStack(null)
        .commit()

【讨论】:

【参考方案8】:

只需从

中删除滚动条
app:layout_scrollFlags="scroll|enterAlways"

应该是这样的

app:layout_scrollFlags="enterAlways"

【讨论】:

【参考方案9】:

您可以在您的 XML 中添加属性 layout_behaviour,其值为 @string/appbar_scrolling_view_behavior

app:layout_behavior="@string/appbar_scrolling_view_behavior"

【讨论】:

【参考方案10】:
//turn off scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mToolbar.setLayoutParams(toolbarLayoutParams);

【讨论】:

以上是关于RecyclerView 适合屏幕时不要折叠 Toolbar的主要内容,如果未能解决你的问题,请参考以下文章

在 ConstraintLayout 中滚动 RecyclerView 时如何折叠工具栏?

带有 MotionLayout 的 Android 折叠工具栏 - 当 RecyclerView 为空/不可滚动时禁用运动

在 RecyclerView 上设置最大高度

关于折叠滑动吸顶tab置顶问题(recyclerView嵌套与CollapsingToolbarLayout冲突)

RecyclerView 展开/折叠项目

给 RecyclerView 加上折叠的效果