Android
Нижние листы
Поиск…
Вступление
Нижний лист - это лист, который скользит вверх от нижнего края экрана.
замечания
Нижние листы скользят вверх от нижней части экрана, чтобы показать больше контента.
Они были добавлены в библиотеку поддержки Android в версии v.2.2.0.
BottomSheetBehavior как карты Google
Этот пример зависит от библиотеки поддержки 23.4.0. +.
BottomSheetBehavior характеризуется:
- Две панели инструментов с анимацией, которые реагируют на движение нижнего листа.
- FAB, который скрывается, когда он находится рядом с «модальной панелью инструментов» (тот, который появляется, когда вы сдвигаетесь вверх).
- Изображение заднего плана за нижним листом с каким-то эффектом параллакса.
- Заголовок (TextView) на панели инструментов, который появляется, когда нижний лист достигает его.
- Панель состояния уведомлений может превращать свой фон в прозрачный или полный цвет.
- Пользовательское поведение нижнего листа с состоянием «якорь».
Теперь давайте проверим их один за другим:
Панели инструментов
Когда вы открываете это представление в Картах Google, вы можете увидеть панель инструментов, где вы можете искать, это единственный, что я не делаю точно так же, как Карты Google, потому что я хотел сделать это более общим. Во всяком случае , что ToolBar
находится внутри AppBarLayout
и он получил скрытый , когда вы начинаете перетаскивание BottomSheet и появляется снова , когда BottomSheet достигают COLLAPSED
состояние.
Для этого вам необходимо:
- создать
Behavior
и расширить его изAppBarLayout.ScrollingViewBehavior
- переопределить
layoutDependsOn
иonDependentViewChanged
. Сделав это, вы будете слушать движения нижнего листа. - создайте некоторые методы для скрытия и отображения AppBarLayout / ToolBar с анимацией.
Так я сделал это для первой панели инструментов или 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();
}
Вот полный файл, если вам это нужно
Вторая панель инструментов или «Модальная» панель инструментов:
Вы должны переопределить те же методы, но в этом вам нужно позаботиться о более поведении:
- показать / скрыть панель инструментов с анимацией
- изменить цвет строки состояния / фона
- показать / скрыть заголовок BottomSheet в ToolBar
- закройте нижний лист или отправьте его в состояние свертывания
Код для этого немного обширен, поэтому я позволю ссылку
FAB
Это также пользовательское поведение, но распространяется от FloatingActionButton.Behavior
. В onDependentViewChanged
вам нужно посмотреть, когда он достигнет «offSet» или указать, где вы хотите скрыть это. В моем случае я хочу скрыть его, когда он находится рядом со второй панелью инструментов, поэтому я копаю в родительском блоке FAB (CoordinatorLayout), который ищет AppBarLayout, который содержит ToolBar, тогда я использую позицию ToolBar, такую как 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;
}
Полная пользовательская ссылка на поведение FAB
Изображение за нижним листом с эффектом параллакса :
Как и другие, это обычное поведение, единственная «сложная» вещь в этом заключается в небольшом алгоритме, который удерживает изображение привязанным к BottomSheet и избегает краха изображения, как эффект параллакса по умолчанию:
@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;
}
Полный файл для фонового изображения с эффектом параллакса
Теперь для завершения: пользовательское поведение нижнего листа
Для достижения 3-х шагов сначала вам нужно понять, что по умолчанию у BottomSheetBehavior есть 5 состояний: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN
и для поведения Google Maps вам нужно добавить среднее состояние между свернутыми и расширенными: STATE_ANCHOR_POINT
.
Я попытался расширить дефолтное значение bottomSheetBehavior без каких-либо успехов, поэтому просто скопировал весь код и изменил то, что мне нужно.
Чтобы достичь того, о чем я говорю, выполните следующие шаги:
Создайте класс Java и расширьте его из
CoordinatorLayout.Behavior<V>
Скопируйте код вставки из файла
BottomSheetBehavior
умолчанию в новый.Измените метод
clampViewPositionVertical
следующим кодом:@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); }
Добавить новое состояние
public static final int STATE_ANCHOR_POINT = X;
Измените следующие методы:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
иsetState
(необязательно)
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;
}
Ссылка на весь проект, где вы можете увидеть все пользовательские поведения
И вот как это выглядит:
[ ]
Быстрая установка
Убедитесь, что в файл build.gradle вашего приложения добавлена следующая зависимость:
compile 'com.android.support:design:25.3.1'
Затем вы можете использовать нижний лист, используя следующие параметры:
-
BottomSheetBehavior
для использования сCoordinatorLayout
-
BottomSheetDialog
который является диалогом с поведением нижнего листа -
BottomSheetDialogFragment
который является расширениемDialogFragment
, который создаетBottomSheetDialog
вместо стандартного диалога.
Стойкие нижние листы
Вы можете достичь BottomSheetBehavior
листа, содержащего BottomSheetBehavior
для ребенка. Вид 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>
Затем в вашем коде вы можете создать ссылку, используя:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
Вы можете установить состояние своего метода BottomSheetBehavior с помощью метода setState () :
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
Вы можете использовать одно из следующих состояний:
STATE_COLLAPSED
: этоSTATE_COLLAPSED
состояние по умолчанию и показывает только часть макета по дну. Высота можно контролировать с помощью атрибутаapp:behavior_peekHeight
(по умолчанию 0)STATE_EXPANDED
: полностью расширенное состояние нижнего листа, где видна вся нижняяSTATE_EXPANDED
(если ее высота меньше, чем содержащаяCoordinatorLayout
) или заполняется весьCoordinatorLayout
STATE_HIDDEN
: отключено по умолчанию (и включено с атрибутомapp:behavior_hideable
), что позволяет пользователям прокручивать нижний лист, чтобы полностью скрыть нижний лист
Если вы хотите получать обратные вызовы изменений состояния, вы можете добавить 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
}
});
Модальные нижние листы с BottomSheetDialogFragment
Вы можете реализовать модальные нижние листы, используя BottomSheetDialogFragment
.
BottomSheetDialogFragment
- это модальный нижний лист.
Это версия DialogFragment
которая показывает нижний лист с использованием BottomSheetDialog
вместо плавающего диалога.
Просто определите фрагмент:
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);
}
}
Затем используйте этот код, чтобы показать фрагмент:
MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");
Этот фрагмент создаст BottomSheetDialog
.
Модальные нижние листы с BottomSheetDialog
BottomSheetDialog
- это диалог в виде нижнего листа
Просто используйте:
//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();
В этом случае вам не нужно вводить поведение BottomSheet.
Open BottomSheet DialogFragment в расширенном режиме по умолчанию.
BottomSheet DialogFragment по умолчанию открывается в STATE_COLLAPSED
. Который может быть принудительно открыт для STATE_EXPANDED
и STATE_EXPANDED
полный экран устройства с помощью следующего шаблона кода.
@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;
}
Хотя диалогическая анимация немного заметна, но задача открытия диалога DialogFragment в полноэкранном режиме очень хорошо.