Android
Fogli inferiori
Ricerca…
introduzione
Un foglio inferiore è un foglio che scorre dal bordo inferiore dello schermo.
Osservazioni
I fogli in basso scorrono verso l'alto dalla parte inferiore dello schermo per rivelare più contenuti.
Sono stati aggiunti alla libreria di supporto Android nella versione v23.2.0.
BottomSheetBehavior come le mappe di Google
Questo esempio dipende dalla libreria di supporto 23.4.0. +.
BottomSheetBehavior è caratterizzato da:
- Due barre degli strumenti con animazioni che rispondono ai movimenti del foglio inferiore.
- Un FAB che si nasconde quando è vicino alla "barra degli strumenti modale" (quella che appare quando si fa scorrere verso l'alto).
- Un'immagine sullo sfondo dietro il foglio inferiore con una sorta di effetto di parallasse.
- Un titolo (TextView) nella barra degli strumenti che appare quando il foglio di base lo raggiunge.
- La barra di notifica satus può trasformare il suo sfondo in trasparente o a colori.
- Un comportamento del foglio di fondo personalizzato con uno stato di "ancora".
Ora controlliamoli uno per uno:
ToolBars
Quando apri la visualizzazione in Google Maps, puoi vedere una barra degli strumenti in cui puoi effettuare ricerche, è l'unica che non sto facendo esattamente come Google Maps, perché volevo farlo più generico. In ogni caso che ToolBar
è all'interno di un AppBarLayout
ed ottenuto nascosto quando si inizia a trascinare il BottomSheet e appare di nuovo quando il BottomSheet raggiungere il COLLAPSED
Stato.
Per realizzarlo è necessario:
- creare un
Behavior
ed estenderlo daAppBarLayout.ScrollingViewBehavior
- sovrascrivere i metodi
layoutDependsOn
eonDependentViewChanged
. Facendolo ascolterai i movimenti del bottomSheet. - creare alcuni metodi per nascondere e mostrare AppBarLayout / ToolBar con animazioni.
Ecco come l'ho fatto per la prima barra degli strumenti 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();
}
Ecco il file completo se ne hai bisogno
La seconda barra degli strumenti o la barra degli strumenti "Modale":
Devi sovrascrivere gli stessi metodi, ma in questo devi occuparti di più comportamenti:
- mostra / nascondi la barra degli strumenti con animazioni
- cambia colore / sfondo della barra di stato
- mostra / nasconde il titolo BottomSheet nella barra degli strumenti
- chiudi il bottomSheet o invialo allo stato compresso
Il codice per questo è un po 'esteso, quindi lascerò il link
Il FAB
Anche questo è un comportamento personalizzato, ma si estende da FloatingActionButton.Behavior
. In onDependentViewChanged
devi guardare quando raggiunge "offSet" o puntare dove vuoi nasconderlo. Nel mio caso, voglio nasconderlo quando è vicino alla seconda barra degli strumenti, quindi scaverò nel genitore FAB (un CoordinatorLayout) cercando l'AppBarLayout che contiene la barra degli strumenti, quindi uso la posizione di ToolBar come 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 il collegamento Comportamento FAB personalizzato
L'immagine dietro il BottomSheet con effetto di parallasse :
Come gli altri, è un comportamento personalizzato, l'unica cosa "complicata" in questo è il piccolo algoritmo che mantiene l'immagine ancorata al BottomSheet ed evita il collasso dell'immagine come l'effetto di parallasse predefinito:
@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;
}
Il file completo per l'immagine di sfondo con effetto di parallasse
Ora per la fine: il comportamento del documento di base personalizzato
Per raggiungere i 3 passaggi, devi prima capire che il predefinito BottomSheetBehavior ha 5 stati: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN
e per il comportamento di Google Maps devi aggiungere uno stato intermedio tra collassato ed espanso: STATE_ANCHOR_POINT
.
Ho provato ad estendere il bottomSheetBehavior predefinito senza successo, quindi ho appena copiato tutto il codice e ho modificato ciò di cui ho bisogno.
Per ottenere ciò di cui sto parlando, segui i seguenti passi:
Creare una classe Java ed estenderla da
CoordinatorLayout.Behavior<V>
Copia il codice incolla dal file
BottomSheetBehavior
predefinito a quello nuovo.Modificare il metodo
clampViewPositionVertical
con il seguente codice:@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); }
Aggiungi un nuovo stato
public static final int STATE_ANCHOR_POINT = X;
Modificare i seguenti metodi:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
esetState
(opzionale)
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;
}
Collegamento all'intero progetto in cui è possibile visualizzare tutti i comportamenti personalizzati
Ed ecco come appare:
[ ]
Configurazione rapida
Assicurati che la seguente dipendenza venga aggiunta al file build.gradle dell'app in dipendenze:
compile 'com.android.support:design:25.3.1'
Quindi puoi utilizzare il foglio in basso usando queste opzioni:
-
BottomSheetBehavior
da utilizzare conCoordinatorLayout
-
BottomSheetDialog
che è una finestra di dialogo con un comportamento del foglio di fondo -
BottomSheetDialogFragment
che è un'estensione diDialogFragment
, che crea unBottomSheetDialog
anziché una finestra di dialogo standard.
Fogli inferiori persistenti
È possibile ottenere un foglio inferiore persistente allegando un BottomSheetBehavior
foglio inferiore a un bambino Vista di un CoordinatorLayout
BottomSheetBehavior
:
<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>
Quindi nel tuo codice puoi creare un riferimento usando:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
Puoi impostare lo stato di BottomSheetBehavior usando il metodo setState () :
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
Puoi utilizzare uno di questi stati:
STATE_COLLAPSED
: questo stato compresso è l'impostazione predefinita e mostra solo una parte del layout lungo il fondo. L'altezza può essere controllata con l'app:behavior_peekHeight
attributoapp:behavior_peekHeight
(predefinito su 0)STATE_EXPANDED
: lo stato completamente espanso del foglio inferiore, dove è visibile l'intero foglio inferiore (se la sua altezza è inferiore alCoordinatorLayout
contenente) o l'interoCoordinatorLayout
è pienoSTATE_HIDDEN
: disabilitato per impostazione predefinita (e abilitato con l'app:behavior_hideable
attributoapp:behavior_hideable
), abilitando questo consente agli utenti di scorrere verso il basso sul foglio inferiore per nascondere completamente il foglio inferiore
Se desideri ricevere i callback delle modifiche di stato, puoi aggiungere 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
}
});
Fogli di fondo modali con BottomSheetDialogFragment
È possibile realizzare un foglio inferiore modale utilizzando un oggetto BottomSheetDialogFragment
.
BottomSheetDialogFragment
è un foglio inferiore modale.
Questa è una versione di DialogFragment
che mostra un foglio inferiore utilizzando BottomSheetDialog
anziché una finestra di dialogo mobile.
Basta definire il frammento:
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);
}
}
Quindi utilizzare questo codice per mostrare il frammento:
MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");
Questo frammento creerà un BottomSheetDialog
.
Fogli di fondo modali con BottomSheetDialog
BottomSheetDialog
è una finestra di dialogo disegnata come un foglio inferiore
Basta usare:
//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();
In questo caso non è necessario allegare un comportamento BottomSheet.
Apri il parametro BottomFragment BottomSheet in modalità estesa per impostazione predefinita.
BottomSheet DialogFragment si apre in STATE_COLLAPSED
per impostazione predefinita. Quale può essere forzato per aprirsi a STATE_EXPANDED
e occupare lo schermo di dispositivo completo con l'aiuto del seguente modello di codice.
@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;
}
Sebbene l'animazione delle finestre di dialogo sia leggermente visibile, l'operazione di apertura di DialogFragment a schermo intero è molto buona.