How do I make a Scrollable TextView inside CollapsingToolbar? - android

I am currently experimenting with CoordinatorLayout + AppbarLayout + CollapsingToolbarLayout in a way such that:
1) Scroll down the Appbar using "Toolbar" [ No Nested ScrollView / RecyclerView ].
2) The content below the appbar should move along with the appbar scrolling.
3) Multiple images kept under ViewPager.
4) The last item in the ViewPager would be an textview.
I have achieved 1) and 2) using the following layout :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<android.support.design.widget.AppBarLayout
android:id="#+id/flexible.example.appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
>
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/flexible.example.collapsing"
android:layout_width="match_parent"
android:layout_height="300dp"
app:expandedTitleMarginBottom="94dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:contentScrim="?colorPrimary"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:id="#+id/text_sample"
android:scrollbars="vertical"
android:scrollIndicators="right"
app:layout_collapseMode="parallax"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_gravity="center"
android:nestedScrollingEnabled="true"
/>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.v7.widget.Toolbar
android:id="#+id/ioexample.toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/PM01"
android:elevation="4dp"
app:layout_collapseMode="pin"
app:layout_anchor="#id/flexible.example.collapsing"
app:layout_anchorGravity="bottom"
app:theme="#style/ThemeOverlay.AppCompat.Light"
style="#style/ToolBarWithNavigationBack"
app:layout_scrollFlags="scroll|enterAlways|snap"
>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/recyclerviewcontainer"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
What I am trying to achieve now is to make the textview inside collpasingtoolbarlayout is to be scrollable (#4 above). Since my search till now has made me believe that the Appbar is handling all the touch events by itself, this doesn't seems to be easy. But since it is a requirement, I would be more than happy to have a guidance / pointers to help me complete this.
Can someone please let me know what and where to look for achieving this functionality.

After a lot of research and some more searching through SO, I got an idea what I need to do in order to achieve the desired effect:
1) Implement a custom behavior for appbarlayout :
public class AppBarLayoutCustomBehavior extends AppBarLayout.Behavior {
private boolean setIntercept = false;
private boolean lockAppBar = false;
DragCallback mDragCallback = new DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return !lockAppBar;
}
};
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
super.onInterceptTouchEvent(parent, child, ev);
return setIntercept;
}
public void setInterceptTouchEvent(boolean set) {
setIntercept = set;
}
public AppBarLayoutCustomBehavior() {
super();
setDragCallback(mDragCallback);
}
public AppBarLayoutCustomBehavior(Context ctx, AttributeSet attributeSet) {
super(ctx, attributeSet);
setDragCallback(mDragCallback);
}
public void lockAppBar() {
lockAppBar = true;
}
public void unlockAppBar() {
lockAppBar = false;
}
}
2) Use this custom behavior with app bar :
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appBarLayout.getLayoutParams();
final AppBarLayoutCustomBehavior mBehavior = new AppBarLayoutCustomBehavior();
lp.setBehavior(mBehavior);
/// use toolbar to enable/disable dragging on the appbar behavior. This way
/// out toolbar acts as a drag handle for the app bar.
Toolbar toolbar = (Toolbar) activity.findViewById(R.id.main_toolbar);
toolbar.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
mBehavior.setInterceptTouchEvent(true);
return true;
case MotionEvent.ACTION_CANCEL:
mBehavior.setInterceptTouchEvent(false);
return true;
}
return false;
}
});
3) Set movement method on the textview to make it scrollable
textView.setMovementMethod(ScrollingMovementMethod.getInstance());

Related

How to swipe ViewPager while the content is being scrolled?

I am able to implement TabLayout with ViewPager nicely but when the content inside the ViewPager is being scrolled (the deceleration animation), I cannot swipe left and right to change to other fragments inside ViewPager. I have to wait until the scrolling animation stops and then swipe.
Below is some of my code snippets.
I have a simple activity_main.xml layout like below.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbarHome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"/>
<LinearLayout
android:id="#+id/mainContainerLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
<android.support.design.widget.BottomNavigationView
android:id="#+id/btmNavView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/white"
app:menu="#menu/menu_main" />
</LinearLayout>
I then inflate a fragment containing TabLayout and ViewPager into the LinearLayout in the middle of the activity_main.xml. This layout can be seen below:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.pchatanan.sontana.custom_views.AppTabLayout
android:id="#+id/contactTabLayout"
app:tabMode="fixed"
android:layout_height="wrap_content"
android:layout_width="match_parent"
style="#style/AppTabLayout"/>
<android.support.v4.view.ViewPager
android:id="#+id/contactViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
The logic inside my fragment is below:
fragmentArray = new Fragment[]{new SimpleFragment(), new SimpleFragment(), new SimpleFragment()};
titleArray = new String[]{"fragment1", "fragment2", "fragment3"};
contactPagerAdapter = new AppPagerAdapter(getChildFragmentManager(), fragmentArray, titleArray);
contactViewPager.setAdapter(contactPagerAdapter);
contactViewPager.setOffscreenPageLimit(fragmentArray.length - 1);
contactTabLayout.setupWithViewPager(contactViewPager);
Use this instead defualt ViewPager.
Detect when ScrollView or RecyclerView is scrolling. Then call
viewPager.setPagingEnabled(pagingEnable);
If pagingEnable
public class CustomViewPager extends ViewPager {
private boolean isPagingEnabled = true;
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean onTouchEvent(MotionEvent event) {
return this.isPagingEnabled && super.onTouchEvent(event);
}
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.isPagingEnabled && super.onInterceptTouchEvent(event);
}
public void setPagingEnabled(boolean b) {
this.isPagingEnabled = b;
}
}
Update:
You can do reverse to like you can disable ScrollView scroll when ViewPager is swiping
mScrollView.requestDisallowInterceptTouchEvent(true);
Similar to what suggested by #Khemraj, the problem is the touch is not intercepted by ViewPager. It is only intercepted by the ScrollView. To fix this, we should allow ViewPage to intercept the touch when scrolling:
scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
#Override
public void onScrollChange(View view, int i, int i1, int i2, int i3) {
viewPager.requestDisallowInterceptTouchEvent(false);
}
});
A better implementation is to make use of CallBackListener onto the fragment containing the scrollView.

How to avoid blocking of scrolling itself when using setNestedScrollingEnabled(false)?

Background
We have quite a complex layout that has CollapsingToolbarLayout in it, together with a RecyclerView at the bottom.
In certain cases, we temporarily disable the expanding/collapsing of the CollapsingToolbarLayout, by calling setNestedScrollingEnabled(boolean) on the RecyclerView.
The problem
This usually works fine.
However, on some (bit rare) cases, slow scrolling on the RecyclerView gets semi-blocked, meaning it tries to scroll back when scrolling down. It's as if it has 2 scrolling that fight each other (scroll up and scroll down):
The code to trigger this is as such:
res/layout/activity_scrolling.xml
<android.support.design.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="#dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/nestedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_anchor="#id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
android:id="#+id/disableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="disable"/>
<Button
android:id="#+id/enableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="enable"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
ScrollingActivity.java
public class ScrollingActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
final RecyclerView nestedView = (RecyclerView) findViewById(R.id.nestedView);
findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
nestedView.setNestedScrollingEnabled(false);
}
});
findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
nestedView.setNestedScrollingEnabled(true);
}
});
nestedView.setLayoutManager(new LinearLayoutManager(this));
nestedView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
android.R.layout.simple_list_item_1,
parent,
false)) {
};
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
}
#Override
public int getItemCount() {
return 100;
}
});
}
}
What I've tried
At first I thought it's because of something else (I thought it's a weird combination with DrawerLayout), but then I've found a minimal sample to show it, and it's just as I thought: it's all because of the setNestedScrollingEnabled.
I tried to report about this on Google's website (here), hoping it will get fixed if it's a real bug. If you wish to try it out, or watch the videos of the issue, go there, as I can't upload them all here (too large and too many files).
I've also tried to use special flags as instructed on other posts (examples: here, here, here, here and here) , but none helped. In fact each of them had an issue, whether it's staying in expanded mode, or scrolling in a different way than what I do.
The questions
Is this a known issue? Why does it happen?
Is there a way to overcome this?
Is there perhaps an alternative to calling this function of setNestedScrollingEnabled ? One without any issues of scrolling or locking the state of the CollapsingToolbarLayout ?
This is an alternate approach to achieving the same goal as this answer. While that answer used Reflection, this answer does not, but the reasoning remains the same.
Why is this happening?
The problem is that RecyclerView sometimes uses a stale value for the member variable mScrollOffset. mScrollOffset is set in only two places in RecyclerView: dispatchNestedPreScroll and dispatchNestedScroll. We are only concerned with dispatchNestedPreScroll. This method is invoked by RecyclerView#onTouchEvent when it handles MotionEvent.ACTION_MOVE events.
The following is from the documentation for dispatchNestedPreScroll.
dispatchNestedPreScroll
boolean dispatchNestedPreScroll (int dx,
int dy,
int[] consumed,
int[] offsetInWindow)
Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
Nested pre-scroll events are to nested scroll events what touch intercept is to touch. dispatchNestedPreScroll offers an opportunity for the parent view in a nested scrolling operation to consume some or all of the scroll operation before the child view consumes it.
...
offsetInWindow int: Optional. If not null, on return this will contain the offset in local view coordinates of this view from before this operation to after it completes. View implementations may use this to adjust expected input coordinate tracking.
offsetInWindow is actually an int[2] with the second index representing the y shift to be applied to the RecyclerView due to nested scrolling.
RecyclerView#DispatchNestedPrescroll resolves to a method with the same name in NestedScrollingChildHelper.
When RecyclerView calls dispatchNestedPreScroll,
mScrollOffset is used as the offsetInWindow argument. So any changes made to offsetInWindow directly updates mScrollOffset. dispatchNestedPreScroll updates mScrollOffset as long as nested scrolling is in effect. If nested scrolling is not in effect, then mScrollOffset is not updated and proceeds with the value that was last set by dispatchNestedPreScroll. Thus, when nested scrolling is turned off, the value of mScrollOffset becomes immediately stale but RecyclerView continues to use it.
The correct value of mScrollOffset[1] upon return from dispatchNestedPreScroll is the amount to adjust for input coordinate tracking (see above). In RecyclerView the following lines adjusts the y touch coordinate:
mLastTouchY = y - mScrollOffset[1];
If mScrollOffset[1] is, let's say, -30 (because it is stale and should be zero) then mLastTouchY will be off by +30 pixels (--30=+30). The effect of this miscalculation is that it will appear that the touch occurred further down the screen than it really did. So, a slow downward scroll will actually scrolls up and an upward scroll will scroll faster. (If a downward scroll is fast enough to overcome this 30px barrier, then downward scrolling will occur but more slowly than it should.) Upward scrolling will be overly quick since the app thinks more space has been covered.
mScrollOffset will continue as a stale variable until nested scrolling is turned on and dispatchNestedPreScroll once again reports the correct value in mScrollOffset.
Approach
Since mScrollOffset[1] has a stale value under certain circumstances, the goal is to set it to the correct value under those circumstances. This value should be zero when nested scrolling is not taking place, i.e., When the AppBar is expanded or collapsed. Unfortunately, mScrollOffset is local to RecyclerView and there is no setter for it. To gain access to mScrollOffset without resorting to Reflection, a custom RecyclerView is created that overrides dispatchNestedPreScroll. The fourth agument is offsetInWindow which is the variable we need to change.
A stale mScrollOffset occurs whenever nested scrolling is disabled for the RecyclerView. An additional condition we will impose is that the AppBar must be idle so we can safely say that mScrollOffset[1] should be zero. This is not an issue since the CollapsingToolbarLayout specifies snap in the scroll flags.
In the sample app, ScrollingActivity has been modified to record when the AppBar is expanded and closed. A callback has also been created (clampPrescrollOffsetListener) that will return true when our two conditions are met. Our overridden dispatchNestedPreScroll will invoke this callback and clamp mScrollOffset[1] to zero on a true response.
The updated source file for ScrollingActivity is presented below as is the custom RecyclerView - MyRecyclerView.
The XML layout file must be changed to reflect the custom MyRecyclerView.
ScrollingActivity
public class ScrollingActivity extends AppCompatActivity
implements MyRecyclerView.OnClampPrescrollOffsetListener {
private CollapsingToolbarLayout mCollapsingToolbarLayout;
private AppBarLayout mAppBarLayout;
private MyRecyclerView mNestedView;
// This variable will be true when the app bar is completely open or completely collapsed.
private boolean mAppBarIdle = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mNestedView = (MyRecyclerView) findViewById(R.id.nestedView);
mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout);
// Set the listener for the patch code.
mNestedView.setOnClampPrescrollOffsetListener(this);
// Listener to determine when the app bar is collapsed or fully open (idle).
mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarIdle = verticalOffset == 0
|| verticalOffset <= appBarLayout.getTotalScrollRange();
}
});
findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
// If the AppBar is fully expanded or fully collapsed (idle), then disable
// expansion and apply the patch; otherwise, set a flag to disable the expansion
// and apply the patch when the AppBar is idle.
setExpandEnabled(false);
}
});
findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
setExpandEnabled(true);
}
});
mNestedView.setLayoutManager(new LinearLayoutManager(this));
mNestedView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
android.R.layout.simple_list_item_1,
parent,
false)) {
};
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
}
#Override
public int getItemCount() {
return 100;
}
});
}
private void setExpandEnabled(boolean enabled) {
mNestedView.setNestedScrollingEnabled(enabled);
}
// Return "true" when the app bar is idle and nested scrolling is disabled. This is a signal
// to the custom RecyclerView to clamp the y prescroll offset to zero.
#Override
public boolean clampPrescrollOffsetListener() {
return mAppBarIdle && !mNestedView.isNestedScrollingEnabled();
}
private static final String TAG = "ScrollingActivity";
}
MyRecyclerView
public class MyRecyclerView extends RecyclerView {
private OnClampPrescrollOffsetListener mPatchListener;
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
// Just a call to super plus code to force offsetInWindow[1] to zero if the patchlistener
// instructs it.
#Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
boolean returnValue;
int currentOffset;
returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
currentOffset = offsetInWindow[1];
Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset);
if (mPatchListener.clampPrescrollOffsetListener() && offsetInWindow[1] != 0) {
Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset + " -> 0");
offsetInWindow[1] = 0;
}
return returnValue;
}
public void setOnClampPrescrollOffsetListener(OnClampPrescrollOffsetListener patchListener) {
mPatchListener = patchListener;
}
public interface OnClampPrescrollOffsetListener {
boolean clampPrescrollOffsetListener();
}
private static final String TAG = "MyRecyclerView";
}
Actually, you might be looking at the problem in the wrong way.
The only thing you need is to set the Toolbar flags accordingly. You don't really anything else so I would say that your layout should be simplified to:
<android.support.design.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="#dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:title="Title" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/nestedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_anchor="#id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
android:id="#+id/disableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="disable"/>
<Button
android:id="#+id/enableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="enable"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
Then when you wish to disable the collapsing just set your toolbar flags:
// To disable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
toolbar.setLayoutParams(params);
And to enable
// To enable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL|AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
toolbar.setLayoutParams(params);
Hold a reference to the layout params if you are changing instead of getting it all the time.
If you need to have the CollapsingToolbarLayout get from and set the LayoutParams to that View instead, update the flags the same way but now adding the appBarLayout.setExpanded(true/false)
Note: Using the setScrollFlags clears all previous flags, so be careful and set all required flags when using this method.
As #Moinkhan points out, you could try wrapping the RecyclerView and next elements in a NestedScrollView like this, this should resolve your problem of scrolling alongside with your collapsing toolbar layout:
<android.support.design.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="#dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="fill_vertical"
android:fillViewport="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/nestedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_anchor="#id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
android:id="#+id/disableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="disable"/>
<Button
android:id="#+id/enableNestedScrollingButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="enable"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
In case the contents of the recyclerview are not displayed you can follow this thread to solve that issue How to use RecyclerView inside NestedScrollView?.
Hope it helps.
inside the recycler view, to scrolling smooth
android:nestedScrollingEnabled="false"
to overlap the cardView in the toolbar
app:behavior_overlapTop = "24dp"
Try this code for CollapsingToolbar:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/background"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="#dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:title="Title" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="#android:color/transparent"
app:behavior_overlapTop="#dimen/behavior_overlap_top"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="#+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_min_padding"
android:nestedScrollingEnabled="false"
android:scrollbarSize="2dp"
android:scrollbarStyle="outsideInset"
android:scrollbarThumbVertical="#color/colorAccent"
android:scrollbars="vertical" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
Screenshot
I had to solve a similar issue and did it using a custom behaviour on the AppBarLayout. Everything works great.
By overriding onStartNestedScroll in the custom behaviour it is possible to block to collapsing toolbar layout from expanding or collapsing while keeping the scroll view (NestedScrollView) in my case, working as expected. I explained the details here, hope it helps.
private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
var canDrag = true
var acceptsNestedScroll = true
init {
setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
// Allow/Do not allow dragging down/up to expand/collapse the layout
return canDrag
}
})
}
override fun onStartNestedScroll(parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int): Boolean {
// Refuse/Accept any nested scroll event
return acceptsNestedScroll
}}
Use following code, it works fine for me:
lockAppBarClosed();
ViewCompat.setNestedScrollingEnabled(recyclerView, false); // to lock the CollapsingToolbarLayout
and implement the following methods:
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);
}
public void unlockAppBarOpen() {
appBarLayout.setExpanded(true, false);
appBarLayout.setActivated(true);
setAppBarDragging(false);
}
public void lockAppBarClosed() {
appBarLayout.setExpanded(false, false);
appBarLayout.setActivated(false);
setAppBarDragging(false);
}
I believe that this problem is related to the collapsing toolbar snapping into place (either closed or open) and leaving a vertical offset variable (mScrollOffset[1] in RecyclerView) with a non-zero value that subsequently biases the scroll - slowing or reversing the scroll in one direction and speeding it up in the other. This variable only seems to be set in NestedScrollingChildHelper if nested scrolling is enabled. So, whatever value mScrollOffset[1] has goes unchanged once nest scrolling is disabled.
To reliably reproduce this issue, you can cause the toolbar to snap into place then immediately click disable. See this video for a demonstration. I believe, that the magnitude of the issue varies by how much "snapping" occurs.
If I drag the toolbar to the fully open or closed position and don't let it "snap", then I have not been able to reproduce this problem and mScrollOffset[1] is set to zero which I think is the right value. I have also reproduced the problem by removing snap from the layout_scrollFlags of the collapsing toolbar in the layout and placing the toolbar in a partially open state.
If you want to play around with this, you can put your demo app into debug mode and observe the value of mScrollOffset[1] in RecyclerView#onTouchEvent. Also take a look at NestedScrollingChildHelper's dispatchNestedScroll and dispatchNestedPreScroll methods to see how the offset is set only when nested scrolling is enabled.
So, how to fix this? mScrollOffset is private toRecyclerView and it is not immediately obvious how to subclass anything to change the value of mScrollOffset[1]. That would leave Reflection, but that may not be desirable to you. Maybe another reader has an idea about how to approach this or knows of some secret sauce. I will repost if anything occurs to me.
Edit: I have provided a new ScrollingActivity.java class that overcomes this issue. It does use reflection and applies a patch to set mScrollOffset[1] of RecyclerView to zero when the disable scroll button has been pressed and the AppBar is idle. I have done some preliminary testing and it is working. Here is the gist. (See updated gist below.)
Second edit: I was able to get the toolbar to snap in funny ways and get stuck in the middle without the patch, so it doesn't look like the patch is causing that particular issue. I can get the toolbar to bounce from fully open to collapsed by scrolling down fast enough in the unpatched app.
I also took another look at what the patch is doing and I think that it will behave itself: The variable is private and referred to only in one place after scrolling is turned off. With scrolling enabled, the variable is always reset before use. The real answer is for Google to fix this problem. Until they do, I think this may be the closest you can get to an acceptable work-around with this particular design. (I have posted an updated gist that addresses potential issues with a quick click-around leaving switches in a potential unsuitable state.)
Regardless, the underlying issue has been identified and you have a reliable way to reproduce the problem, so you can more easily verify other proposed solutions.
I hope this helps.
I want to present a nice alternative, mainly based on the one here :
AppBarLayoutEx.kt
class AppBarLayoutEx : AppBarLayout {
private var isAppBarExpanded = true
private val behavior = AppBarLayoutBehavior()
private var onStateChangedListener: (Boolean) -> Unit = {}
var enableExpandAndCollapseByDraggingToolbar: Boolean
get() = behavior.canDrag
set(value) {
behavior.canDrag = value
}
var enableExpandAndCollapseByDraggingContent: Boolean
get() = behavior.acceptsNestedScroll
set(value) {
behavior.acceptsNestedScroll = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
init {
addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
isAppBarExpanded = verticalOffset == 0
onStateChangedListener(isAppBarExpanded)
})
}
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
super.setLayoutParams(params)
(params as CoordinatorLayout.LayoutParams).behavior = behavior
}
fun toggleExpandedState() {
setExpanded(!isAppBarExpanded, true)
}
fun setOnExpandAndCollapseListener(onStateChangedListener: (Boolean) -> Unit) {
this.onStateChangedListener = onStateChangedListener
}
private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
var canDrag = true
var acceptsNestedScroll = true
init {
setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout) = canDrag
})
}
override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View,
target: View, nestedScrollAxes: Int, type: Int) = acceptsNestedScroll
}
}
Usage: besides using it in the layout XML file, you can disable/enable the expanding of it using:
appBarLayout.enableExpandAndCollapseByDraggingToolbar = true/false
appBarLayout.enableExpandAndCollapseByDraggingContent = true/false

Is it possible to touch the ImageView inside CollapsingToolbarLayout and scroll it?

I'm trying to get a fullscreen CollapsingToolbar but when I set match_parent to the height of AppBarLayout I'm not able to scroll the ImageView which is inside CollapsingToolbarLayout. I have to leave some space so that I can touch the "white" of the activity (in AppBarLayout I added android:layout-marginBottom:"16dp" ) and only then, after I touched it, I can scroll the ImageView otherwise I can't.
This happens everytime I run the app and touch the layout for the first time. So I have to touch the white first and then scroll the image.
Could you help me?
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="#+id/drawer">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp">
<ImageView
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"
android:contentDescription="#null"
android:src="#drawable/background" />
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin"
app:theme="#style/ToolbarTheme"
android:id="#+id/toolbar"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:id="#+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</FrameLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
<com.myapplication.ScrimInsetsFrameLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="304dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:insetForeground="#4000"
android:clickable="true"
android:background="#ffffff">
...
</com.myapplication.ScrimInsetsFrameLayout>
</android.support.v4.widget.DrawerLayout>
EDIT #PPartisan I've done what you said but here's what I got:
This isn't a nice solution, but it does work on my test device. It kick starts the scrolling process by explicitly assigning a touch listener to the AppBar that triggers a nested scroll.
First, create a custom class that extends NestedScrollView and add the following method so it look something like this:
public class CustomNestedScrollView extends NestedScrollView {
private int y;
public CustomNestedScrollView(Context context) {
super(context);
}
public CustomNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchHandlerScroll(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
y = (int) e.getY();
startNestedScroll(2);
break;
case MotionEvent.ACTION_MOVE:
int dY = y - ((int)e.getY());
dispatchNestedPreScroll(0, dY, null, null);
dispatchNestedScroll(0, 0, 0, dY, null);
break;
case MotionEvent.ACTION_UP:
stopNestedScroll();
break;
}
return true;
}
}
Then, inside your Activity class, assign a TouchListener to your AppBarLayout:
appBarLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return customNestedScrollView.dispatchHandlerScroll(event);
}
});
and remove it when the AppBar collapses fully for the first time:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Math.abs(appBarLayout.getY()) == appBarLayout.getTotalScrollRange()) {
appBarLayout.setOnTouchListener(null);
}
return super.dispatchTouchEvent(ev);
}
Edit
Take the following steps to get it up and running:
Replace the NestedScrollView in your xml(android.support.v4.widget.NestedScrollView) with the CustomNestedScrollView (which will take the form of com.something.somethingelse.CustomNestedScrollView, depending on where it is in your project).
Assign it to a variable in your Activity onCreate()(i.e. CustomScrollView customScrollView = (CustomScrollView) findViewById(R.id.custom_scroll_view_id);)
Set up the TouchListener on your appBarLayout as you have done in your edit. Now when you call dispatchHandlerScroll(), it will be on your customNestedScrollView instance.
dispatchTouchEvent() is a method you override in your Activity class, so it should be outside the TouchListener
So, for example:
public class MainActivity extends AppCompatActivity {
private AppBarLayout appBarLayout;
private CustomNestedScrollView customNestedScrollView;
//...
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
customNestedScrollView = (CustomNestedScrollView) findViewById(R.id.scroll);
appBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout);
appBarLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return customNestedScrollView.dispatchHandlerScroll(event);
}
});
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Math.abs(appBarLayout.getY()) == appBarLayout.getTotalScrollRange()) {
appBarLayout.setOnTouchListener(null);
}
return super.dispatchTouchEvent(ev);
}
}
Hope that's cleared things up.
Try to add a tiny margin so the white space below will be almost invisible (you might also want to change white to accent color so the space will not be visible)
Another approach is to set the height of app bar layout dynamically by getting the height of the screen.
EDIT:
This might be a focus problem, try to add dummy layout to your main content, that will be focused automatically
<LinearLayout
android:layout_width="0px"
android:layout_height="0px"
android:focusable="true"
android:focusableInTouchMode="true" />
or even just add these attributes to your content layout
android:focusable="true"
android:focusableInTouchMode="true"

scrolling up in webview causes refresh by SwipeRefreshLayout

I have a WebView inside a FrameLayout, to add tabs because i am creating a browser. The FrameLayout is inside a SwipeRefreshLayout.
The problem: Whenever i scroll the content up fast in the WebView, the refresh icon appears from behind the toolbar. It should only happen when the WebView content is at the top. It has something to do with the FrameLayout, when i remove it, the issue is gone.
The layout looks like this:
<SwipeRefreshLayout
android:id="#+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<FrameLayout
android:id="#+id/webViewFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView
android:id="#+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</SwipeRefreshLayout>
Just implement your Fragment or Activity with ViewTreeObserver.OnScrollChangedListener then set Listener like webview.getViewTreeObserver().addOnScrollChangedListener(this);
In onScrollChanged() method do like this
#Override
public void onScrollChanged() {
if (webview.getScrollY() == 0) {
swipeLayout.setEnabled(true);
} else {
swipeLayout.setEnabled(false);
}
}
No Solution worked for me. Here is my approach.
#Override
public boolean dispatchTouchEvent(MotionEvent event)
{
super.dispatchTouchEvent(event);
int x = (int)event.getX();
int y = (int)event.getY();
if(y>800){
swipeRefreshLayout.setEnabled(false);
}else {
swipeRefreshLayout.setEnabled(true);
}
return false;
}

How do the animation hiding the ActionBar and keeping tabs?

In version 5 of Google Play Store app, scroll to the content, ActionBar on with scrolling, but the tabs are fixed to get on top.
How to do this?
BEFORE SCROLL
AFTER SCROLL
As others have suggested, use ObservableScrollView from: https://github.com/ksoichiro/Android-ObservableScrollView
Try putting both the Toolbar and the SlidingTabStrip in the same container, then animate that container as the user scrolls the ObservableScrollView, for example:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.github.ksoichiro.android.observablescrollview.ObservableListView
android:id="#+id/listView"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
<LinearLayout
android:id="#+id/toolbarContainer"
android:orientation="vertical"
android:elevation="10dp"
android:background="#color/material_deep_teal_200"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
<!--Placeholder view, your tabstrip goes here-->
<View
android:layout_width="wrap_content"
android:layout_height="48dp"/>
</LinearLayout>
</FrameLayout>
Then when you override the ObservableScrollViewCallbacks you could do something like this:
#Override
public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
toolbarContainer.animate().cancel();
int scrollDelta = scrollY - oldScrollY;
oldScrollY = scrollY;
float currentYTranslation = -toolbarContainer.getTranslationY();
float targetYTranslation = Math.min(Math.max(currentYTranslation + scrollDelta, 0), toolbarHeight);
toolbarContainer.setTranslationY(-targetYTranslation);
}
#Override
public void onUpOrCancelMotionEvent(ScrollState scrollState) {
float currentYTranslation = -toolbarContainer.getTranslationY();
int currentScroll = listView.getCurrentScrollY();
if (currentScroll < toolbarHeight) {
toolbarContainer.animate().translationY(0);
} else if (currentYTranslation > toolbarHeight /2) {
toolbarContainer.animate().translationY(-toolbarHeight);
} else {
toolbarContainer.animate().translationY(0);
}
}
The onUpOrCancelMotionEvent stuff is to animate the container to prevent the toolbar from being only half shown/hidden.
Here's a demo video just for reference: https://drive.google.com/file/d/0B7TH7VeIpgSQSzZER1NneWpYa1E/view?usp=sharing
Answer is here:
https://github.com/ksoichiro/Android-ObservableScrollView :D
This library is excellent for my case and very others
Great that you answer your question by yourself ;)
Here is another small hint:
Use a seperated layout for your tabs or integrate them into your toolbar and then tranlsate the toolbar only as far as you can see the tabs on top.

Categories

Resources