layout_collapsemode example custom coordinatorlayout coordinator appbarlayout android android-xml android-coordinatorlayout android-appbarlayout

android - example - layout_collapsemode



La barra de herramientas en AppBarLayout es desplazable aunque RecyclerView no tiene suficiente contenido para desplazarse (9)

Algo así en una subclase de LayoutManager parece dar como resultado el comportamiento deseado:

@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 documentación de canScrollVertically() dice:

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

Observe que la redacción de "puede desplazar el contenido actual verticalmente", lo que creo implica que el estado actual debe reflejarse en el valor de retorno.

Sin embargo, eso no lo hace ninguna de las subclases de LayoutManager proporcionadas a través de la biblioteca v7 recyclerview (23.1.1) , lo que me hace dudar un poco si es una solución correcta; puede causar efectos no deseados en otras situaciones distintas a la discutida en esta pregunta.

¿Realmente se pretende que la barra de herramientas en un AppBarLayout sea desplazable aunque el contenedor principal con "appbar_scrolling_view_behavior" no tiene suficiente contenido para desplazarse realmente?

Lo que he probado hasta ahora:
Cuando uso un NestedScrollView (con el atributo "wrap_content") como contenedor principal y un TextView como secundario, AppBarLayout funciona correctamente y no se desplaza.

Sin embargo, cuando uso un RecyclerView con solo unas pocas entradas y el atributo "wrap_content" (para que no haya necesidad de desplazarse), la barra de herramientas en AppBarLayout se puede desplazar aunque RecyclerView nunca reciba un evento de desplazamiento (probado con un OnScrollChangeListener )

Aquí está mi código de diseño:

<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>

Con el siguiente efecto, la barra de herramientas es desplazable aunque no es necesaria:

También he encontrado una manera de lidiar con esto comprobando si todos los elementos de RecyclerView son visibles y usando el método setNestedScrollingEnabled () de RecyclerView.
Sin embargo, parece más un error según lo previsto para mí. Alguna opinión? :RE

EDITAR # 1:

Para las personas que podrían estar interesadas en mi solución actual, tuve que poner la lógica setNestedScrollingEnabled () en el método postDelayed () de un controlador con 5 ms de retraso debido al LayoutManager que siempre devolvía -1 cuando llamaba a los métodos para averiguar si el primer y el último elemento están visibles.
Uso este código en el método onStart () (después de que mi RecyclerView ha sido inicializado) y cada vez que ocurre un cambio de contenido de 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);

EDITAR # 2:

Acabo de jugar con una nueva aplicación y parece que este comportamiento (no intencionado) se ha solucionado en la biblioteca de soporte versión 23.3.0 (o incluso anterior). Por lo tanto, ya no hay necesidad de soluciones.


En su Toolbar elimine la bandera de scroll , dejando solo la bandera enterAlways y debería obtener el efecto deseado. Para completar, su diseño debería verse así:

<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="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>


Entonces, crédito correcto, esta respuesta casi lo resolvió para mí https://.com/a/32923226/5050087 . Pero como no mostraba la barra de herramientas cuando realmente tenía una vista de reciclable desplazable y su último elemento era visible (no mostraría la barra de herramientas en el primer desplazamiento hacia arriba), decidí modificarla y adaptarla para una implementación más fácil y dinámica adaptadores

Primero, debe crear un comportamiento de diseño personalizado para su barra de aplicaciones:

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; } }

Luego, solo necesita definir este comportamiento para su barra de aplicaciones en su archivo de diseño:

<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" >

No lo he probado para la rotación de la pantalla, así que avíseme si funciona así. Supongo que debería funcionar ya que no creo que la variable de conteo se guarde cuando ocurre la rotación, pero avíseme si no es así.

Esta fue la implementación más fácil y limpia para mí, disfrútala.


Gracias, creé una clase personalizada de RecyclerView pero la clave todavía usa setNestedScrollingEnabled() . Funcionó bien de mi lado.

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); } } }


Lo implementé usando mi propia clase de comportamiento que podría estar asociada a 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; }

}

Y a continuación se muestra el código de cómo configurar este comportamiento:

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)));


Me gustaría agregar un poco a la respuesta del usuario3623735 . El siguiente código es absolutamente incorrecto.

// 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);

E incluso cuando funciona, no cubre todos los casos. No hay absolutamente ninguna garantía de que los datos se mostrarán en 100 ms, y los datos pueden estirar la altura de la vista en el proceso de trabajar con ella, no solo en el método onCreateView. Es por eso que debe usar el siguiente código y realizar un seguimiento de los cambios en la altura de la vista:

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)); } } });

Además, no es necesario crear dos métodos separados para controlar el estado de desplazamiento, debe usar un método 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); }


No es un error, todos los eventos en un viewGroup se manejan de esta manera. Debido a que su vista de reciclado es un elemento secundario de coordinatorLayout, cada vez que se genera el evento, primero se verifica para el elemento primario y, si el elemento principal no está interesado, se pasa al elemento secundario. Ver documentation google


Te sugerí que probaras esta muestra para apoyar el diseño de elementos de la biblioteca.

Este es un diseño como su diseño en la muestra.

<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>


Edición 2:

Resulta que la única forma de garantizar que la barra de herramientas no se pueda desplazar cuando RecyclerView no se puede desplazar es establecer setScrollFlags mediante programación, lo que requiere verificar si RecyclerView es desplazable. Esta verificación debe realizarse cada vez que se modifica el adaptador.

Interfaz para comunicarse con la Actividad:

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

Actividad principal:

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>

Fragmento de prueba:

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); } }

Fuentes:

Editar:

Debe CollapsingToolbarLayout para controlar el comportamiento.

Agregar una barra de herramientas directamente a un AppBarLayout le da acceso a las banderas de desplazamiento enterAlwaysCollapsed y exitUntilCollapsed, pero no al control detallado sobre cómo reaccionan los diferentes elementos al colapso. [...] la instalación utiliza la aplicación CollapsingToolbarLayout: layout_collapseMode = "pin" para garantizar que la barra de herramientas permanezca anclada en la parte superior de la pantalla mientras la vista se contrae. 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>

Añadir

app:layout_collapseMode="pin"

a su barra de herramientas 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" />