Recherche…


Introduction

Une feuille inférieure est une feuille qui glisse du bord inférieur de l'écran.

Remarques

Les feuilles du bas glissent du bas de l'écran pour révéler plus de contenu.
Ils ont été ajoutés à la bibliothèque de support Android en version v23.2.0.

BottomSheetBehavior comme Google maps

2.1.x

Cet exemple dépend de la bibliothèque de support 23.4.0. +.

BottomSheetBehavior se caractérise par:

  1. Deux barres d'outils avec des animations qui répondent aux mouvements de la feuille du bas.
  2. Un FAB qui se cache près de la "barre d'outils modale" (celle qui apparaît lorsque vous glissez vers le haut).
  3. Une image de fond derrière la feuille de fond avec un effet de parallaxe.
  4. Un titre (TextView) dans la barre d'outils qui apparaît lorsque la feuille du bas l'atteint.
  5. La barre de notification satus peut transformer son arrière-plan en couleur transparente ou pleine.
  6. Un comportement de feuille de fond personnalisé avec un état "ancre".

Maintenant vérifions-les un par un:

Barres d'outils
Lorsque vous ouvrez cette vue dans Google Maps, vous pouvez voir une barre d’outils dans laquelle vous pouvez effectuer une recherche, la seule que je ne fais pas exactement comme Google Maps, car je voulais le faire plus générique. Quoi qu'il en soit, ToolBar trouve à l'intérieur d'un AppBarLayout et il a été masqué lorsque vous avez commencé à faire glisser la BottomSheet et il apparaît à nouveau lorsque la feuille de fond atteint l'état COLLAPSED .
Pour y parvenir, vous devez:

  • créer un Behavior et l'étendre depuis AppBarLayout.ScrollingViewBehavior
  • onDependentViewChanged méthodes layoutDependsOn et onDependentViewChanged . En faisant cela, vous écouterez les mouvements bottomSheet.
  • créer des méthodes pour masquer et afficher la barre d'outils AppBarLayout / ToolBar avec des animations.

Voici comment je l'ai fait pour la première barre d'outils ou 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();
}

Voici le fichier complet si vous en avez besoin

La deuxième barre d'outils ou barre d'outils "Modal":
Vous devez remplacer les mêmes méthodes, mais dans celle-ci vous devez prendre en compte plus de comportements:

  • afficher / masquer la barre d'outils avec des animations
  • changer la couleur de la barre d'état / fond
  • afficher / masquer le titre de la feuille de fond dans la barre d'outils
  • fermer la bottomSheet ou l'envoyer à l'état réduit

Le code pour celui-ci est un peu long, alors je vais laisser le lien

Le FAB

Ceci est un comportement personnalisé également, mais s'étend de FloatingActionButton.Behavior . Dans onDependentViewChanged vous devez regarder quand il atteint le "offSet" ou le point où vous voulez le cacher. Dans mon cas, je veux le cacher quand il est proche de la deuxième barre d'outils, alors je creuse dans le parent FAB (un CoordinatorLayout) à la recherche de AppBarLayout contenant la barre d'outils, puis j'utilise la position ToolBar comme 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;
}

Lien Complet sur le Comportement FAB personnalisé

L'image derrière la feuille de fond avec effet de parallaxe :
Comme les autres, c'est un comportement personnalisé, la seule chose "compliquée" dans celui-ci est le petit algorithme qui garde l'image ancrée dans la feuille de fond et évite que l'image ne s'effondre comme l'effet de parallaxe par défaut:

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


Le fichier complet pour l'image de fond avec effet de parallaxe

Maintenant pour la fin: le comportement de BottomSheet personnalisé
Pour réaliser les 3 étapes, vous devez d'abord comprendre que BottomSheetBehavior par défaut comporte 5 états: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN et pour le comportement de Google Maps, vous devez ajouter un état intermédiaire entre STATE_ANCHOR_POINT .
J'ai essayé de prolonger le fichier bottomSheetBehavior par défaut sans succès, alors je viens de copier tout le code collé et de modifier ce dont j'ai besoin.
Pour réaliser ce dont je parle, suivez les étapes suivantes:

  1. Créez une classe Java et étendez-la à partir de CoordinatorLayout.Behavior<V>

  2. Copiez le code de BottomSheetBehavior fichier BottomSheetBehavior par défaut vers le nouveau.

  3. Modifiez la méthode clampViewPositionVertical avec le code suivant:

    @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. Ajouter un nouvel état

    public static final int STATE_ANCHOR_POINT = X;

  5. Modifiez les méthodes suivantes: onLayoutChild , onStopNestedScroll , BottomSheetBehavior<V> from(V view) et setState (facultatif)



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



Lien vers l'ensemble du projet où vous pouvez voir tous les comportements personnalisés

Et voici à quoi ça ressemble:
[ CustomBottomSheetBehavior ]

Installation rapide

Assurez-vous que la dépendance suivante est ajoutée au fichier build.gradle de votre application sous les dépendances:

compile 'com.android.support:design:25.3.1'

Vous pouvez ensuite utiliser la feuille inférieure en utilisant ces options:

Feuilles de fond persistantes

Vous pouvez obtenir une feuille inférieure persistante associant un BottomSheetBehavior à un enfant. Vue d'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>

Ensuite, dans votre code, vous pouvez créer une référence en utilisant:

 // The View with the BottomSheetBehavior  
 View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);  
 BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);  

Vous pouvez définir l'état de votre BottomSheetBehavior en utilisant la méthode setState () :

mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);

Vous pouvez utiliser l'un de ces états:

  • STATE_COLLAPSED : cet état réduit est la valeur par défaut et ne montre qu'une partie de la disposition en bas. La hauteur peut être contrôlée avec l'attribut app:behavior_peekHeight (la valeur par défaut est 0)

  • STATE_EXPANDED : l'état complètement développé de la feuille du bas, où soit la totalité de la feuille inférieure est visible (si sa hauteur est inférieure à celle du CoordinatorLayout ), soit la totalité du CoordinatorLayout est remplie

  • STATE_HIDDEN : désactivé par défaut (et activé avec l'attribut app:behavior_hideable ), ce qui permet aux utilisateurs de glisser le bas de la feuille pour masquer complètement la feuille du bas

Si vous souhaitez recevoir des rappels de modifications d'état, vous pouvez ajouter un 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  
   }  
 });  

Feuilles de fond modales avec BottomSheetDialogFragment

Vous pouvez réaliser des feuilles de fond modales en utilisant un BottomSheetDialogFragment .

Le BottomSheetDialogFragment est une feuille de fond modale.
Ceci est une version de DialogFragment qui affiche une feuille inférieure en utilisant BottomSheetDialog au lieu d'une boîte de dialogue flottante.

Définissez simplement le fragment:

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

Ensuite, utilisez ce code pour afficher le fragment:

MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");

Ce fragment va créer un BottomSheetDialog .

Feuilles de fond modales avec BottomSheetDialog

Le BottomSheetDialog est une boîte de dialogue conçue comme une feuille inférieure

Utilisez simplement:

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

Dans ce cas, vous n'avez pas besoin d'attacher un comportement BottomSheet.

Ouvrez BottomSheet DialogFragment en mode étendu par défaut.

BottomSheet DialogFragment s'ouvre dans STATE_COLLAPSED par défaut. Qui peut être forcé d'ouvrir à STATE_EXPANDED et prendre l'écran complet du périphérique avec l'aide du modèle de code suivant.

@NonNull @Override Dialogue public 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;
}

Bien que l'animation de dialogue soit légèrement perceptible, la tâche d'ouvrir le DialogFragment en plein écran est très bonne.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow