Android: Nested bottom sheet click/drag touch event issue - android

I have a bottom sheet nested inside another bottom sheet (FrameLayouts using the BottomSheet layout behavior)
I also have a couple of 'peek views' (FrameLayouts) which have click listeners attached, to expand the bottom sheet(s) respectively, when clicked.
So the app basically has 3 main screens. The 'main container', then the first 'bottom sheet', which can be expanded full-screen, and then at the bottom of the first bottom sheet, is the second bottom sheet, which can also be expanded full-screen.
Problem:
When I add a RecyclerView to the nested bottom sheet 'container' view, dragging stops working for the second peek view (Sheet 2 Peek). If I remove the peek view ClickListener or the RecyclerView, things seem to work perfectly fine.
Desired result:
Both bottom sheets should remain draggable, and the peek views should be able to be clicked to expand their parent bottom sheet. The bottom sheet should respond to nested scrolls as it would normally.
I've tried removing the ClickListener and using touch gestures instead, but nothing I've tried seems to help.
I'm using v25.3.1 of the design support library, and I'm able to reproduce this problem on a Galaxy S4 running 4.4.4 stock, and a Nexus 6P running 7.1.2 stock. (I don't have any other devices available).
I've also created a test project on github for anyone interested in taking a closer look:
https://github.com/timusus/bottomsheet-test
Here's a couple of screenshots demonstrating the layout:
The layout structure looks like this (some code omitted for clarity):
<CoordinatorLayout>
<FrameLayout
android:id="#+id/mainContainer"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/sheet1"
android:layout_height="match_parent"
app:layout_behavior="CustomBottomSheetBehavior"
app:behavior_peekHeight="64dp">
<FrameLayout
android:id="#+id/sheet1Container"
android:layout_height="match_parent"/>
<CoordinatorLayout>
<FrameLayout
android:id="#+id/sheet2
android:layout_height="match_parent"
app:layout_behavior="CustomBottomSheetBehavior"
app:behavior_peekHeight="64dp">
<FrameLayout
android:id="#+id/sheet2Container"
android:layout_height="match_parent">
<!-- Problematic RecyclerView -->
<RecyclerView
android:layout_height="match_parent"/>
</FrameLayout>
<!-- Problematic Click Listener on this view -->
<FrameLayout
android:id="#+id/sheet2PeekView"
android:layout_height=64dp"/>
</FrameLayout>
</CoordinatorLayout>
<FrameLayout
android:id="#+id/sheet1PeekView"
android:layout_height=64dp"/>
</FrameLayout>
</CoordinatorLayout/>
The CustomBottomSheetBehavior is just a simple subclass of BottomSheetBehavior which prevents the first sheet from intercepting touch events if the second sheet is expanded or dragging. This allows the second sheet to be dragged from 'expanded' to 'collapsed' without also collapsing the first sheet.
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
private boolean allowDragging = true;
public void setAllowDragging(boolean allowDragging) {
this.allowDragging = allowDragging;
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
if (!allowDragging) {
return false;
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
I don't believe the customisation of BottomSheetBehavior is relevant to this problem, but for completeness, here's how it's used:
FrameLayout sheet1 = (FrameLayout) findViewById(R.id.sheet1);
bottomSheetBehavior1 = (CustomBottomSheetBehavior) BottomSheetBehavior.from(sheet1);
FrameLayout sheet2 = (FrameLayout) findViewById(R.id.sheet2);
bottomSheetBehavior2 = (CustomBottomSheetBehavior) BottomSheetBehavior.from(sheet2);
bottomSheetBehavior2.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
//If the second sheet is expanded or dragging, don't allow the first sheet to respond to touch events.
if (newState == BottomSheetBehavior.STATE_EXPANDED || newState == BottomSheetBehavior.STATE_DRAGGING) {
bottomSheetBehavior1.setAllowDragging(false);
} else {
bottomSheetBehavior1.setAllowDragging(true);
}
}
I can't seem to figure out if this is to do with the onInterceptTouchEvent of the BottomSheet, nested scroll handling of the inner RecyclerView, View.ClickListener stealing touch events, a combination of the above, or something else altogether.
Any help would be much appreciated.

FIXED
I can't seem to figure out if this is to do with the
onInterceptTouchEvent of the BottomSheet, nested scroll handling of
the inner RecyclerView, View.ClickListener stealing touch events, a
combination of the above, or something else altogether.
It is a combination of the above CustomBottomSheetBehavior and View.ClickListener
Issue was bottomSheetBehavior1 is taking drag event when getSheet2PeekView is dragging so detect touch event on getSheet2PeekView and set bottomSheetBehavior1 dragging false and bottomSheetBehavior2 true
Solution
Put this code and your problem is resolved.
findViewById(getSheet2PeekViewResId()).setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: ");
bottomSheetBehavior1.setAllowDragging(false);
bottomSheetBehavior2.setAllowDragging(true);
return false;
}
});
Also created Pull Request to your repo with fully working changes.

Related

Create custom AppBarLayout child with minimum height?

I want to create a custom view that will be a child of an AppBarLayout. I need this view to collapse partially as I scroll up, but not completely. It will have a minimum height and stay fixed to the top of the AppBarLayout in it's small size mode and then expand back to it's large size mode when the view is scrolled back down.
I've spent a lot of time looking through the source of the AppBarLayout and CoordinatorLayout, and so far I don't see a way to do what I want. It looks like children of AppBarLayout must either stay visible or disappear completely when the view is scrolled up.
Can anyone suggest a way to create a child of an AppBarLayout that will behave this way?
Thank you
Here's the recipe:
If you set android:minHeight, the AppBarLayout will respect that value by not scrolling beyond the point that would make your component smaller. So your XML layout might be something like this:
<com.example.CustomCollapsingLayout
android:layout_width="match_parent"
android:layout_height="320dp"
android:minHeight="108dp"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
Next you want to have your class register an OnOffsetChangedListener with the parent AppBarLayout. Your component will get events as the app bar is scrolled so that you know how to configure your view.
class OnOffsetChangedListener implements AppBarLayout.OnOffsetChangedListener {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
final int scrollRange = appBarLayout.getTotalScrollRange();
float offsetFactor = (float) (-verticalOffset) / (float) scrollRange;
...
This shows you how to find the total scroll range and then find the ratio between the total scroll range and the current scroll position i.e. where the app bar is in its scroll.
You should do what CollapsingToolbarLayout does; override onAttachedToWindow and add the listener there:
// Add an OnOffsetChangedListener if possible
final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
if (mOnOffsetChangedListener == null) {
mOnOffsetChangedListener = new OnOffsetChangedListener();
}
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
}
Take a look at the source code for CollapsingToolbarLayout as it will give you some ideas. Your view needs to do a lot of the same things.
You can also look at my sample project that has an image that scales and moves as the toolbar is scrolled: https://github.com/klarson2/Collapsing-Image

Block scroll along with Expand/Collapse Collapsible Toolbar

I was using Collapsible Toolbar in my app. On activity launch Collapsible Toolbar is expanded state with scrolling enabled and its working well normally. But now I have a requirement to show a full screen error layout in case my API fails. In that case I have to collapsed toolbar with scrolling effect blocked.
Error Layout shows a Retry Button. On Retry I make API call again and if API gives success I have to again expand Toolbar and enable scrolling effect.
I was able to collapse toolbar with setExpanded(flag, animate) but in that case I am not able to block scrolling effect of Collapsible Toolbar while error layout is shown.
I need to provide a way to block as well as unblock scroll effect + Expand/Collapse Toolbar. Any help would be really appreciated.. !!!
Make your error layout such that it will overlap Collapsible Toolbar. Also set android:clickable="true" to your error layout.
When you set visibility to your error layout, set Toolbar scrolling accordingly.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f3f3f3"
android:orientation="vertical"
>
<!-- Add your other layout including Collapsible Toolbar here.-->
<RelativeLayout
android:id="#+id/errorLayout"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
I created a library AppBarrr to lock the screen in expanded mode, based on my previous answer.
As I said, the height of the Toolbar is the key: the CollapsingToolbarLayout will collapse until the Toolbar's height and will expand until the AppBarLayout's height.
With this library, you must set two layouts as the Toolbar and your Expanded Layout (used to lock the screen and the scroll), it will create a CollapsingToolbarLayout and inflate these layouts inside.
You can declare the animations duration, the color of the inner CollapsingToolbarLayout, the collapsed/expanded title's style, even the height of the locked layout... You could also hide the Expanded Layout if you click outside it. It can support NestedScrollView and ScrollView inside the Expanded Layout. The documentation and a sample app are available on Github.
For those who don't want to use the library, my previous answer shows the way to do it. Here's the output of the previous answer:
Basically, this is the same concept, but no need to write a full class, with the lib you just need to have a simple widget in xml and that's it!
Feel free to use, fork or test. Hope it will be useful ;)
If you use AlertDialog to communicate the error and a ProgressDialog (spinner) to show you are doing stuff, you can block user input while your app is doing it's thing.
A simple solution that you can apply is just use the property
android:visibility="gone"
for the content that you don't want to show and just make your error layout visible by using property android:visibility="visible"
place the error layout at the bottom of your parent layout
once the contents are not visible on screen and error layout is just visible you will achieve the desired result that you want. Hope this helps you.
You can implement the interface and call its methods when to enable or disable the collapsing effect.
public interface AppbarRequestListener {
void unlockAppBarOpen();
void lockAppBarClosed();
}
#Override
public void unlockAppBarOpen() {
appBarLayout.setExpanded(true, false);
appBarLayout.setActivated(true);
setAppBarDragging(false);
}
#Override
public void lockAppBarClosed() {
appBarLayout.setExpanded(false, false);
appBarLayout.setActivated(false);
setAppBarDragging(false);
}
private void setAppBarDragging(final boolean isEnabled) {
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
AppBarLayout.Behavior behavior = new AppBarLayout.Behavior();
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
#Override
public boolean canDrag(AppBarLayout appBarLayout) {
return isEnabled;
}
});
params.setBehavior(behavior);
}

hide toolbar programmatically with coordinator layout

After extensive searching it seems that surprisingly no one is interested in the same behavior. Please point me to the appropriate place if I missed it.
The problem is the following:
I have a coordinator layout in the main activity xml. Inside of the coordinator layout there is a view pager. Inside two of the three fragments that I put inside the view pager there are recycle views that trigger the hiding of the toolbar in the coordinator layout. The third fragment does not have a recycle view though. The issue is that when the toolbar is shown the third fragment is drawn lower than it should, hiding part of the ui below the bottom edge of the screen. If the toolbar is hidden everything is shown normally.
So the question is - how can I hide the toolbar programmatically? e.g. when the user swipes to the third fragment in the view pager?
If you think there is a better approach - I would be also glad to hear that. Thanks!
Answer to your question :
I assume your Toolbar is included in a AppBarLayout.
To achieve it, you can add a ViewPager.OnPageChangeListener to your ViewPager, and in the onPageSelected() callback call setExpanded() on your AppBarLayout :
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
#Override
public void onPageSelected(int position) {
if(position == 2) { //the position of your non-scrolling fragment
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.setExpanded(false, true); //Hide the toolbar.
}
}
#Override
public void onPageScrollStateChanged(int state) { }
});
Alternate approach :
Another approach, won't say better, I'll let you judge depending on your content and user interactions, can be to keep having the same scrolling effect on your non-recycler fragment.
You can easily implement that by wrapping your fragment content in a NestedScrollView (included in support-v4) with the corresponding Behaviour that will trigger the hiding of the toolbar in your CoordinatorLayout :
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:fillViewport="true">
<!-- Your non-recycler view fragment layout -->
</android.support.v4.widget.NestedScrollView>
A nice blog post about scrolling, tabs and CoordinatorLayout : https://mzgreen.github.io/2015/06/23/How-to-hideshow-Toolbar-when-list-is-scrolling(part3)/

Scrollable MultiLine TextView within a ScrollView (No Touch Interaction Involved)

I have created a scroll view with a EditText in the middle that is multiLine (Scrollable). When that view is edited and lines are added beyond the allowed height it scrolls as expected. However, the parent scroll view for the whole container also scrolls as if it is following the text.
<ScrollView
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:background="#F7F3DE"
p1:id="#+id/scrollview">
<RelativeLayout
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:clickable="true"
p1:focusableInTouchMode="true"
p1:id="#+id/realtiveLayout">
<EditText
p1:id="#+id/editText"
p1:focusableInTouchMode="true"
p1:layout_width="match_parent"
p1:layout_height="150dp"
p1:hint="Comments"
p1:background="#00000000"
p1:textSize="16sp"
p1:textColor="#555555"
p1:gravity="top"
p1:minLines="5"
p1:maxLines="5"
p1:inputType="textCapSentences|textMultiLine"
p1:scrollHorizontally="false"
p1:layout_marginTop="5dp"
p1:textColorHint="#A9A9A9" />
</RelativeLayout>
</ScrollView>
Has anyone seen or know of a resolution for this issue?
(Note: This is not a question of how to scroll one instead of the other by touch as that I already understand. It is a question of the main scrollview moving while typing inside the EditText even though the text is not going lower but scrolling instead.
There is an answer here:
https://stackoverflow.com/a/28180281/3956566
Using this Managing Touch Events in a ViewGroup
Each child touch needs to provide an intercept that returns the parents touch event as false. To disable the parents touch event whilst the child element is being used. You can do this by creating an onInterceptTouchEvent(MotionEvent ev) in your java.
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
// ...
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
EDIT
This answer provided by
https://stackoverflow.com/users/498468/carl-odonnell
should help:
https://stackoverflow.com/a/5090420/3956566
Where the scrollview is disabled when the text field is touched.
// Get the ScrollView
final ScrollView myScroll = (ScrollView) findViewById(R.id.display_scrollview);
// Disable Scrolling by setting up an OnTouchListener to do nothing
myScroll.setOnTouchListener( new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
// Enable Scrolling by removing the OnTouchListner
tvDisplayScroll.setOnTouchListener(null);

Android: Synchronized scrolling of two different views

I have a tricky problem related to synchronized scrolling of two
different views.
I've made my own custom grid view widget, which has "sticky" views to
the left and top that only in one direction with the grid. Think of a
calendar where you have times at the top, dates at the left, and when
you scroll horizontally through time, the date view should stay put,
and when you scroll vertically through the dates, the time view should
stay put.
The grid itself is implemented using a nested horizontal scrollview in
a vertical scrollview. The grid is working great, so no problem there.
Since the sticky views are not in the actual grid, I have overriden
onScrollChanged in the grid scrollviews and programatically call
scrollTo on the sticky views when the user scrolls the grid.
That works as expected, except that there is a slight time offset as
to when the two different views start scrolling and ends scrolling. It
makes sense when you consider that the scrolling likely is executed
linearly on the UI thread I suppose..
All the views are scroll views, and I have enabled smooth scrolling
and used smoothScrollTo, etc, instead to try to improve this, but it's
the same problem nonetheless. The problem is especially noticeable on
larger screens, such as the Samsung Galaxy Tab, whereas it's hardly
noticeable on small-medium screen devices.
Any help is appreciated! If there is an easy fix, great..if it means
new design (that meets the sticky view usecase above), then so be it.
Code to trigger prog. scroll, same for horizontal
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
mListener.onScrollY(y);
super.onScrollChanged(x, y, oldx, oldy);
}
// which leads to,
// Handle vertical scroll
public void onScrollY(final int y) {
mCurrentY = y;
mVerticalScroll.smoothScrollTo(0, y);
}
XML layouts below, if that's of any help
The actual grid, which is a horizontal scroll view wrapped in a vertical scroll view and the grid items are added vertically in the nested linearlayout
>
< com.....VerticalScrollView
android:id="#+id/gridscroll"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="#id/timescroll"
android:layout_toRightOf="#id/vertscroll"
android:layout_alignTop="#id/vertscroll"
android:layout_marginLeft="2dp" android:scrollbars="none"
android:fadingEdge="none">
< com....HorizScrollView
android:id="#+id/horizscroll"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout android:id="#+id/grid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
< /LinearLayout>
< /com.....HorizScrollView>
< /com.....VerticalScrollView>
The horizontal sticky view
< com.....GridTimeScrollView
android:id="#+id/timescroller"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout android:id="#+id/timelist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" />
< /com.....GridTimeScrollView>
The vertical sticky view
< com....GridVertListScrollView
android:id="#+id/vertscroller"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="none"
android:fadingEdge="none">
< LinearLayout
android:id="#+id/vertitemlist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" />
< /com.....GridVertListScrollView>
First of all, I think you should be aware of this: ScrollView Inside ScrollView
In short: using scrollviews inside scrollviews is a bad thing that breaks many optimizations.
Now, onto your question.
I've had a similar need to what you described. I ended up implementing a custom view and its onDraw method. This was in part because I was drawing something not trivial and you may not have to do it.
Anyway, I believe that your best option is:
Implement a custom view that extends relative layout
create the layout of this view with the top, left and "main" views that will be the scrollable components
add a OnGestureListener to this view and pass touch events in your activity into the custom view
when your gesture listener detects a fling or a scroll, invoke scrollBy in each of the scrolling views. When you do this, if you want the top view to scroll horizontally only, pass 0 as the vertical scroll distance.
In order to implement smooth fling movements, you need to create a scroller (in your custom view). When the gesture listener detects a fling event, set the scroller up. Then, override your custom view's computeScroll() method and update the scroll in each of child views. Check this example to know how to implement it. I apologize, I will try to post a better example when possible. Check my code below... it's simpler :)
Update: sample code
#Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
if (!scrolledLastFrame) {
lastX = scroller.getStartX();
lastY = scroller.getStartY();
}
int dx = scroller.getCurrX() - lastX;
int dy = scroller.getCurrY() - lastY;
lastX = scroller.getCurrX();
lastY = scroller.getCurrY();
doScroll(dx, dy);
scrolledLastFrame = true;
} else {
scrolledLastFrame = false;
}
}
Don't use smoothScrollTo, use scrollTo instead. smoothScrollTo will animate a scroll from its current position to the position you want, because you want them to be synced, you want the other scrollview to be instantly exactly where the other scrollview is, scrollTo does that.
Looking at the answers. Why don't you handle it with a OnTouch Listener. When the touch event is MOVE you call.
public void onScrollY(final int y) {
mCurrentY = y;
mVerticalScroll.smoothScrollTo(0, y);
}
with the value of getVerticalScroll on your other ScrollView. That's pretty easy. And return true so TouchEvents will be handled further.
Here's what it could look like
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
lv2.onScrollY(lv.getScrollY());
break;
default:
break;
}
return false;
}
Simple right?
Not sure if it's exactly what you wanted. But could you elaborate exactly what you're trying to achieve. Not in the sense of layout but in practice..whats the point of your UI?

Categories

Resources