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

Est-il vraiment prévu que la barre d’outils dans un AppBarLayout soit défilable alors que le conteneur principal avec le “appbar_scrolling_view_behavior” ne contient pas assez de contenu pour réellement faire défiler?

Ce que j’ai testé jusqu’à présent:
Lorsque j’utilise un atsortingbut NestedScrollView (avec l’atsortingbut “wrap_content”) en tant que conteneur principal et un object TextView en tant qu’enfant, AppBarLayout fonctionne correctement et ne défile pas.

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

Voici mon code de mise en page:

      

Avec l’effet suivant que la barre d’outils est défilante bien que ce ne soit pas 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 () du RecyclerView.
Néanmoins, cela ressemble plus à un bug que je le souhaite. Des opinions? :RÉ

EDIT # 1:

Pour les personnes susceptibles d’être intéressées par ma solution actuelle, j’ai dû mettre la logique setNestedScrollingEnabled () dans la méthode postDelayed () d’un gestionnaire avec un délai de 5 ms en raison du LayoutManager qui renvoyait toujours -1 lors de l’appel des méthodes pour le 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 qu’un changement de contenu de RecyclerView se produit.

 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 (involontaire) ait été corrigé dans la version 23.3.0 de la bibliothèque de support (ou même plus tôt). Ainsi, il n’y a plus besoin de solutions de contournement!

Edit 2:

La seule façon de s’assurer que la barre d’outils ne peut pas être défilée apparaît lorsque RecyclerView n’est pas défilable. La méthode consiste à définir setScrollFlags par programmation, ce qui nécessite de vérifier si RecyclerView est déroulable. 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:

           

Fragment d’essai:

 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 SimpleSsortingngRecyclerViewAdapter adapter = new SimpleSsortingngRecyclerViewAdapter( getActivity(), // Test ToolBar scroll getRandomList(/* with enough items to scroll */) // Test ToolBar pin getRandomList(/* with only 3 items*/) ); recyclerView.setAdapter(adapter); } } 

Sources:

  • Modifier les drapeaux de défilement par programmation
  • Code original par Chris Banes
  • Besoin d’un postDelayed pour s’assurer que les enfants RecyclerView sont prêts pour les calculs

Modifier:

Vous devriez CollapsingToolbarLayout pour contrôler le comportement.

L’ajout d’une barre d’outils directement à un AppBarLayout vous donne access aux indicateurs de défilement enterAlwaysCollapsed et exitUntilCollapsed, mais pas au contrôle détaillé de la réaction des différents éléments à la réduction. L’installation utilise l’application CollapsingToolbarLayout: layout_collapseMode = “pin” pour s’assurer que la barre d’outils elle-même rest épinglée en haut de l’écran lorsque la vue est réduite. http://android-developers.blogspot.com.tr/2015/05/android-design-support-library.html

    

Ajouter

 app:layout_collapseMode="pin" 

à votre barre d’outils en xml.

   

Donc, bon crédit, cette réponse l’a presque résolu pour moi https://stackoverflow.com/a/32923226/5050087 . Mais comme il ne montrait pas la barre d’outils quand vous aviez une recyclerview à défilement et que son dernier élément était visible (la barre d’outils n’était pas visible sur le premier défilement), j’ai décidé de le modifier et de l’adapter pour une implémentation plus dynamic et dynamic. adaptateurs

Tout d’abord, vous devez créer un comportement de présentation personnalisé pour vous appbar:

 public class ToolbarBehavior extends AppBarLayout.Behavior{ private boolean scrollableRecyclerView = false; private int count; public ToolbarBehavior() { } public ToolbarBehavior(Context context, AtsortingbuteSet 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 appbar dans votre fichier de mise en page:

  

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

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

Ce n’est pas un bogue, tous les événements d’un viewGroup sont traités de cette manière. Étant donné que votre recyclerview est un enfant de coordinatorLayout, chaque fois que l’événement est généré, il est d’abord vérifié pour le parent et si le parent n’est pas intéressé uniquement, il est transmis à l’enfant. Voir la documentation google

Quelque chose comme ceci dans une sous-classe de LayoutManager semble avoir pour résultat le comportement désiré:

 @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 que le libellé de “peut faire défiler le contenu actuel verticalement”, ce qui implique que l’état actuel devrait être reflété par la valeur de retour.

Cependant, cela ne se fait pas avec les sous-classes de LayoutManager fournies par la bibliothèque v7 recyclerview (23.1.1) , ce qui me rend quelque peu hésitant quant à savoir s’il s’agit d’une solution correcte. cela pourrait causer des effets indésirables dans d’autres situations que celle dont il est question dans cette question.

Je l’ai implémenté en utilisant ma propre classe Behavior qui pourrait ê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))); 

Je vous ai suggéré d’essayer cet exemple pour le support des éléments de la bibliothèque.

ceci une disposition comme votre mise en page dans l’exemple.

        

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 AtsortingbuteSet attrs) { super(context, attrs); } public RecyclerViewCustom(Context context, @Nullable AtsortingbuteSet 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); } } } 

Supprimez l’indicateur de scroll dans votre Toolbar , en laissant uniquement l’indicateur enterAlways et obtenez l’effet souhaité. Pour être complet, votre mise en page devrait ressembler à: