Sök…


Introduktion

Ett bottenark är ett ark som glider upp från skärmens underkant.

Anmärkningar

Bottenarken glider upp från botten av skärmen för att avslöja mer innehåll.
De lades till Android Support Library i version 23.2.0.

BottomSheetBehavare som Google maps

2.1.x

Detta exempel beror på Support Library 23.4.0. +.

BottomSheetBehavior kännetecknas av:

  1. Två verktygsfält med animationer som svarar på bottenarkets rörelser.
  2. En FAB som döljer när det är nära "modalverktygsfältet" (det som visas när du skjuter upp).
  3. En bakgrundsbild bakom bottenarket med någon form av parallaxeffekt.
  4. En titel (TextView) i verktygsfältet som visas när det nedre arket når det.
  5. Meddelandesatusfältet kan vända sin bakgrund till transparent eller fullfärg.
  6. Ett anpassat bottenarkbeteende med ett "ankar" -läge.

Låt oss nu kolla dem en efter en:

verktygsfält
När du öppnar den vyn i Google Maps kan du se ett verktygsfält där du kan söka, det är det enda som jag inte gör precis som Google Maps, eftersom jag ville göra det mer generiskt. Hur som helst att ToolBar finns i en AppBarLayout och det gömdes när du börjar dra i bottenarket och det visas igen när bottenarket når COLLAPSED tillståndet.
För att uppnå det måste du:

  • skapa ett Behavior och utvidga det från AppBarLayout.ScrollingViewBehavior
  • åsidosätta layoutDependsOn och onDependentViewChanged metoder. Genom att göra det lyssnar du på rörelser i bottenarket.
  • skapa några metoder för att dölja och dölja AppBarLayout / Toolbar med animationer.

Så här gjorde jag det för första verktygsfältet eller 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();
}

Här är den kompletta filen om du behöver den

Det andra verktygsfältet eller "Modal" -verktygsfältet:
Du måste åsidosätta samma metoder, men i den här måste du ta hand om fler beteenden:

  • visa / dölja verktygsfältet med animationer
  • ändra statusfärgens färg / bakgrund
  • visa / dölj bottenbladets titel i verktygsfältet
  • stäng bottenarket eller skicka det till kollapsat tillstånd

Koden för den här är lite omfattande, så jag låter länken

FAB

Detta är ett anpassat beteende också, men sträcker sig från FloatingActionButton.Behavior . I onDependentViewChanged måste du titta när det når "offSet" eller peka på var du vill dölja det. I mitt fall vill jag gömma det när det är nära det andra verktygsfältet, så jag gräver in FAB-förälder (en koordinatorLayout) och letar efter AppBarLayout som innehåller verktygsfältet, sedan använder jag ToolBar-positionen som 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;
}

Komplett anpassad FAB-beteendelänk

Bilden bakom bottenarket med parallaxeffekt :
Liksom de andra är det ett anpassat beteende, det enda "komplicerade" i det här är den lilla algoritmen som håller bilden förankrad i bottenarket och undviker att bilden kollapsar som standardparallaxeffekt:

@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;
}


Den kompletta filen för bakgrundsbild med parallaxeffekt

Nu till slut: Det anpassade bottenarkbeteendet
För att uppnå de tre stegen måste du först förstå att standard BottomSheetBehavior har 5 tillstånd: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN och för Google Maps-beteende måste du lägga till ett mellanläge mellan kollapsat och expanderat: STATE_ANCHOR_POINT .
Jag försökte utvidga standard bottomSheetBehavior utan framgång, så jag kopierade bara in alla koder och ändrade det jag behöver.
Följ de följande stegen för att uppnå det jag talar om:

  1. Skapa en Java-klass och utvidga den från CoordinatorLayout.Behavior<V>

  2. Kopiera klistra in kod från standard BottomSheetBehavior fil till din nya.

  3. Ändra clampViewPositionVertical med följande kod:

    @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. Lägg till ett nytt tillstånd

    public static final int STATE_ANCHOR_POINT = X;

  5. Ändra nästa metoder: onLayoutChild , onStopNestedScroll , BottomSheetBehavior<V> from(V view) och setState (valfritt)



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;
}



Länk till hela projektet där du kan se alla anpassade beteenden

Och här ser det ut:
[ CustomBottomSheetBehavior ]

Snabbinställning

Se till att följande beroende läggs till i appens build.gradle-fil under beroenden:

compile 'com.android.support:design:25.3.1'

Sedan kan du använda bottenarket med hjälp av dessa alternativ:

Ihållande bottenark

Du kan uppnå en beständig underlakan fästa en BottomSheetBehavior till ett barn Vy över en 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>

Sedan kan du skapa en referens i din kod med:

 // The View with the BottomSheetBehavior  
 View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);  
 BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);  

Du kan ställa in statusen för din BottomSheetBehavior med metoden setState () :

mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);

Du kan använda ett av dessa tillstånd:

  • STATE_COLLAPSED : detta kollapsade tillstånd är standard och visar bara en del av layouten längst ner. Höjden kan styras med app:behavior_peekHeight attribut (standard till 0)

  • STATE_EXPANDED : det fullständigt utvidgade tillståndet för bottenarket, där antingen hela bottenarket är synligt (om dess höjd är mindre än den innehållande CoordinatorLayout ) eller hela CoordinatorLayout är fylld

  • STATE_HIDDEN : inaktiverad som standard (och aktiverad med app:behavior_hideable attribut), vilket gör det möjligt för användare att dra ner på bottenarket för att helt dölja bottenarket

Om du vill ta emot återuppringningar av tillståndsändringar kan du lägga till ett 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  
   }  
 });  

Modala bottenark med BottomSheetDialogFragment

Du kan förverkliga modala bottenark med hjälp av ett BottomSheetDialogFragment .

BottomSheetDialogFragment är ett modalt bottenark.
Detta är en version av DialogFragment som visar ett bottenark med hjälp av BottomSheetDialog stället för en flytande dialog.

Definiera bara fragmentet:

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);
    }
}

Använd sedan den här koden för att visa fragmentet:

MyBottomSheetDialogFragment mySheetDialog = new MyBottomSheetDialogFragment();
FragmentManager fm = getSupportFragmentManager();
mySheetDialog.show(fm, "modalSheetDialog");

Detta fragment skapar en BottomSheetDialog .

Modala bottenark med BottomSheetDialog

BottomSheetDialog är en dialog som är utformad som ett bottenark

Använd bara:

//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();

I det här fallet behöver du inte bifoga ett BottomSheet-beteende.

Öppna BottomSheet DialogFragment i Expanded-läge som standard.

BottomSheet DialogFragment öppnas som standard i STATE_COLLAPSED . Vilket kan tvingas öppna för STATE_EXPANDED och ta upp hela enhetsskärmen med hjälp av följande kodmall.

@NonNull @Override public Dialog onCreateDialog (Bundle sparadInstanceState) {

    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;
}

Även om dialoganimering är något märkbar men gör uppgiften att öppna DialogFragment i helskärm mycket bra.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow