Android
Hojas inferiores
Buscar..
Introducción
Una hoja inferior es una hoja que se desliza hacia arriba desde el borde inferior de la pantalla.
Observaciones
Las hojas inferiores se deslizan hacia arriba desde la parte inferior de la pantalla para revelar más contenido.
Se agregaron a la biblioteca de soporte de Android en la versión v23.2.0.
BottomSheetBehavior como los mapas de Google
Este ejemplo depende de la biblioteca de soporte 23.4.0. +.
BottomSheetBehavior se caracteriza por:
- Dos barras de herramientas con animaciones que responden a los movimientos de la hoja inferior.
- Un FAB que se oculta cuando está cerca de la "barra de herramientas modal" (la que aparece cuando se desliza hacia arriba).
- Una imagen de fondo detrás de la hoja inferior con algún tipo de efecto de paralaje.
- Un título (TextView) en la barra de herramientas que aparece cuando la hoja inferior lo alcanza.
- La barra de notificación satus puede convertir su fondo a transparente o a todo color.
- Un comportamiento personalizado de la hoja inferior con un estado "ancla".
Ahora vamos a revisarlos uno por uno:
Barras de herramientas
Cuando abres esa vista en Google Maps, puedes ver una barra de herramientas en la que puedes buscar, es la única que no estoy haciendo exactamente como Google Maps, porque quería hacerlo más genérico. De todos modos, la AppBarLayout
ToolBar
está dentro de un AppBarLayout
y se ocultó cuando comenzó a arrastrar la hoja inferior y aparece de nuevo cuando la hoja inferior llega al estado COLLAPSED
.
Para lograrlo necesitas:
- cree un
Behavior
yAppBarLayout.ScrollingViewBehavior
desdeAppBarLayout.ScrollingViewBehavior
- anular los métodos
layoutDependsOn
yonDependentViewChanged
. Al hacerlo escucharás movimientos de la parte inferior. - cree algunos métodos para ocultar y mostrar la barra de herramientas AppBarLayout / ToolBar.
Así es como lo hice para la primera barra de herramientas o ActionBar:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
if (mChild == null) {
initValues(child, dependency);
return false;
}
float dVerticalScroll = dependency.getY() - mPreviousY;
mPreviousY = dependency.getY();
//going up
if (dVerticalScroll <= 0 && !hidden) {
dismissAppBar(child);
return true;
}
return false;
}
private void initValues(final View child, View dependency) {
mChild = child;
mInitialY = child.getY();
BottomSheetBehaviorGoogleMapsLike bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(dependency);
bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, @BottomSheetBehaviorGoogleMapsLike.State int newState) {
if (newState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED ||
newState == BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN)
showAppBar(child);
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
}
private void dismissAppBar(View child){
hidden = true;
AppBarLayout appBarLayout = (AppBarLayout)child;
mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_shortAnimTime));
mToolbarAnimation.y(-(mChild.getHeight()+25)).start();
}
private void showAppBar(View child) {
hidden = false;
AppBarLayout appBarLayout = (AppBarLayout)child;
mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_mediumAnimTime));
mToolbarAnimation.y(mInitialY).start();
}
Aquí está el archivo completo si lo necesitas
La segunda barra de herramientas o barra de herramientas "Modal":
Debe anular los mismos métodos, pero en este tiene que cuidar más conductas:
- mostrar / ocultar la barra de herramientas con animaciones
- Cambiar color de barra de estado / fondo
- mostrar / ocultar el título de la hoja de fondo en la barra de herramientas
- cerrar la parte inferior de la hoja o enviarla al estado contraído
El código para este es un poco extenso, así que dejaré el enlace
El fab
Este también es un comportamiento personalizado, pero se extiende desde FloatingActionButton.Behavior
. En onDependentViewChanged
, debe mirar cuando llegue a "offSet" o señalar dónde desea ocultarlo. En mi caso, quiero ocultarlo cuando está cerca de la segunda barra de herramientas, así que entro en el FAB padre (un CoordinatorLayout) buscando el AppBarLayout que contiene la ToolBar, luego uso la posición ToolBar como OffSet
:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
if (offset == 0)
setOffsetValue(parent);
if (dependency.getY() <=0)
return false;
if (child.getY() <= (offset + child.getHeight()) && child.getVisibility() == View.VISIBLE)
child.hide();
else if (child.getY() > offset && child.getVisibility() != View.VISIBLE)
child.show();
return false;
}
Completa enlace de comportamiento FAB personalizado
La imagen detrás del BottomSheet con efecto de paralaje :
Al igual que los demás, es un comportamiento personalizado, lo único "complicado" en este es el pequeño algoritmo que mantiene la imagen anclada en la Hoja Inferior y evita el colapso de la imagen como el efecto de paralaje predeterminado:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
if (mYmultiplier == 0) {
initValues(child, dependency);
return true;
}
float dVerticalScroll = dependency.getY() - mPreviousY;
mPreviousY = dependency.getY();
//going up
if (dVerticalScroll <= 0 && child.getY() <= 0) {
child.setY(0);
return true;
}
//going down
if (dVerticalScroll >= 0 && dependency.getY() <= mImageHeight)
return false;
child.setY( (int)(child.getY() + (dVerticalScroll * mYmultiplier) ) );
return true;
}
El archivo completo para la imagen de fondo con efecto de paralaje.
Ahora, para el final: el comportamiento personalizado de BottomSheet
Para alcanzar los 3 pasos, primero que hay que entender que por defecto BottomSheetBehavior tiene 5 estados: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN
y para el comportamiento de Google Maps es necesario agregar un estado intermedio entre colapsado y ampliado: STATE_ANCHOR_POINT
.
Intenté extender el comportamiento predeterminado de bottomSheetBehavior sin éxito, así que simplemente copié todo el código y modifiqué lo que necesito.
Para lograr lo que estoy hablando, siga los siguientes pasos:
Cree una clase de Java y extiéndala desde
CoordinatorLayout.Behavior<V>
Copie el código de
BottomSheetBehavior
archivo deBottomSheetBehavior
predeterminado a su nuevo.Modifique el método
clampViewPositionVertical
con el siguiente código:@Override public int clampViewPositionVertical(View child, int top, int dy) { return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset); } int constrain(int amount, int low, int high) { return amount < low ? low : (amount > high ? high : amount); }
Añadir un nuevo estado
public static final int STATE_ANCHOR_POINT = X;
Modifique los siguientes métodos:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
ysetState
(opcional)
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
// First let the parent lay it out
if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
if (ViewCompat.getFitsSystemWindows(parent) &&
!ViewCompat.getFitsSystemWindows(child)) {
ViewCompat.setFitsSystemWindows(child, true);
}
parent.onLayoutChild(child, layoutDirection);
}
// Offset the bottom sheet
mParentHeight = parent.getHeight();
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
//if (mState == STATE_EXPANDED) {
// ViewCompat.offsetTopAndBottom(child, mMinOffset);
//} else if (mHideable && mState == STATE_HIDDEN...
if (mState == STATE_ANCHOR_POINT) {
ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
} else if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight);
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
}
if (mViewDragHelper == null) {
mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
}
mViewRef = new WeakReference<>(child);
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
return true;
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
if (child.getTop() == mMinOffset) {
setStateInternal(STATE_EXPANDED);
return;
}
if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {
return;
}
int top;
int targetState;
if (mLastNestedScrollDy > 0) {
//top = mMinOffset;
//targetState = STATE_EXPANDED;
int currentTop = child.getTop();
if (currentTop > mAnchorPoint) {
top = mAnchorPoint;
targetState = STATE_ANCHOR_POINT;
}
else {
top = mMinOffset;
targetState = STATE_EXPANDED;
}
} else if (mHideable && shouldHide(child, getYVelocity())) {
top = mParentHeight;
targetState = STATE_HIDDEN;
} else if (mLastNestedScrollDy == 0) {
int currentTop = child.getTop();
if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
top = mMinOffset;
targetState = STATE_EXPANDED;
} else {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
} else {
//top = mMaxOffset;
//targetState = STATE_COLLAPSED;
int currentTop = child.getTop();
if (currentTop > mAnchorPoint) {
top = mMaxOffset;
targetState = STATE_COLLAPSED;
}
else {
top = mAnchorPoint;
targetState = STATE_ANCHOR_POINT;
}
}
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
} else {
setStateInternal(targetState);
}
mNestedScrolled = false;
}
public final void setState(@State int state) {
if (state == mState) {
return;
}
if (mViewRef == null) {
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
/**
* New behavior (added: state == STATE_ANCHOR_POINT ||)
*/
if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
state == STATE_ANCHOR_POINT ||
(mHideable && state == STATE_HIDDEN)) {
mState = state;
}
return;
}
V child = mViewRef.get();
if (child == null) {
return;
}
int top;
if (state == STATE_COLLAPSED) {
top = mMaxOffset;
} else if (state == STATE_ANCHOR_POINT) {
top = mAnchorPoint;
} else if (state == STATE_EXPANDED) {
top = mMinOffset;
} else if (mHideable && state == STATE_HIDDEN) {
top = mParentHeight;
} else {
throw new IllegalArgumentException("Illegal state argument: " + state);
}
setStateInternal(STATE_SETTLING);
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
}
}
public static <V extends View> BottomSheetBehaviorGoogleMapsLike<V> from(V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
}
CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
.getBehavior();
if (!(behavior instanceof BottomSheetBehaviorGoogleMapsLike)) {
throw new IllegalArgumentException(
"The view is not associated with BottomSheetBehaviorGoogleMapsLike");
}
return (BottomSheetBehaviorGoogleMapsLike<V>) behavior;
}
Enlace a todo el proyecto donde se pueden ver todos los comportamientos personalizados.
Y aquí es como se ve:
El ]
Configuración rápida
Asegúrese de que la siguiente dependencia se agregue al archivo build.gradle de su aplicación en las dependencias:
compile 'com.android.support:design:25.3.1'
Luego puedes usar la hoja inferior usando estas opciones:
-
BottomSheetBehavior
para ser utilizado conCoordinatorLayout
-
BottomSheetDialog
que es un diálogo con un comportamiento de la hoja inferior -
BottomSheetDialogFragment
que es una extensión deDialogFragment
, que crea unBottomSheetDialog
lugar de un diálogo estándar.
Hojas inferiores persistentes
Puede lograr una Hoja de abajo persistente adjuntando una BottomSheetBehavior
de BottomSheetBehavior
a una vista de niño de un CoordinatorLayout
:
<android.support.design.widget.CoordinatorLayout >
<!-- ..... -->
<LinearLayout
android:id="@+id/bottom_sheet"
android:elevation="4dp"
android:minHeight="120dp"
app:behavior_peekHeight="120dp"
...
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<!-- ..... -->
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Luego, en tu código puedes crear una referencia usando:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
Puede establecer el estado de su BottomSheetBehavior utilizando el método setState () :
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
Puedes usar uno de estos estados:
STATE_COLLAPSED
: este estado colapsado es el predeterminado y muestra solo una parte del diseño en la parte inferior. La altura se puede controlar con el atributoapp:behavior_peekHeight
(predeterminado en 0)STATE_EXPANDED
: el estado totalmente expandido de la hoja inferior, donde puede verse toda la hoja inferior (si su altura es menor que la que contiene elCoordinatorLayout
) o la totalidad delCoordinatorLayout
se llenaSTATE_HIDDEN
: deshabilitado de forma predeterminada (y habilitado con laapp:behavior_hideable
atributo deapp:behavior_hideable
ocultable), lo que permite a los usuarios deslizarse hacia abajo en la hoja inferior para ocultar completamente la hoja inferior
Si desea recibir devoluciones de llamadas de cambios de estado, puede agregar una BottomSheetCallback
:
mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events
}
});
Hojas inferiores modales con BottomSheetDialogFragment
Puede realizar hojas BottomSheetDialogFragment
modales utilizando un BottomSheetDialogFragment
.
El BottomSheetDialogFragment
es una hoja de fondo modal.
Esta es una versión de DialogFragment
que muestra una hoja inferior usando BottomSheetDialog
lugar de un diálogo flotante.
Solo define el fragmento:
public class MyBottomSheetDialogFragment extends BottomSheetDialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.my_fragment_bottom_sheet, container);
}
}
Luego usa este código para mostrar el fragmento:
MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");
Este fragmento creará un BottomSheetDialog
.
Hojas de fondo modales con BottomSheetDialog
El BottomSheetDialog
es un diálogo con el estilo de una hoja inferior
Solo usa:
//Create a new BottomSheetDialog
BottomSheetDialog dialog = new BottomSheetDialog(context);
//Inflate the layout R.layout.my_dialog_layout
dialog.setContentView(R.layout.my_dialog_layout);
//Show the dialog
dialog.show();
En este caso, no es necesario adjuntar un comportamiento BottomSheet.
Abra BottomSheet DialogFragment en modo Expandido de forma predeterminada.
BottomSheet DialogFragment se abre en STATE_COLLAPSED
de forma predeterminada. Se puede forzar para abrir STATE_EXPANDED
y ocupar la pantalla completa del dispositivo con la ayuda de la siguiente plantilla de código.
@NonNull @Override public Dialog onCreateDialog (Bundle savedInstanceState) {
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
BottomSheetDialog d = (BottomSheetDialog) dialog;
FrameLayout bottomSheet = (FrameLayout) d.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
// Do something with your dialog like setContentView() or whatever
return dialog;
}
Aunque la animación del diálogo es ligeramente perceptible, pero la tarea de abrir DialogFragment en pantalla completa es muy buena.