Android
하단 시트
수색…
소개
하단 시트는 화면의 하단 가장자리에서 위로 밀어 올리는 시트입니다.
비고
하단 시트 는 화면 하단에서 위로 밀어서 더 많은 내용을 표시합니다.
그들은 v23.2.0 버전의 안드로이드 지원 라이브러리에 추가되었습니다.
Google지도와 같은 BottomSheetBehavior
이 예제는 지원 라이브러리 23.4.0. +에 따라 다릅니다.
BottomSheetBehavior의 특징은 다음과 같습니다.
- 하단 시트 이동에 반응하는 애니메이션이있는 두 개의 도구 모음.
- "모달 도구 모음"(위로 올릴 때 나타나는 모달 도구 모음) 근처에있을 때 숨 깁니다.
- 어떤 종류의 시차 효과와 함께 바닥 시트 뒤에 배경 이미지.
- 툴바의 제목 (TextView)은 하단 시트가 도달 할 때 나타납니다.
- 알림 상태 표시 줄은 배경을 투명하거나 풀 컬러로 바꿀 수 있습니다.
- "앵커"상태의 사용자 정의 바닥 시트 동작입니다.
이제 하나씩 확인해 보겠습니다.
도구 모음
Google지도에서 해당보기를 열면 검색 할 수있는 곳에서 도구 모음을 볼 수 있습니다.이 도구 모음은 내가 더 일반적으로하고 싶었 기 때문에 Google지도와 정확히 똑같지는 않습니다. 어쨌든 ToolBar
는 AppBarLayout
안에 있고 AppBarLayout
끌기 시작할 때 숨겨져 있으며 BottomSheet가 COLLAPSED
상태에 도달하면 다시 나타납니다.
그것을 달성하기 위해서는 다음을해야합니다.
-
Behavior
생성하고 그것을AppBarLayout.ScrollingViewBehavior
에서 확장하십시오. -
layoutDependsOn
및onDependentViewChanged
메소드를 오버라이드 (override)합니다. 그것을하면 바닥 시트 운동을 듣게됩니다. - 애니메이션으로 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 제목 표시 / 숨기기
- 바닥 시트를 닫거나 접힌 상태로 보냅니다.
FAB
이것은 사용자 정의 동작이지만 FloatingActionButton.Behavior
에서 확장됩니다. onDependentViewChanged
에서 "offSet"에 도달하거나 숨길 위치를 가리켜 야합니다. 내 경우에는 내가 두 번째 도구 모음에 가까운 때를 숨기려면, 그래서 나는 그때처럼 툴바의 위치를 사용하여 도구 모음을 포함하는 AppBarLayout을 찾고 FAB 부모 (A CoordinatorLayout) 파고 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;
}
시차 효과가있는 BottomSheet 뒤에있는 이미지 :
다른 것들과 마찬가지로, 이것은 커스텀 비헤이비어이며, 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;
}
이제 마지막으로 : Custom BottomSheet Behavior
3 단계를 수행하려면 먼저 기본 BottomSheetBehavior에 STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN
5 가지 상태가 있고 Google지도 비헤이비어에 대해 접힌 상태와 펼쳐진 상태 사이에 중간 상태를 추가해야한다고 STATE_ANCHOR_POINT
합니다 ( STATE_ANCHOR_POINT
.
나는 성공없이 기본 bottomSheetBehavior를 확장하려고 시도했기 때문에 모든 코드를 붙여 넣고 필요한 부분을 수정했다.
제가 이야기하는 것을 성취하려면 다음 단계를 따르십시오.
Java 클래스를 만들고
CoordinatorLayout.Behavior<V>
에서 확장합니다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); }
새 상태 추가
공공 정적 final int STATE_ANCHOR_POINT = 엑스;
다음 메소드를 수정하십시오 :
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'
그런 다음 아래 옵션을 사용하여 하단 시트를 사용할 수 있습니다.
-
CoordinatorLayout
과 함께 사용할BottomSheetBehavior
- 하단 시트 비헤이비어가있는 대화 상자 인
BottomSheetDialog
-
BottomSheetDialogFragment
는 표준 대화 상자 대신BottomSheetDialog
를 만드는DialogFragment
의 확장입니다.
영구적 인 바닥 시트
BottomSheetBehavior
를 자식에게 연결하는 지속 바닥 시트 는 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>
그런 다음 코드에서 다음을 사용하여 참조를 만들 수 있습니다.
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
setState () 메서드를 사용하여 BottomSheetBehavior의 상태를 설정할 수 있습니다.
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
다음 상태 중 하나를 사용할 수 있습니다.
STATE_COLLAPSED
:이 접힌 상태가 기본값이며 하단의 레이아웃 일부만 표시됩니다. 높이는app:behavior_peekHeight
속성 (기본값은 0)으로 제어 할 수 있습니다app:behavior_peekHeight
STATE_EXPANDED
: 전체 바닥 시트 중 하나가 표시되는 바닥 판의 완전 팽창 상태 (높이가 함유 미만인 경우CoordinatorLayout
) 또는 전체CoordinatorLayout
가득STATE_HIDDEN
: 기본적으로 사용 중지되며app:behavior_hideable
속성을 사용하도록 설정하면 사용자가 하단 시트를 아래로 스 와이프하여 하단 시트를 완전히 숨길 수 있습니다.
상태 변경 콜백을 받으려면 BottomSheetCallback
추가하면 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");
이 Fragment는 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 비헤이비어를 첨부 할 필요가 없습니다.
BottomSheet DialogFragment를 기본적으로 펼치기 모드로 엽니 다.
BottomSheet DialogFragment는 기본적으로 STATE_COLLAPSED
에서 열립니다. 다음 코드 템플릿의 도움으로 STATE_EXPANDED
를 열어 전체 장치 화면을 열도록 강요 할 수 있습니다.
@NonNull @Override public 대화 상자 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를 전체 화면으로 아주 잘 열어 보는 작업입니다.