#android #google-maps #bottomnavigationview #bottom-sheet
#Android #google-карты #bottomnavigationview #нижний лист
Вопрос:
Я пытаюсь воспроизвести поведение, присущее текущим картам Google, которое позволяет отображать нижний лист при перемещении вверх от нижней панели. Обратите внимание на запись ниже, что я сначала нажимаю на одну из кнопок на нижней панели, а затем перемещаю вверх, что, в свою очередь, показывает лист за ним.
Я нигде не могу найти объяснения, как можно достичь чего-то подобного. Я попытался изучить поведение нижнего листа и настроить его, но нигде я не могу найти способ отслеживать начальное нажатие, а затем позволить листу выполнять движение после достижения порога отклонения касания.
Как я могу добиться такого поведения, не прибегая к библиотекам? Или существуют какие-либо официальные представления Google / Android, которые допускают такое поведение между двумя разделами (панель навигации и нижний лист)?
Ответ №1:
Потребовалось некоторое время, но я нашел решение, основанное на примерах и обсуждении, предоставленных двумя авторами, их вклад можно найти здесь:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
Основная логика заключается в обработке событий касания в onInterceptTouchEvent
пользовательском BottomSheetBehavior
режиме и проверке в CoordinatorLayout
, представляет ли данное представление (с этого момента именуемое proxy view
) интерес для остальной части сенсорного делегирования в isPointInChildBounds
. При необходимости это можно адаптировать для использования более одного прокси-представления, единственное изменение, необходимое для этого, — создать список прокси-представлений и повторять список вместо использования одной ссылки на прокси-представление.
Ниже приведен пример кода этой реализации. Обратите внимание, что это настроено только для обработки вертикальных перемещений, если необходимы горизонтальные перемещения, то адаптируйте код к вашим потребностям.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.tabsheet.CustomCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/customCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@ id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/darker_gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_launcher_background"
android:text="Tab 1" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_launcher_background"
android:text="Tab 2" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_launcher_background"
android:text="Tab 3" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_launcher_background"
android:text="Tab 4" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_launcher_background"
android:text="Tab 5" />
</com.google.android.material.tabs.TabLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@ id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3F51B5"
android:clipToPadding="false"
app:behavior_peekHeight="0dp"
app:layout_behavior=".CustomBottomSheetBehavior" />
</com.example.tabsheet.CustomCoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
final CustomCoordinatorLayout customCoordinatorLayout;
final CoordinatorLayout bottomSheet;
final TabLayout tabLayout;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
bottomSheet = findViewById(R.id.bottomSheet);
tabLayout = findViewById(R.id.tabLayout);
iniList(bottomSheet);
customCoordinatorLayout.setProxyView(tabLayout);
}
private void iniList(final ViewGroup parent) {
@ColorInt int backgroundColor;
final int padding;
final int maxItems;
final float density;
final NestedScrollView nestedScrollView;
final LinearLayout linearLayout;
final ColorDrawable dividerDrawable;
int i;
TextView textView;
ViewGroup.LayoutParams layoutParams;
density = Resources.getSystem().getDisplayMetrics().density;
padding = (int) (20 * density);
maxItems = 50;
backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
dividerDrawable = new ColorDrawable(Color.WHITE);
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = new NestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = new LinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i ) {
textView = new TextView(this);
textView.setText("Item " (1 i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
public class CustomCoordinatorLayout extends CoordinatorLayout {
private View proxyView;
public CustomCoordinatorLayout(@NonNull Context context) {
super(context);
}
public CustomCoordinatorLayout(
@NonNull Context context,
@Nullable AttributeSet attrs
) {
super(context, attrs);
}
public CustomCoordinatorLayout(
@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean isPointInChildBounds(
@NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
return true;
}
// we want to intercept touch events if they are
// within the proxy view bounds, for this reason
// we instruct the coordinator layout to check
// if this is true and let the touch delegation
// respond to that result
if (proxyView != null) {
return super.isPointInChildBounds(proxyView, x, y);
}
return false;
}
// for this example we are only interested in intercepting
// touch events for a single view, if more are needed use
// a List<View> viewList instead and iterate in
// isPointInChildBounds
public void setProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
// we'll use the device's touch slop value to find out when a tap
// becomes a scroll by checking how far the finger moved to be
// considered a scroll. if the finger moves more than the touch
// slop then it's a scroll, otherwise it is just a tap and we
// ignore the touch events
private int touchSlop;
private float initialY;
private boolean ignoreUntilClose;
public CustomBottomSheetBehavior(
@NonNull Context context,
@Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull MotionEvent event
) {
// touch events are ignored if the bottom sheet is already
// open and we save that state for further processing
if (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
return super.onInterceptTouchEvent(parent, child, event);
}
switch (event.getAction()) {
// this is the first event we want to begin observing
// so we set the initial value for further processing
// as a positive value to make things easier
case MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
return super.onInterceptTouchEvent(parent, child, event);
// if the last bottom sheet state was not open then
// we check if the current finger movement has exceed
// the touch slop in which case we return true to tell
// the system we are consuming the touch event
// otherwise we let the default handling behavior
// since we don't care about the direction of the
// movement we ensure its difference is a positive
// integer to simplify the condition check
case MotionEvent.ACTION_MOVE:
return !ignoreUntilClose
amp;amp; Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
|| super.onInterceptTouchEvent(parent, child, event);
// once the tap or movement is completed we reset
// the initial values to restore normal behavior
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
return super.onInterceptTouchEvent(parent, child, event);
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
Результат с прозрачной строкой состояния и панелью навигации, которые помогают визуализировать скольжение нижнего листа вверх, но исключены из приведенного выше кода, поскольку это не относится к этому вопросу.
Примечание: Возможно, вам даже не понадобится пользовательское поведение нижнего листа, если ваш макет нижнего листа содержит определенный тип просмотра с возможностью прокрутки ( NestedScrollView
например), который может использоваться CoordinatorLayout
как есть, поэтому попробуйте без пользовательского поведения нижнего листа, как только ваш макет будет готов, поскольку это упростит задачу.
Ответ №2:
Вы могли бы попробовать что-то вроде этого (это псевдокод, надеюсь, вы понимаете, к чему я клоню):
<FrameLayout id=" id/bottomSheet">
<View id="exploreNearby bottomMargin="buttonContainerHeight/>
<LinearLayout>
<Button id="explore"/>
<Button id="explore"/>
<Button id="explore"/>
</LinearLayout>
<View width="match" height="match" id=" id/touchCatcher"
</FrameLayout>
Добавьте детектор жестов в представление нижней таблицы при переопределении onTouch (). который использует SimpleOnGestureListener
для ожидания событий «прокрутки» — все, кроме события прокрутки, которое вы можете воспроизвести до просмотра в обычном режиме.
При событии прокрутки вы можете увеличить свой exploreNearby в виде дельты (убедитесь, что он не повторяется и не становится слишком высоким или слишком низким).
Ответ №3:
Класс Bottom sheet уже сделает это за вас. Просто установите для него высоту просмотра равной 0, и он уже должен прослушивать жест скольжения вверх.
Однако я не уверен, что это будет работать с высотой просмотра 0. Поэтому, если это не сработает, просто установите высоту просмотра 20dp и сделайте верхнюю часть макета нижнего листа прозрачной, чтобы ее не было видно.
Это должно сработать для вас, если я не неправильно понимаю ваш вопрос. Если ваша цель состоит в том, чтобы просто иметь возможность нажать внизу и сдвинуть вверх, открывая нижний лист, который должен быть довольно простым.
Единственная возможная проблема, с которой вы «могли» столкнуться, заключается в том, что нижний лист не получает события касания из-за того, что кнопка уже использует его. Если это произойдет, вам нужно будет создать обработчик касания для всего экрана и возвращать «true», что вы обрабатываете его каждый раз, затем просто перенаправлять события касания в базовый вид, поэтому, когда вы превысите пороговое значение нижней панели вкладок, вы начнете отправлять события касания в макет нижнего листа вместо панели вкладок.
Это звучит сложнее, чем есть на самом деле. Большинство классов имеют onTouch, и вы просто перенаправляете его дальше. Однако, используйте только этот маршрут, если он не работает для вас «из коробки», как я описал в первых двух сценариях.
Наконец, еще один вариант, который может сработать, — создать кнопки вкладок как часть bottomSheetLayout и сделать высоту просмотра эквивалентной панели вкладок. Затем убедитесь, что панель вкладок ограничена нижним родительским листом, чтобы при пролистывании вверх она просто оставалась внизу. Это позволит вам нажимать кнопки или получать бесплатное поведение нижнего листа.
Удачного кодирования!