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

2.1.x

Ten przykład zależy od Biblioteki pomocy technicznej 23.4.0. +.

BottomSheetBehavior charakteryzuje się:

  1. Dwa paski narzędzi z animacjami reagującymi na ruchy dolnego arkusza.
  2. FAB, który ukrywa się, gdy znajduje się w pobliżu „modalnego paska narzędzi” (ten, który pojawia się podczas zsuwania się).
  3. Obraz w tle za dolnym arkuszem z pewnym efektem paralaksy.
  4. Tytuł (TextView) na pasku narzędzi, który pojawia się, gdy osiągnie go dolny arkusz.
  5. Pasek powiadomień może zmienić tło na przezroczyste lub pełny kolor.
  6. 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 z AppBarLayout.ScrollingViewBehavior
  • override layoutDependsOn i onDependentViewChanged 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:

  1. Utwórz klasę Java i rozszerz ją z CoordinatorLayout.Behavior<V>

  2. Skopiuj kod wklej z domyślnego pliku BottomSheetBehavior do nowego.

  3. 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);
    }
    
  4. Dodaj nowy stan

    public static final int STATE_ANCHOR_POINT = X;

  5. Zmodyfikuj kolejne metody: onLayoutChild , onStopNestedScroll , BottomSheetBehavior<V> from(V view) i setState (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:
[ CustomBottomSheetBehavior ]

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:

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ący CoordinatorLayout ) lub cały CoordinatorLayout jest wypełniony

  • STATE_HIDDEN : domyślnie wyłączone (i włączone z atrybutem app: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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow