Looking for android Answers? Try Ask4KnowledgeBase
Looking for android Keywords? Try Ask4Keywords

Android BottomSheetBehavior comme Google maps


Exemple

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 ]