How to make bottom sheet (BottomSheetBehavior) expandable to arbitrary position? - android

I have bottom sheet, and I want to change its behavior so it would work like on the main screen of Google Maps application, where you can expand it to any position and leave it there and it won't automatically stick to the bottom or to the top. Here's my layout with bottom sheet:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.gms.maps.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<View
android:id="#+id/shadow"
android:layout_width="match_parent"
android:layout_height="16dp"
android:background="#drawable/shape_gradient_top_shadow"
app:layout_anchor="#+id/map_bottom_sheet" />
<LinearLayout
android:id="#+id/map_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="300dp"
android:fillViewport="false"
android:orientation="vertical"
app:behavior_peekHeight="50dp"
android:background="#color/lightGray"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<include layout="#layout/bottom_sheet_top_buttons"/>
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottom_sheet_content_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/lightGray"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
What I need in essence is eliminate forcing of STATE_EXPANDED and STATE_COLLAPSED states when dragging is ended.
Here's a visual explanation of what I try to achieve:
As you can see, bottom sheet doesn't automatically anchor to the top or the bottom but stays at whatever position it was left.

Copy the code from android.support.design.widget.BottomSheetBehavior to make your own custom behavior. Then modify the onViewReleased() method which is responsible for the movement of the sheet after the drag ends. You also have to introduce a new state besides the existing ones - the state is helpful to restore the position and let others know in which state your sheet is at the moment with getState().
#Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
int top;
#State int targetState;
// Use the position where the drag ended as new top
top = releasedChild.getTop();
// You have to manage the states here, too (introduce a new one)
targetState = STATE_ANCHORED;
if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(releasedChild, new SettleRunnable(releasedChild, targetState));
} else {
setStateInternal(targetState);
}
}

I have created a proof of concept originating from the orginal source code from the design library. You can view it here. The problem with the original behavior is it doesn't allow flings, and most methods are private so extending the class and overriding some methods in an attempt to achieve it won't get you very far either. My implementation allows for optional snapping behavior, transient states (don't automatically snap after drag) and customizations around setting peek height and max height.

Hi Alex you can try this code for similar expected behaviour, it is not as optimised but it will help you to understand the concept.
final DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
bottomSheetBehavior.setPeekHeight(200);
// set callback for changes
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
Log.d(TAG, "onStateChanged: " + bottomSheet.getY() + "::" + bottomSheet.getMeasuredHeight() + " :: " + bottomSheet.getTop());
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
ViewGroup.LayoutParams params = bottomSheet.getLayoutParams();
params.height = Math.max(0, metrics.heightPixels - (int) bottomSheet.getTop());
bottomSheet.setLayoutParams(params);
}
});

Related

Is there a way to make RecyclerView requiresFadingEdge unaffected by paddingTop and paddingBottom

Currently, I need to use paddingTop and paddingBottom of RecyclerView, as I want to avoid complex space calculation, in my first RecyclerView item and last item.
However, I notice that, requiresFadingEdge effect will be affected as well.
This is my XML
<androidx.recyclerview.widget.RecyclerView
android:requiresFadingEdge="vertical"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:background="?attr/recyclerViewBackground"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
When paddingTop and paddingBottom is 40dp
As you can see, the fading effect shift down by 40dp, which is not what I want.
When paddingTop and paddingBottom is 0dp
Fading effect looks fine. But, I need to have non-zero paddingTop and paddingBottom, for my RecyclerView.
Is there a way to make RecyclerView's requiresFadingEdge unaffected by paddingTop and paddingBottom?
I found the best and kind of official solution for this. Override this 3 methods of RecyclerView. Which play most important role for fading edge.
First create your recyclerView.
public class MyRecyclerView extends RecyclerView {
// ... required constructor
#Override
protected boolean isPaddingOffsetRequired() {
return true;
}
#Override
protected int getTopPaddingOffset() {
return -getPaddingTop();
}
#Override
protected int getBottomPaddingOffset() {
return getPaddingBottom();
}
}
That's it. Use this recyclerview and you will see fadding edge unaffected.
Following content is just for explanation. If you want to know behind the scene.
To understand how this edge effect is working I dig into the class where android:requiresFadingEdge is used, And I found that it's not handled by RecyclerView instead It's handled by View class which is parent for all view.
In onDraw method of View class I found the code for drawing fade edge by using help of this method isPaddingOffsetRequired. Which used only for handling the fade effect.
According to documentation this method should be overridden by child class If you want to change the behaviour of fading edge. Bydefault It return false. So By returning true we are asking view to apply some offset for edge at the time of view drawing.
Look following snippet of onDraw method of View class to understand the calculation.
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
As we can see top variable is initialize using getFadeTop(offsetRequired).
protected int getFadeTop(boolean offsetRequired) {
int top = mPaddingTop;
if (offsetRequired) top += getTopPaddingOffset();
return top;
}
In this method, top is calculated by adding value of topOffSet when offset is needed. So to reverse the effect we need to pass negative value of padding which you are passing. so we need to return -getPaddingTop().
Now for bottom we are not passing negative value because bottom is working on top + height. So passing negative value make fade more shorter from the bottom so we need to add bottom padding to make it proper visible.
You can override this 4 method to play with it. getLeftPaddingOffset(), getRightPaddingOffset(), getTopPaddingOffset(), getBottomPaddingOffset()
I suggest a few solutions
, I hope to be helpful
1- RecyclerView.ItemDecoration (keep requiresFadingEdge)
class ItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration(){
override fun getItemOffsets(outRect: Rect, view: View, parent:
RecyclerView, state: RecyclerView.State?) {
val position = parent.getChildAdapterPosition(view)
when(position)
0 -> { outRect.top = spacing /*40dp*/ }
parent.adapter.itemCount-1 -> { outRect.bottom = spacing /*40dp*/ }
}
2- Multiple ViewHolders (depending on the ViewHolder either keep or remove requiresFadingEdge)
https://stackoverflow.com/a/26245463/5255963
3- Gradient (remove requiresFadingEdge)
Make two gradients with drawables like this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="90"
android:startColor="#000"
android:endColor="#FFF" />
</shape>
And set background to two views at the top and bottom of the Recyclerview.
There are many ways to achieve that but I preferred below solution that works for me.
You can use a third-party library called Android-FadingEdgeLayout checkout here
Here is a dependency.
implementation 'com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0'
In yours.xml
<com.bosphere.fadingedgelayout.FadingEdgeLayout
android:id="#+id/fading_edge_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fel_edge="top|left|bottom|right"
app:fel_size_top="40dp"
app:fel_size_bottom="40dp"
app:fel_size_left="0dp"
app:fel_size_right="0dp">
<androidx.recyclerview.widget.RecyclerView
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</com.bosphere.fadingedgelayout.FadingEdgeLayout>
Change your ReacyclerView property according to your need. below is example image with all sided sades. I hope that will help you.
Credits Android-FadingEdgeLayout

Allow BottomSheet to slide up after threshold is reached on an area outside

I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar.
Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.
I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.
How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?
Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
The basic logic is to handle touch events in onInterceptTouchEvent in a custom BottomSheetBehavior and check in a CoordinatorLayout if the given view (from now on named proxy view) is of interest for the rest of the touch delegation in isPointInChildBounds.
This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.
Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.
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
&& 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);
}
}
Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.
Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView for example) that can be used as is by the CoordinatorLayout, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<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>
Add a gesture detector on the bottomSheet view on override onTouch(). which uses SimpleOnGestureListener to wait for a "scroll" events - everything but a scroll event you can replicate down through to the view as normal.
On a scroll event you can grow your exploreNearby as a delta (make sure it doesn't recurse or go to high or too low).
The Bottom sheet class will already do this for you. Just set it's peek height to 0 and it should already listen for the slide up gesture.
However, I'm not positive it will work with a peek height of 0. So if that doesn't work, simply put a peek height of 20dp and make the top portion of the bottom sheet layout transparent so it is not visible.
That should do the trick for ya, unless I'm misunderstanding your question. If your goal is to simply be able to tap at the bottom and slide upwards bringing up the bottom sheet that should be pretty straight forward.
The one possible issue that you "could" encounter is if the bottom sheet doesn't receive the touch events due to the button already consuming it. If this happens you will need to create a touch handler for the whole screen and return "true" that you are handling it each time, then simply forward the touch events to the underlying view, so when you get above the threshold of your bottom tab bar you start sending the touch events to the bottom sheet layout instead of the tab bar.
It sounds harder than it is. Most classes have an onTouch and you just forward it on. However, only go that route, if it doesn't work for you out of the box the way I described in the first two scenarios.
Lastly, one other option that might work is to create your tab buttons as part of the bottomSheetLayout and make the peek height equivalent of the tab bar. Then make sure the tab bar is constrained to bottomsheet parent bottom, so that when you swipe up it simply stays at the bottom. This would enable you to click the buttons or get the free bottom sheet behavior.
Happy Coding!

Open an activity or fragment with Bottom Sheet Deep Linking

I want to open a Bottom Sheet (Deep Linking way) but inside of it instead of share options or just a layout, I want to have an activity with its layout or a fragment with its layout.
Known libraries that open Bottom Sheet Like Flipboard/BottomSheet can open layout, not whole activity.
Is there any possibility to achieve that with a Coordinator Layout?
I found a Google's Photo on Bottom Sheet Component Page that shows what exactly I have in mind. Google's description says:
The app on the right displays a bottom sheet containing content from the app on the left. This allows the user to view content from another app without leaving their current app.
I am not an expert, but after some research, I've found a simple way to do this. In your activity_main.xml first, make sure that your root layout is the android.support.design.widget.CoordinatorLayout.
Just inside that CoodrdinatorLayout add an include to your Bottom Sheet Layout:
<include layout="#layout/bottom_sheet_main" />
Then, and this is probably the most important step, you need to specify the behavior of the Bottom Sheet layout, so here is a sample code:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="vertical"
android:background="#FFFFFF"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_hideable="true"
app:behavior_peekHeight="64dp" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="#style/TextAppearance.AppCompat.Title"
android:padding="16dp"
android:text="BOTTOM SHEET" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="#style/TextAppearance.AppCompat.Body1"
android:padding="16dp"
android:text="Here goes text" />
</LinearLayout>
Okay, so that was all the XML code. Notice that we applied an app:layout_behavior so that it has the properties we want. Another important thing is to specify app:behavior_hideable="true" if we want to have the option of hiding the whole layout. The attribute app:behavior_peekHeight="64dp" means that the view will be 64dp high when it is collapsed (but not hidden).
There are 3 main stages of this view:
Expanded (STATE_EXPANDED): when the Bottom Sheet is completely open.
Collapsed (STATE_COLLAPSED): when the user only sees a small part from the top of the view. The attribute app:behavior_peekHeight determines this height.
Hidden(STATE_HIDDEN): When it is completely hidden (SURPRISE HAHA!).
We also have STATE_SETTLING and STATE_DRAGGING which are transitory, but they are not that important.
Now, if you run your app (you don't have to write any JAVA code) you will see that if you swipe up the title that will appear at the bottom of your layout, the Sheet will expand, and the same in the other way.
But you may notice that if you click on the Bottom Sheet, nothing happens. You can play with some Java code to manage the state of the Bottom Sheet:
Declare the view: LinearLayout bottomSheet = (LinearLayout)findViewById(R.id.bottomSheet);
Declare the behavior "manager":
final BottomSheetBehavior bsb = BottomSheetBehavior.from(bottomSheet);
And then you can get state changes:
bsb.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
String strNewState = "";
switch(newState) {
case BottomSheetBehavior.STATE_COLLAPSED:
strNewState = "STATE_COLLAPSED";
break;
case BottomSheetBehavior.STATE_EXPANDED:
strNewState = "STATE_EXPANDED";
break;
case BottomSheetBehavior.STATE_HIDDEN:
strNewState = "STATE_HIDDEN";
break;
case BottomSheetBehavior.STATE_DRAGGING:
strNewState = "STATE_DRAGGING";
break;
case BottomSheetBehavior.STATE_SETTLING:
strNewState = "STATE_SETTLING";
break;
}
Log.i("BottomSheets", "New state: " + strNewState);
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
Log.i("BottomSheets", "Offset: " + slideOffset);
}});
And there it is!
You can also use a Modal Bottom Sheet, which lets you create a Bottom Sheet as if it was a fragment.
For create that you need to have a BottomSheetDialogFragment from com.google.android.material library like this :
public class FragmentBottomSheetDialogFull extends BottomSheetDialogFragment {
private BottomSheetBehavior mBehavior;
private AppBarLayout app_bar_layout;
#NonNull
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
final View view = View.inflate(getContext(), R.layout.fragment_bottom_sheet_dialog_full, null);
dialog.setContentView(view);
mBehavior = BottomSheetBehavior.from((View) view.getParent());
mBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO);
mBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (BottomSheetBehavior.STATE_EXPANDED == newState) {
// View is expended
}
if (BottomSheetBehavior.STATE_COLLAPSED == newState) {
// View is collapsed
}
if (BottomSheetBehavior.STATE_HIDDEN == newState) {
dismiss();
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
return dialog;
}
#Override
public void onStart() {
super.onStart();
mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
then in your activity call that to open
// display sheet
FragmentBottomSheetDialogFull fragment = new FragmentBottomSheetDialogFull();
fragment.show(getSupportFragmentManager(), fragment.getTag());

Animate view from outside of bounds to show partly then full

I am currently struggling to find a good way how to animate some views in a specific way.
Following screenshots should show what I want to achieve:
First state (HIDDEN):
Second state (COLLAPSED)
Third state (EXPANDED)
The change between these states should be animated.
Those views are not draggable or slideable at all.
I know that there is the SlidingUpPanel by umano but I think that would be kind of an overkill.
At the moment the way I achieve this behaviour is the following:
I wrap the 2 panels (top and bot) in a relative layout and use the property animator to animate a change of the height of the relative layout.
So when the state is COLLAPSED then the height of the relative layout will be animated from 0 to the height of the top panel.
This works fine but I think that this is a really bad way to do this.
I already tried out to create a custom ViewGroup but the animating part didnt work yet.
Any input is appreciated.
I would use FrameLayout here as follows:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/screen"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/top_panel"
android:layout_width="match_parent"
android:layout_height="#dimen/top_panel_height"
android:layout_gravity="bottom"/>
<FrameLayout
android:id="#+id/bottom_panel"
android:layout_width="match_parent"
android:layout_height="#dimen/bottom_panel_height"
android:layout_gravity="bottom"/>
</FrameLayout>
Then, create enum for states
enum State {
HIDDEN {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(topPanel.getHeight()).setDuration(animationDuration);
bottomPanel.animate().translationY(topPanel.getHeight() + bottomPanel.getHeight()).setDuration(animationDuration);
}
},
COLLAPSED {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(0).setDuration(animationDuration);
bottomPanel.animate().translationY(bottomPanel.getHeight()).setDuration(animationDuration);
}
},
EXPANDED {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(-bottomPanel.getHeight()).setDuration(animationDuration);
bottomPanel.animate().translationY(0).setDuration(animationDuration);
}
};
public abstract void moveTo(View topPanel, View bottomPanel, long animationDuration);
}
Usage of this would be as follows:
State newState = State.EXPANDED;
newState.moveTo(topPanel, bottomPanel, 200);

Android, why adding left/right margin scales up image?

I have two image views, one on top of the other. Behind imageView displays user's image while the top one is cover image (just face area is fully transparent like following screenshot).
My layout is like this:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/rlContainer">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/ivUserImage"
android:contentDescription="#string/content_description"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/ivCoverImage"
android:contentDescription="#string/content_description"
android:scaleType="fitXY"/>
</RelativeLayout>
I'm using OnSwipeTouchListener class in order to adjust user's shape in transparent (face) area. I have following code in onCreateView() of my fragment:
mrlContainer = (RelativeLayout) view.findViewById(R.id.rlContainer);
mUserImage = (ImageView) view.findViewById(R.id.ivUserImage);
mUserImage.setImageURI(mImgUri);
mCoverImage = (ImageView) view.findViewById(R.id.ivCoverImage);
mCoverImage.setOnTouchListener(new OnSwipeTouchListener(mContext) {
public void onSwipeTop() {
moveImageToTop();
}
public void onSwipeRight() {
moveImageToRight();
}
public void onSwipeLeft() {
moveImageToLeft();
}
public void onSwipeBottom() {
moveImageToBottom();
}
public boolean onTouch(View v, MotionEvent event) {
return getGestureDetector().onTouchEvent(event);
}
});
And my movement methods are these:
private void moveImageToTop() {
LayoutParams layoutParams = (LayoutParams) mUserImage.getLayoutParams();
layoutParams.topMargin -= 20;
mUserImage.setLayoutParams(layoutParams);
}
private void moveImageToBottom() {
LayoutParams layoutParams = (LayoutParams) mUserImage.getLayoutParams();
layoutParams.bottomMargin -= 20;
mUserImage.setLayoutParams(layoutParams);
}
private void moveImageToRight() {
LayoutParams layoutParams = (LayoutParams) mUserImage.getLayoutParams();
layoutParams.rightMargin -= 20;
mUserImage.setLayoutParams(layoutParams);
}
private void moveImageToLeft() {
LayoutParams layoutParams = (LayoutParams) mUserImage.getLayoutParams();
layoutParams.leftMargin -= 20;
mUserImage.setLayoutParams(layoutParams);
}
Now, moveImageToTop() and moveImageToBottom() are working fine when I touch screen and move my finger top or bottom. However, image scales up when I move left or right.
What you think? Where is my mistake? Any suggestion would be appreciated. Thanks
As I know, default image scaleType is FIT_CENTER. You didn't change only position since you set MATCH_PARENT in both axis. Also you change View boundaries. By changing vertical boundaries you didn't change image size inside ImageView if your image is fit in horizontal axis.
If I were you, I will use Animation framework or change position during onLayout change and ask for relayout after every swipe.
I changed xml code of behind image to following code and left/right movement fixed:
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/ivUserImage"
android:contentDescription="#string/content_description"
android:scaleType="center"/>
Anyway, I decided to use PhotoView library by chrisbanes which is very easy to use.

Categories

Resources