Android
Feuilles de fond
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
Cet exemple dépend de la bibliothèque de support 23.4.0. +.
BottomSheetBehavior se caractérise par:
- Deux barres d'outils avec des animations qui répondent aux mouvements de la feuille du bas.
- Un FAB qui se cache près de la "barre d'outils modale" (celle qui apparaît lorsque vous glissez vers le haut).
- Une image de fond derrière la feuille de fond avec un effet de parallaxe.
- Un titre (TextView) dans la barre d'outils qui apparaît lorsque la feuille du bas l'atteint.
- La barre de notification satus peut transformer son arrière-plan en couleur transparente ou pleine.
- 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 depuisAppBarLayout.ScrollingViewBehavior
-
onDependentViewChanged
méthodeslayoutDependsOn
etonDependentViewChanged
. 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:
Créez une classe Java et étendez-la à partir de
CoordinatorLayout.Behavior<V>
Copiez le code de
BottomSheetBehavior
fichierBottomSheetBehavior
par défaut vers le nouveau.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); }
Ajouter un nouvel état
public static final int STATE_ANCHOR_POINT = X;
Modifiez les méthodes suivantes:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
etsetState
(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:
[ ]
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:
-
BottomSheetBehavior
à utiliser avecCoordinatorLayout
-
BottomSheetDialog
qui est un dialogue avec un comportement de feuille inférieure -
BottomSheetDialogFragment
qui est une extension deDialogFragment
, qui crée unBottomSheetDialog
au lieu d'une boîte de dialogue standard.
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'attributapp: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 duCoordinatorLayout
), soit la totalité duCoordinatorLayout
est remplieSTATE_HIDDEN
: désactivé par défaut (et activé avec l'attributapp: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.