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

2.1.x

Este ejemplo depende de la biblioteca de soporte 23.4.0. +.

BottomSheetBehavior se caracteriza por:

  1. Dos barras de herramientas con animaciones que responden a los movimientos de la hoja inferior.
  2. Un FAB que se oculta cuando está cerca de la "barra de herramientas modal" (la que aparece cuando se desliza hacia arriba).
  3. Una imagen de fondo detrás de la hoja inferior con algún tipo de efecto de paralaje.
  4. Un título (TextView) en la barra de herramientas que aparece cuando la hoja inferior lo alcanza.
  5. La barra de notificación satus puede convertir su fondo a transparente o a todo color.
  6. 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 y AppBarLayout.ScrollingViewBehavior desde AppBarLayout.ScrollingViewBehavior
  • anular los métodos layoutDependsOn y onDependentViewChanged . 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:

  1. Cree una clase de Java y extiéndala desde CoordinatorLayout.Behavior<V>

  2. Copie el código de BottomSheetBehavior archivo de BottomSheetBehavior predeterminado a su nuevo.

  3. 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);
    }
    
  4. Añadir un nuevo estado

    public static final int STATE_ANCHOR_POINT = X;

  5. Modifique los siguientes métodos: onLayoutChild , onStopNestedScroll , BottomSheetBehavior<V> from(V view) y setState (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 CustomBottomSheetBehavior ]

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:

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 atributo app: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 el CoordinatorLayout ) o la totalidad del CoordinatorLayout se llena

  • STATE_HIDDEN : deshabilitado de forma predeterminada (y habilitado con la app:behavior_hideable atributo de app: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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow