web-dev-qa-db-fra.com

La barre d’outils dans AppBarLayout est déroulable bien que RecyclerView n’ait pas assez de contenu pour faire défiler

Est-il vraiment prévu que la barre d'outils d'un AppBarLayout soit déroulable bien que le conteneur principal avec "appbar_scrolling_view_behavior" ne contienne pas assez de contenu pour vraiment faire défiler?

Ce que j'ai testé jusqu'à présent:
Lorsque j'utilise un NestedScrollView (avec l'attribut "wrap_content") comme conteneur principal et un TextView comme enfant, AppBarLayout fonctionne correctement et ne fait pas défiler.

Cependant, lorsque j'utilise un RecyclerView avec seulement quelques entrées et l'attribut "wrap_content" (afin qu'il ne soit pas nécessaire de faire défiler), la barre d'outils de AppBarLayout peut défiler même si RecyclerView ne reçoit jamais d'événement de défilement (testé avec OnScrollChangeListener ).

Voici mon code de mise en page:

<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/coordinatorLayout"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <Android.support.design.widget.AppBarLayout
        Android:id="@+id/appBarLayout"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content">

        <Android.support.v7.widget.Toolbar
            Android:id="@+id/toolbar"
            Android:layout_width="match_parent"
            Android:layout_height="?attr/actionBarSize"
            Android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:theme="@style/ToolbarStyle" />
    </Android.support.design.widget.AppBarLayout>

    <Android.support.v7.widget.RecyclerView
        Android:id="@+id/recycler"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</Android.support.design.widget.CoordinatorLayout>

L’effet suivant est que la barre d’outils peut défiler sans que cela soit nécessaire:

 

J'ai également trouvé un moyen de résoudre ce problème en vérifiant si tous les éléments RecyclerView sont visibles et en utilisant la méthode setNestedScrollingEnabled () de RecyclerView.
Néanmoins, il me semble plus que cela un bogue. Des opinions? :RÉ

EDIT # 1:

Pour les personnes qui pourraient être intéressées par ma solution actuelle, je devais mettre la logique setNestedScrollingEnabled () dans la méthode postDelayed () d'un gestionnaire avec un délai de 5 ms en raison du LayoutManager qui retournait toujours -1 lors de l'appel des méthodes pour savoir si le premier et le dernier élément sont visibles.
J'utilise ce code dans la méthode onStart () (après l'initialisation de mon RecyclerView) et chaque fois après un changement de contenu du RecyclerView.

final LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        //no items in the RecyclerView
        if (mRecyclerView.getAdapter().getItemCount() == 0)
            mRecyclerView.setNestedScrollingEnabled(false);
        //if the first and the last item is visible
        else if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0
                && layoutManager.findLastCompletelyVisibleItemPosition() == mRecyclerView.getAdapter().getItemCount() - 1)
            mRecyclerView.setNestedScrollingEnabled(false);
        else
            mRecyclerView.setNestedScrollingEnabled(true);
    }
}, 5);

EDIT # 2:

Je viens de jouer avec une nouvelle application et il semble que ce comportement (inattendu) ait été corrigé dans la version de bibliothèque prise en charge 23.3.0 (ou même avant). Ainsi, les solutions de contournement ne sont plus nécessaires!

57
eickeee

Edit 2:

Il s'avère que le seul moyen de s'assurer que la barre d'outils ne peut pas défiler lorsque RecyclerView n'est pas défilable consiste à définir setScrollFlags par programme, ce qui nécessite de vérifier si RecyclerView est défilable. Cette vérification doit être effectuée chaque fois que l’adaptateur est modifié. 

Interface pour communiquer avec l'activité:

public interface LayoutController {
    void enableScroll();
    void disableScroll();
}

Activité principale:

public class MainActivity extends AppCompatActivity implements 
    LayoutController {

    private CollapsingToolbarLayout collapsingToolbarLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        collapsingToolbarLayout = 
              (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);

        final FragmentManager manager = getSupportFragmentManager();
        final Fragment fragment = new CheeseListFragment();
        manager.beginTransaction()
                .replace(R.id.root_content, fragment)
                .commit();
    }

    @Override
    public void enableScroll() {
        final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
                                  collapsingToolbarLayout.getLayoutParams();
        params.setScrollFlags(
                AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 
                | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
        );
        collapsingToolbarLayout.setLayoutParams(params);
    }

    @Override
    public void disableScroll() {
        final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
                                  collapsingToolbarLayout.getLayoutParams();
        params.setScrollFlags(0);
        collapsingToolbarLayout.setLayoutParams(params);
    }
}

activity_main.xml:

<Android.support.v4.widget.DrawerLayout
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:id="@+id/drawer_layout"
    Android:layout_height="match_parent"
    Android:layout_width="match_parent"
    Android:fitsSystemWindows="true">

    <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/main_content"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent">

        <Android.support.design.widget.AppBarLayout
            Android:id="@+id/appbar"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <Android.support.design.widget.CollapsingToolbarLayout
                Android:id="@+id/collapsing_toolbar"
                Android:layout_width="match_parent"
                Android:layout_height="match_parent"
                Android:fitsSystemWindows="true"
                app:contentScrim="?attr/colorPrimary">

                <Android.support.v7.widget.Toolbar
                    Android:id="@+id/toolbar"
                    Android:layout_width="match_parent"
                    Android:layout_height="?attr/actionBarSize"
                    Android:background="?attr/colorPrimary"
                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

            </Android.support.design.widget.CollapsingToolbarLayout>

        </Android.support.design.widget.AppBarLayout>

        <FrameLayout
            Android:id="@+id/root_content"
            Android:layout_width="match_parent"
            Android:layout_height="match_parent"
            Android:layout_gravity="fill_vertical"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    </Android.support.design.widget.CoordinatorLayout>

</Android.support.v4.widget.DrawerLayout>

Fragment de test:

public class CheeseListFragment extends Fragment {

    private static final int DOWN = 1;
    private static final int UP = 0;

    private LayoutController controller;
    private RecyclerView rv;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        try {
            controller = (MainActivity) getActivity();
        } catch (ClassCastException e) {
            throw new RuntimeException(getActivity().getLocalClassName()
                    + "must implement controller.", e);
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        rv = (RecyclerView) inflater.inflate(
                R.layout.fragment_cheese_list, container, false);
        setupRecyclerView(rv);

        // Find out if RecyclerView are scrollable, delay required
        final Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (rv.canScrollVertically(DOWN) || rv.canScrollVertically(UP)) {
                    controller.enableScroll();
                } else {
                    controller.disableScroll();
                }
            }
        }, 100);

        return rv;
    }

    private void setupRecyclerView(RecyclerView recyclerView) {
        final LinearLayoutManager layoutManager = new LinearLayoutManager(recyclerView.getContext());

        recyclerView.setLayoutManager(layoutManager);

        final SimpleStringRecyclerViewAdapter adapter =
                new SimpleStringRecyclerViewAdapter(
                        getActivity(),
                        // Test ToolBar scroll
                        getRandomList(/* with enough items to scroll */)
                        // Test ToolBar pin
                        getRandomList(/* with only 3 items*/)
                );

        recyclerView.setAdapter(adapter);
    }
}

Sources:

Modifier:

Vous devez CollapsingToolbarLayout pour contrôler le comportement.

L'ajout d'une barre d'outils directement à un AppBarLayout vous donne accès aux indicateurs de défilement enterAlwaysCollapsed et exitUntilCollapsed, mais pas au contrôle détaillé de la réaction de différents éléments en cas de réduction . [...] Setup utilise l'application de CollapsingToolbarLayout: layout_collapseMode = "pin" pour s'assurer que la barre d'outils reste épinglée en haut de l'écran pendant que la vue s'effondre . http://Android-developers.blogspot.com.tr /2015/05/Android-design-support-library.html

<Android.support.design.widget.CollapsingToolbarLayout
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed">

    <Android.support.v7.widget.Toolbar
        Android:id="@+id/drawer_toolbar"
        Android:layout_width="match_parent"
        Android:layout_height="?attr/actionBarSize"
        app:layout_collapseMode="pin"/>

</Android.support.design.widget.CollapsingToolbarLayout>

Ajouter

app:layout_collapseMode="pin"

à votre barre d'outils en XML.

    <Android.support.v7.widget.Toolbar
        Android:id="@+id/toolbar"
        Android:layout_width="match_parent"
        Android:layout_height="?attr/actionBarSize"
        Android:background="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways"
        app:layout_collapseMode="pin"
        app:theme="@style/ToolbarStyle" />
6
user3623735

Donc, bon crédit, cette réponse l’a presque résolue pour moi https://stackoverflow.com/a/32923226/5050087 . Mais comme il ne montrait pas la barre d’outils alors que vous aviez réellement une vue recyclée à défilement et que son dernier élément était visible (il ne montrerait pas la barre d’outils au premier défilement), j’ai décidé de la modifier et de l’adapter pour une implémentation plus facile adaptateurs.

Tout d’abord, vous devez créer un comportement de disposition personnalisé pour votre barre d’application:

public class ToolbarBehavior extends AppBarLayout.Behavior{

private boolean scrollableRecyclerView = false;
private int count;

public ToolbarBehavior() {
}

public ToolbarBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
    return scrollableRecyclerView && super.onInterceptTouchEvent(parent, child, ev);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
    updatedScrollable(directTargetChild);
    return scrollableRecyclerView && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}

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

private void updatedScrollable(View directTargetChild) {
    if (directTargetChild instanceof RecyclerView) {
        RecyclerView recyclerView = (RecyclerView) directTargetChild;
        RecyclerView.Adapter adapter = recyclerView.getAdapter();
        if (adapter != null) {
            if (adapter.getItemCount()!= count) {
                scrollableRecyclerView = false;
                count = adapter.getItemCount();
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager != null) {
                    int lastVisibleItem = 0;
                    if (layoutManager instanceof LinearLayoutManager) {
                        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                        lastVisibleItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());
                    } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                        StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                        int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);
                        lastVisibleItem = Math.abs(lastItems[lastItems.length - 1]);
                    }
                    scrollableRecyclerView = lastVisibleItem < count - 1;
                }
            }
        }
    } else scrollableRecyclerView = true;
  }
}

Ensuite, il vous suffit de définir ce comportement pour votre barre d’application dans votre fichier de présentation:

<Android.support.design.widget.AppBarLayout
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    Android:fitsSystemWindows="true"
    app:layout_behavior="com.yourappname.whateverdir.ToolbarBehavior"
    >

Je ne l'ai pas testé pour la rotation de l'écran, alors laissez-moi savoir si cela fonctionne comme ça. Je suppose que cela devrait fonctionner car je ne pense pas que la variable de comptage soit enregistrée lorsque la rotation est effectuée, mais laissez-moi savoir si ce n'est pas le cas. 

Ce fut la mise en œuvre la plus facile et la plus propre pour moi, profitez-en.

5
emirua

Je l'ai implémenté en utilisant ma propre classe Behavior qui peut être attachée à AppBarLayout:

public class CustomAppBarLayoutBehavior extends AppBarLayout.Behavior {

private RecyclerView recyclerView;
private int additionalHeight;

public CustomAppBarLayoutBehavior(RecyclerView recyclerView, int additionalHeight) {
    this.recyclerView = recyclerView;
    this.additionalHeight = additionalHeight;
}

public boolean isRecyclerViewScrollable(RecyclerView recyclerView) {
    return recyclerView.computeHorizontalScrollRange() > recyclerView.getWidth() || recyclerView.computeVerticalScrollRange() > (recyclerView.getHeight() - additionalHeight);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
    if (isRecyclerViewScrollable(mRecyclerView)) {
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }
    return false;
}

}

Et ci-dessous est le code comment définir ce comportement:

final View appBarLayout = ((DrawerActivity) getActivity()).getAppBarLayoutView();
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
layoutParams.setBehavior(new AppBarLayoutNoEmptyScrollBehavior(recyclerView, getResources().getDimensionPixelSize(R.dimen.control_bar_height)));
1
Sergey Trukhachev

Ce n'est pas un bug, tous les événements d'un groupe de vues sont gérés de cette façon. Parce que votre recyclerview est un enfant de coordinatorLayout, chaque fois que l'événement est généré, il est d'abord vérifié s'il est parent et si ce parent n'est pas intéressé, il est transmis à l'enfant . Voir google documentation .

1
Sulabh Deep Puri

Quelque chose comme ceci dans une sous-classe LayoutManager semble entraîner le comportement souhaité:

@Override
public boolean canScrollVertically() {
    int firstCompletelyVisibleItemPosition = findFirstCompletelyVisibleItemPosition();
    if (firstCompletelyVisibleItemPosition == RecyclerView.NO_POSITION) return false;

    int lastCompletelyVisibleItemPosition = findLastCompletelyVisibleItemPosition();
    if (lastCompletelyVisibleItemPosition == RecyclerView.NO_POSITION) return false;

    if (firstCompletelyVisibleItemPosition == 0 &&
            lastCompletelyVisibleItemPosition == getItemCount() - 1)
        return false;

    return super.canScrollVertically();
}

La documentation de canScrollVertically() indique:

/**
 * Query if vertical scrolling is currently supported. The default implementation
 * returns false.
 *
 * @return True if this LayoutManager can scroll the current contents vertically
 */

Notez le libellé de "peut faire défiler le contenu actuel verticalement", ce qui implique, je crois, que l'état actuel doit être reflété par la valeur de retour.

Cependant, aucune des sous-classes LayoutManager fournies par la bibliothèque v7 recyclerview (23.1.1) n’est utilisée, ce qui me laisse quelque peu hésitant s’il s’agit d’une solution correcte; cela pourrait provoquer des effets indésirables dans des situations autres que celle présentée dans cette question.

1
joelpet

Je vous ai suggéré d’essayer this sample qui permet de prendre en charge les éléments de la bibliothèque de conception.

cela ressemble à votre mise en page dans l'exemple.

<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/main_content"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <Android.support.design.widget.AppBarLayout
        Android:id="@+id/appbar"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <Android.support.v7.widget.Toolbar
            Android:id="@+id/toolbar"
            Android:layout_width="match_parent"
            Android:layout_height="?attr/actionBarSize"
            Android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways" />

        <Android.support.design.widget.TabLayout
            Android:id="@+id/tabs"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content" />

    </Android.support.design.widget.AppBarLayout>

    <Android.support.v4.view.ViewPager
        Android:id="@+id/viewpager"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</Android.support.design.widget.CoordinatorLayout>

Merci, j'ai créé une classe personnalisée de RecyclerView mais la clé utilise toujours setNestedScrollingEnabled(). Cela a bien fonctionné de mon côté.

public class RecyclerViewCustom extends RecyclerView implements ViewTreeObserver.OnGlobalLayoutListener
{
    public RecyclerViewCustom(Context context)
    {
        super(context);
    }

    public RecyclerViewCustom(Context context, @Nullable AttributeSet attrs)
    {
        super(context, attrs);
    }

    public RecyclerViewCustom(Context context, @Nullable AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    /**
     *  This supports scrolling when using RecyclerView with AppbarLayout
     *  Basically RecyclerView should not be scrollable when there's no data or the last item is visible
     *
     *  Call this method after Adapter#updateData() get called
     */
    public void addOnGlobalLayoutListener()
    {
        this.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    public void onGlobalLayout()
    {
        // If the last item is visible or there's no data, the RecyclerView should not be scrollable
        RecyclerView.LayoutManager layoutManager = getLayoutManager();
        final RecyclerView.Adapter adapter = getAdapter();
        if (adapter == null || adapter.getItemCount() <= 0 || layoutManager == null)
        {
            setNestedScrollingEnabled(false);
        }
        else
        {
            int lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
            boolean isLastItemVisible = lastVisibleItemPosition == adapter.getItemCount() - 1;
            setNestedScrollingEnabled(!isLastItemVisible);
        }

        unregisterGlobalLayoutListener();
    }

    private void unregisterGlobalLayoutListener()
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
        else
        {
            getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }
}
0
Peter

Je voudrais ajouter un peu à la réponse de user3623735 . Le code suivant est absolument incorrect.

// Find out if RecyclerView are scrollable, delay required
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if (rv.canScrollVertically(DOWN) || rv.canScrollVertically(UP)) {
                controller.enableScroll();
            } else {
                controller.disableScroll();
            }
        }
    }, 100);

Et même lorsque cela fonctionne, cela ne couvre pas tous les cas. Il n’ya absolument aucune garantie qu’une donnée sera affichée dans 100 ms, et les données peuvent étirer la hauteur de la vue en cours de travail, pas seulement dans la méthode onCreateView. C'est pourquoi vous devez utiliser le code suivant et suivre les changements de hauteur de vue:

view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            if(bottom != oldBottom)
            {
                mActivity.setScrollEnabled(view.canScrollVertically(0) || view.canScrollVertically(1));
            }
        }
    });

De plus, il n'est pas nécessaire de créer deux méthodes séparées pour contrôler l'état du défilement, vous devez utiliser une méthode setScrollEnabled:

public void setScrollEnabled(boolean enabled) {
    final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
            mToolbar.getLayoutParams();

    params.setScrollFlags(enabled ?
            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS : 0);

    mToolbar.setLayoutParams(params);
}
0
Manunich