Android
Dolne arkusze
Szukaj…
Wprowadzenie
Dolny arkusz to arkusz, który wysuwa się z dolnej krawędzi ekranu.
Uwagi
Dolne arkusze przesuwają się w górę od dołu ekranu, aby wyświetlić więcej treści.
Zostały one dodane do biblioteki obsługi Androida w wersji v23.2.0.
BottomSheetBehavior jak mapy Google
Ten przykład zależy od Biblioteki pomocy technicznej 23.4.0. +.
BottomSheetBehavior charakteryzuje się:
- Dwa paski narzędzi z animacjami reagującymi na ruchy dolnego arkusza.
- FAB, który ukrywa się, gdy znajduje się w pobliżu „modalnego paska narzędzi” (ten, który pojawia się podczas zsuwania się).
- Obraz w tle za dolnym arkuszem z pewnym efektem paralaksy.
- Tytuł (TextView) na pasku narzędzi, który pojawia się, gdy osiągnie go dolny arkusz.
- Pasek powiadomień może zmienić tło na przezroczyste lub pełny kolor.
- Niestandardowe zachowanie dolnej kartki ze stanem „zakotwiczenia”.
Teraz sprawdźmy je jeden po drugim:
Paski narzędzi
Gdy otworzysz ten widok w Mapach Google, zobaczysz pasek narzędzi, w którym możesz wyszukiwać, jest to jedyny taki, którego nie robię dokładnie tak jak Mapy Google, ponieważ chciałem to zrobić bardziej ogólnie. W każdym razie, że ToolBar
jest wewnątrz AppBarLayout
i został ukryty po uruchomieniu przeciąganie BottomSheet i pojawia się ponownie, gdy BottomSheet osiągnąć COLLAPSED
stan.
Aby to osiągnąć, musisz:
- utwórz
Behavior
i rozszerz je zAppBarLayout.ScrollingViewBehavior
- override
layoutDependsOn
ionDependentViewChanged
metody. Robiąc to, będziesz nasłuchiwał ruchów bottomSheet. - utwórz metody ukrywania i odkrywania AppBarLayout / ToolBar z animacjami.
Oto jak to zrobiłem dla pierwszego paska narzędzi lub 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();
}
Oto kompletny plik, jeśli go potrzebujesz
Drugi pasek narzędzi lub pasek narzędzi „Modalny”:
Musisz zastąpić te same metody, ale w tej musisz zadbać o więcej zachowań:
- pokaż / ukryj pasek narzędzi z animacjami
- zmień kolor / tło paska stanu
- pokaż / ukryj tytuł BottomSheet na pasku narzędzi
- zamknij bottomSheet lub wyślij go do stanu zwiniętego
Kod tego jest trochę obszerny, więc dam link
FAB
Jest to również zachowanie niestandardowe, ale rozciąga się od FloatingActionButton.Behavior
. W onDependentViewChanged
musisz spojrzeć, kiedy osiągnie „offSet” lub wskaże miejsce, w którym chcesz go ukryć. W moim przypadku chcę go ukryć, gdy znajduje się w pobliżu drugiego paska narzędzi, więc zagłębiam się w element nadrzędny FAB (CoordinatorLayout) szukając AppBarLayout, który zawiera ToolBar, a następnie używam pozycji ToolBar, takiej jak 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;
}
Uzupełnij link Niestandardowe działanie FAB
Obraz za BottomSheet z efektem paralaksy :
Podobnie jak inne, jest to zachowanie niestandardowe, jedyną „skomplikowaną” rzeczą w tym przypadku jest mały algorytm, który utrzymuje obraz zakotwiczony w BottomSheet i zapobiega zawaleniu się obrazu, jak domyślny efekt paralaksy:
@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;
}
Kompletny plik obrazu tła z efektem paralaksy
Teraz koniec: niestandardowe zachowanie BottomSheet
Aby osiągnąć 3 kroki, najpierw musisz zrozumieć, że domyślne zachowanie BottomSheetBehavior ma 5 stanów: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN
a dla zachowania w Mapach Google musisz dodać stan środkowy między zwiniętym a rozwiniętym: STATE_ANCHOR_POINT
.
Próbowałem rozszerzyć domyślny bottomSheetBehavior bez powodzenia, więc po prostu skopiowałem cały kod i zmodyfikowałem to, czego potrzebuję.
Aby osiągnąć to, o czym mówię, wykonaj następujące kroki:
Utwórz klasę Java i rozszerz ją z
CoordinatorLayout.Behavior<V>
Skopiuj kod wklej z domyślnego pliku
BottomSheetBehavior
do nowego.Zmodyfikuj metodę
clampViewPositionVertical
za pomocą następującego kodu:@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); }
Dodaj nowy stan
public static final int STATE_ANCHOR_POINT = X;
Zmodyfikuj kolejne metody:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
isetState
(opcjonalnie)
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;
}
Link do całego projektu, w którym można zobaczyć wszystkie niestandardowe zachowania
A oto jak to wygląda:
[ ]
Szybki montaż
Upewnij się, że do pliku build.gradle aplikacji w zależnościach dodano następującą zależność:
compile 'com.android.support:design:25.3.1'
Następnie możesz użyć dolnego arkusza, używając następujących opcji:
-
BottomSheetBehavior
do użytku zCoordinatorLayout
-
BottomSheetDialog
który jest oknem dialogowym z zachowaniem dolnego arkusza -
BottomSheetDialogFragment
który jest rozszerzeniemDialogFragment
, który tworzyBottomSheetDialog
zamiast standardowego okna dialogowego.
Trwałe dolne arkusze
BottomSheetBehavior
arkusz dolny można uzyskać, dołączając zachowanie BottomSheetBehavior
do zachowania dziecka Widok układu 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>
Następnie w kodzie możesz utworzyć odwołanie, używając:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
Możesz ustawić stan swojego BottomSheetBehavior za pomocą metody setState () :
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
Możesz użyć jednego z tych stanów:
STATE_COLLAPSED
: ten stan zwinięcia jest domyślny i pokazuje tylko część układu na dole. Wysokość może być kontrolowana za pomocąapp:behavior_peekHeight
attribute_peekHeight atrybut (domyślnie 0)STATE_EXPANDED
: w pełni rozwinięty stan dolnego arkusza, w którym widoczny jest cały dolny arkusz (jeśli jego wysokość jest mniejsza niż zawierającyCoordinatorLayout
) lub całyCoordinatorLayout
jest wypełnionySTATE_HIDDEN
: domyślnie wyłączone (i włączone z atrybutemapp:behavior_hideable
), włączenie to pozwala użytkownikom przesunąć palcem w dół po dolnym arkuszu, aby całkowicie ukryć dolny arkusz
Jeśli chcesz otrzymywać wywołania zwrotne zmian stanu, możesz dodać 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
}
});
Modalne dolne arkusze z BottomSheetDialogFragment
Możesz zrealizować modalne dolne arkusze za pomocą BottomSheetDialogFragment
.
BottomSheetDialogFragment
jest modalnym dolnym arkuszem.
Jest to wersja DialogFragment
która pokazuje dolny arkusz za pomocą BottomSheetDialog
zamiast swobodnego okna dialogowego.
Po prostu zdefiniuj 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);
}
}
Następnie użyj tego kodu, aby pokazać fragment:
MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");
Ten fragment utworzy BottomSheetDialog
.
Modalne dolne arkusze z BottomSheetDialog
BottomSheetDialog
to okno dialogowe w stylu dolnego arkusza
Po prostu użyj:
//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();
W takim przypadku nie musisz dołączać zachowania BottomSheet.
Domyślnie otwórz okno dialogowe BottomSheet w trybie rozwiniętym.
BottomSheet DialogFragment domyślnie otwiera się w STATE_COLLAPSED
. Które można zmusić do otwarcia na STATE_EXPANDED
i podjęcia pełnego ekranu urządzenia za pomocą następującego szablonu kodu.
@NonNull @Override public Dialog onCreateDialog (pakiet zapisanyInstanceState) {
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;
}
Chociaż animacja okna dialogowego jest nieznacznie zauważalna, ale bardzo dobrze wykonuje otwieranie okna dialogowego na pełnym ekranie.