Don't collapse Toolbar when RecyclerView fits the screen - android

I've made an app using Android Design Library, with a Toolbar and TabLayout. Actually 2 tabs are present, both with 2 RecyclerView, that automatically collapse the Toolbar when scrolled.
My question is: can I disable Toolbar collapsing when RecyclerView has few items and completely fits the screen (like in TAB 2)?
I've seen a lot of examples like CheeseSquare, made by a Google employee where the issue is still present: even if the RecyclerView has just 1 item, the toolbar keeps hiding on scroll.
I think I can just find out if the first item of the RecyclerView is visible on screen and if yes disable toolbar collapsing. The former is easy to implement, what about the latter?
This is my layout:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
android:background="?attr/colorPrimary"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"/>
<android.support.design.widget.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/glucosio_pink"
app:tabSelectedTextColor="#android:color/white"
app:tabIndicatorColor="#color/glucosio_accent"
app:tabTextColor="#80ffffff"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="#+id/pager"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/main_fab"
android:layout_margin="16dp"
android:onClick="onFabClicked"
app:backgroundTint="#color/glucosio_accent"
android:src="#drawable/ic_add_black_24dp"
android:layout_gravity="bottom|right"
/>
</android.support.design.widget.CoordinatorLayout>

Final Solution (thanks MichaƂ Z.)
Methods to turn off/on Toolbar scrolling:
public void turnOffToolbarScrolling() {
Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);
//turn off scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mToolbar.setLayoutParams(toolbarLayoutParams);
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(null);
appBarLayout.setLayoutParams(appBarLayoutParams);
}
public void turnOnToolbarScrolling() {
Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar_layout);
//turn on scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
mToolbar.setLayoutParams(toolbarLayoutParams);
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(new AppBarLayout.Behavior());
appBarLayout.setLayoutParams(appBarLayoutParams);
}
Find out if last item of RecyclerView is visible in my Fragment.
If yes, disable scrolling:
public void updateToolbarBehaviour(){
if (mLayoutManager.findLastCompletelyVisibleItemPosition() == items.size()-1) {
((MainActivity) getActivity()).turnOffToolbarScrolling();
} else {
((MainActivity)getActivity()).turnOnToolbarScrolling();
}
}

RecyclerView now (since version 23.2) supports wrap_content. Just use wrap_content as the height.

You can check if the last item in RecyclerView is visible. If it's not then turn off scrolling programmatically using this method:
//turn off scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mToolbar.setLayoutParams(toolbarLayoutParams);
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(null);
appBarLayout.setLayoutParams(appBarLayoutParams);

I took a slightly different approach to solve this.
I created a custom AppBarBehavior that disables its self based on cells.
public class CustomAppBarBehavior extends AppBarLayout.Behavior {
private RecyclerView recyclerView;
private boolean enabled;
public CustomAppBarBehavior() {
}
public CustomAppBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
updatedEnabled();
return enabled && super.onInterceptTouchEvent(parent, child, ev);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
return enabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
#Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
return enabled && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
private void updatedEnabled() {
enabled = false;
if(recyclerView != null) {
RecyclerView.Adapter adapter = recyclerView.getAdapter();
if (adapter != null) {
int count = adapter.getItemCount();
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager != null) {
int lastItem = 0;
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
lastItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);
lastItem = Math.abs(lastItems[lastItems.length - 1]);
}
enabled = lastItem < count - 1;
}
}
}
}
public void setRecyclerView(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
}
}
Then set the custom behavior on the app bar layout
appBarBehavior = new CustomAppBarBehavior();
CoordinatorLayout.LayoutParams appBarLayoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
appBarLayoutParams.setBehavior(appBarBehavior);
appBarLayout.setLayoutParams(appBarLayoutParams);
Last on page change of the view pager updated the RecyclerView on the behavior
private ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
#Override
public void onPageSelected(final int position) {
appBarLayout.setExpanded(true, true);
appBarLayout.post(new Runnable() {
#Override
public void run() {
appBarBehavior.setRecyclerView(childFragments.get(position).getRecyclerView());
}
});
}
#Override
public void onPageScrollStateChanged(int state) { }
};
This should work with changing datasets.

Add this code after you change data in your adapter:
recyclerView.afterMeasured {
val isTurnedOff = recyclerView.turnOffNestedScrollingIfEnoughItems()
if (isTurnedOff) appBarLayout.setExpanded(true)
}
And this is the functions:
inline fun <T: View> T.afterMeasured(crossinline action: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
action()
}
})
}
fun RecyclerView.turnOffNestedScrollingIfEnoughItems(): Boolean {
val lm = (layoutManager as LinearLayoutManager)
val count = if (lm.itemCount <= 0) 0 else lm.itemCount - 1
val isFirstVisible = lm.findFirstCompletelyVisibleItemPosition() == 0
val isLastItemVisible = lm.findLastCompletelyVisibleItemPosition() == count
isNestedScrollingEnabled = !(isLastItemVisible && isFirstVisible)
return isNestedScrollingEnabled.not()
}

I guess it's the best solution.
You need to define your custom AppBarLayout behavior:
class CustomScrollingViewBehavior : AppBarLayout.Behavior {
constructor() : super()
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
var shouldScroll = true
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
return shouldScroll && when (target) {
is NestedScrollView, is ScrollView, is RecyclerView -> {
return target.canScrollVertically(1) || target.canScrollVertically(-1)
}
else -> super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
}
}
}
And then you just need to use it in your layout as attribute of AppBarLayout:
...
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="56dp"
app:layout_behavior=".CustomScrollingViewBehavior">
...
That's it.
Note: custom behavior also supports full disabling of the scrolling - you just need to set shouldScroll flag to false
customScrollingViewBehavior.shouldScroll = false

If helps anyome in Kotlin based on correct answer, I did this for Kotlin:
fun changeToolbarScroll(isToScrolling: Boolean){
val params = toolbar.layoutParams as AppBarLayout.LayoutParams
val appBarLayoutParams = appBarLayout.layoutParams as
CoordinatorLayout.LayoutParams
params.scrollFlags = 0
toolbar.layoutParams = params
appBarLayoutParams.behavior = null
appBarLayout.layoutParams = appBarLayoutParams
if(isToScrolling){
params.scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
toolbar.layoutParams = params
appBarLayoutParams.behavior = AppBarLayout.Behavior()
appBarLayout.layoutParams = appBarLayoutParams
}
}
In my case, I have a problem with a MainActivity that's manage navigation, toolbar and other shared things by 2 Fragments, the first Framgent use a RecyclerView and second is for show the deatil.
The problems is when I set a Menu and change MenuItem from MainAcitivity
It can sound silly and totally logical but remember Always make chages to the Menu or MenuItems before call supportFragmentManager.beginTransaction() when change fragments otherwise don't work, or dont update properly, no matter changes in .add, .replace(), show()...
fun showDetailImageFragment(searchImage: SearchImage) {
val searchFragment =
supportFragmentManager.findFragmentByTag(SEARCH_IMAGES)
changeToolbarScroll(false)
if (supportActionBar != null) {
supportActionBar!!.collapseActionView()
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.title = getString(R.string.detail_image_title)
}
actionSearch.isVisible = false
actionNighMOde.isVisible = false
actionAppSetings.isVisible = false
actionAbout.isVisible = false
supportFragmentManager.beginTransaction()
.setCustomAnimations(
R.animator.fade_in,
R.animator.fade_out,
R.animator.fade_in,
R.animator.fade_out
)
.hide(searchFragment!!)
.add(
R.id.frameLayout,
DetailImageFragment().newInstance(searchImage)
).addToBackStack(null)
.commit()
}

Just remove scroll from
app:layout_scrollFlags="scroll|enterAlways"
So it should be
app:layout_scrollFlags="enterAlways"

You can add within your XML, the property layout_behaviour with value #string/appbar_scrolling_view_behavior this way:
app:layout_behavior="#string/appbar_scrolling_view_behavior"

//turn off scrolling
AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
toolbarLayoutParams.setScrollFlags(0);
mToolbar.setLayoutParams(toolbarLayoutParams);

Related

How to avoid CollapsingToolbarLayout not being snapped or being "wobbly" when scrolling?

Background
Suppose you have an app you've created that has a similar UI as the one you can create via the wizard of "scrolling activity", yet you wish the scrolling flags to have snapping, as such:
<android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >
The problem
As it turns out, on many cases it has issues of snapping. Sometimes the UI doesn't snap to top/bottom, making the CollapsingToolbarLayout stay in between.
Sometimes it also tries to snap to one direction, and then decides to snap to the other .
You can see both issues on the attached video here.
What I've tried
I thought it's one of the issues that I got for when I use setNestedScrollingEnabled(false) on a RecyclerView within, so I asked about it here, but then I noticed that even with the solution and without using this command at all and even when using a simple NestedScrollView (as is created by the wizard), I can still notice this behavior.
That's why I decided to report about this as an issue, here.
Sadly, I couldn't find any workaround for those weird bugs here on StackOverflow.
The question
Why does it occur, and more importantly: how can I avoid those issues while still using the behavior it's supposed to have?
EDIT: here's a nice improved Kotlin version of the accepted answer:
class RecyclerViewEx #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
private var mAppBarTracking: AppBarTracking? = null
private var mView: View? = null
private var mTopPos: Int = 0
private var mLayoutManager: LinearLayoutManager? = null
interface AppBarTracking {
fun isAppBarIdle(): Boolean
fun isAppBarExpanded(): Boolean
}
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
if (mAppBarTracking == null)
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
&& isNestedScrollingEnabled) {
if (dy > 0) {
if (mAppBarTracking!!.isAppBarExpanded()) {
consumed!![1] = dy
return true
}
} else {
mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
if (mTopPos == 0) {
mView = mLayoutManager!!.findViewByPosition(mTopPos)
if (-mView!!.top + dy <= 0) {
consumed!![1] = dy - mView!!.top
return true
}
}
}
}
if (dy < 0 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
consumed!![1] = dy
return true
}
val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
offsetInWindow[1] = 0
return returnValue
}
override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
super.setLayoutManager(layout)
mLayoutManager = layoutManager as LinearLayoutManager
}
fun setAppBarTracking(appBarTracking: AppBarTracking) {
mAppBarTracking = appBarTracking
}
fun setAppBarTracking(appBarLayout: AppBarLayout) {
val appBarIdle = AtomicBoolean(true)
val appBarExpanded = AtomicBoolean()
appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
private var mAppBarOffset = Integer.MIN_VALUE
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (mAppBarOffset == verticalOffset)
return
mAppBarOffset = verticalOffset
appBarExpanded.set(verticalOffset == 0)
appBarIdle.set(mAppBarOffset >= 0 || mAppBarOffset <= -appBarLayout.totalScrollRange)
}
})
setAppBarTracking(object : AppBarTracking {
override fun isAppBarIdle(): Boolean = appBarIdle.get()
override fun isAppBarExpanded(): Boolean = appBarExpanded.get()
})
}
override fun fling(velocityX: Int, inputVelocityY: Int): Boolean {
var velocityY = inputVelocityY
if (mAppBarTracking != null && !mAppBarTracking!!.isAppBarIdle()) {
val vc = ViewConfiguration.get(context)
velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
else vc.scaledMinimumFlingVelocity
}
return super.fling(velocityX, velocityY)
}
}
Update
I have changed the code slightly to address remaining issues - at least the ones that I can reproduce. The key update was to dispose of dy only when the AppBar is expanded or collapsed. In the first iteration, dispatchNestedPreScroll() was disposing of scroll without checking the status of the AppBar for a collapsed state.
Other changes are minor and fall under the category of clean up. The code blocks are updated below.
This answer addresses the question's issue regarding RecyclerView. The other answer I have given still stands and applies here. RecyclerView has the same issues as NestedScrollView that were introduced in 26.0.0-beta2 of the support libraries.
The code below is base upon this answer to a related question but includes the fix for the erratic behavior of the AppBar. I have removed the code that fixed the odd scrolling because it no longer seems to be needed.
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
MyRecyclerView.java
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context context) {
this(context, null);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private AppBarTracking mAppBarTracking;
private View mView;
private int mTopPos;
private LinearLayoutManager mLayoutManager;
#Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// App bar latching trouble is only with this type of movement when app bar is expanded
// or collapsed. In touch mode, everything is OK regardless of the open/closed status
// of the app bar.
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
&& isNestedScrollingEnabled()) {
// Make sure the AppBar stays expanded when it should.
if (dy > 0) { // swiped up
if (mAppBarTracking.isAppBarExpanded()) {
// Appbar can only leave its expanded state under the power of touch...
consumed[1] = dy;
return true;
}
} else { // swiped down (or no change)
// Make sure the AppBar stays collapsed when it should.
// Only dy < 0 will open the AppBar. Stop it from opening by consuming dy if needed.
mTopPos = mLayoutManager.findFirstVisibleItemPosition();
if (mTopPos == 0) {
mView = mLayoutManager.findViewByPosition(mTopPos);
if (-mView.getTop() + dy <= 0) {
// Scroll until scroll position = 0 and AppBar is still collapsed.
consumed[1] = dy - mView.getTop();
return true;
}
}
}
}
boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
// Fix the scrolling problems when scrolling is disabled. This issue existed prior
// to 26.0.0-beta2.
if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
offsetInWindow[1] = 0;
}
return returnValue;
}
#Override
public void setLayoutManager(RecyclerView.LayoutManager layout) {
super.setLayoutManager(layout);
mLayoutManager = (LinearLayoutManager) getLayoutManager();
}
public void setAppBarTracking(AppBarTracking appBarTracking) {
mAppBarTracking = appBarTracking;
}
#SuppressWarnings("unused")
private static final String TAG = "MyRecyclerView";
}
ScrollingActivity.java
public class ScrollingActivity extends AppCompatActivity
implements AppBarTracking {
private MyRecyclerView mNestedView;
private int mAppBarOffset;
private boolean mAppBarIdle = false;
private int mAppBarMaxOffset;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mNestedView = findViewById(R.id.nestedView);
final AppBarLayout appBar = findViewById(R.id.app_bar);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
// mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
// mAppBarOffset = mAppBarMaxOffset
// mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
// mAppBarOffset should never be > zero or less than mAppBarMaxOffset
mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
}
});
appBar.post(new Runnable() {
#Override
public void run() {
mAppBarMaxOffset = -appBar.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.setAppBarTracking(this);
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)) {
};
}
#SuppressLint("SetTextI18n")
#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);
}
#Override
public boolean isAppBarExpanded() {
return mAppBarOffset == 0;
}
#Override
public boolean isAppBarIdle() {
return mAppBarIdle;
}
#SuppressWarnings("unused")
private static final String TAG = "ScrollingActivity";
}
What is happening here?
From the question, it was apparent that the layout was failing to snap the app bar closed or open as it should when the user's finger was not on the screen. When dragging, the app bar behaves as it should.
In version 26.0.0-beta2, some new methods were introduced - specifically dispatchNestedPreScroll() with a new type argument. The type argument specifies if the movement specified by dx and dy are due to the user touching the screen ViewCompat.TYPE_TOUCH or not ViewCompat.TYPE_NON_TOUCH.
Although the specific code that causes the problem was not identified, the tack of the fix is to kill vertical movement in dispatchNestedPreScroll() (dispose of dy) when needed by not letting vertical movement propagate. In effect, the app bar is to be latched into place when expanded and will not allowed to start to close until it is closing through a touch gesture. The app bar will also be latched when closed until the RecyclerView is positioned at its topmost extent and there is sufficient dy to open the app bar while performing a touch gesture.
So, this is not so much a fix as much as a discouragement of problematic conditions.
The last part of the MyRecyclerView code deals with an issue that was identified in this question dealing with improper scroll movements when nested scrolling is disabled. This is the part that comes after the call to the super of dispatchNestedPreScroll() that changes the value of offsetInWindow[1]. The thinking behind this code is the same as presented in the accepted answer for the question. The only difference is that since the underlying nested scrolling code has changed, the argument offsetInWindow is sometime null. Fortunately, it seems to be non-null when it matters, so the last part continues to work.
The caveat is that this "fix" is very specific to the question asked and is not a general solution. The fix will likely have a very short shelf life since I expect that such an obvious problem will be addressed shortly.
Looks like onStartNestedScroll and onStopNestedScroll calls can be reordered and it lead to "wobbly" snap. I made a small hack inside AppBarLayout.Behavior. Don't really want to mess up with all that stuff in activity as proposed by other answers.
#SuppressWarnings("unused")
public class ExtAppBarLayoutBehavior extends AppBarLayout.Behavior {
private int mStartedScrollType = -1;
private boolean mSkipNextStop;
public ExtAppBarLayoutBehavior() {
super();
}
public ExtAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
if (mStartedScrollType != -1) {
onStopNestedScroll(parent, child, target, mStartedScrollType);
mSkipNextStop = true;
}
mStartedScrollType = type;
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
#Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
if (mSkipNextStop) {
mSkipNextStop = false;
return;
}
if (mStartedScrollType == -1) {
return;
}
mStartedScrollType = -1;
// Always pass TYPE_TOUCH, because want to snap even after fling
super.onStopNestedScroll(coordinatorLayout, abl, target, ViewCompat.TYPE_TOUCH);
}
}
Usage in XML layout:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.example.ExtAppBarLayoutBehavior">
<!-- Put here everything you usually add to AppBarLayout: CollapsingToolbarLayout, etc... -->
</android.support.design.widget.AppBarLayout>
<!-- Content: recycler for example -->
<android.support.v7.widget.RecyclerView
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
...
</android.support.design.widget.CoordinatorLayout>
It is very likely that the root cause of the problem in the RecyclerView. Do not have an opportunity to dig deeper now.
Edit The code has been updated to bring it more in line with the code for the accepted answer. This answer concerns NestedScrollView while the accepted answer is about RecyclerView.
This is an issue what was introduced in the API 26.0.0-beta2 release. It does not happen on the beta 1 release or with API 25. As you noted, it also happens with API 26.0.0. Generally, the problem seems to be related to how flings and nested scrolling are handled in beta2. There was a major rewrite of nested scrolling (see "Carry on Scrolling"), so it is not surprising that this type of issue has cropped up.
My thinking is that excess scroll is not being disposed of properly somewhere in NestedScrollView. The work-around is to quietly consume certain scrolls that are "non-touch" scrolls (type == ViewCompat.TYPE_NON_TOUCH) when the AppBar is expanded or collapsed. This stops the bouncing, allows snaps and, generally, makes the AppBar better behaved.
ScrollingActivity has been modified to track the status of the AppBar to report whether it is expanded or not. A new class call "MyNestedScrollView" overrides dispatchNestedPreScroll() (the new one, see here) to manipulate the consumption of the excess scroll.
The following code should suffice to stop AppBarLayout from wobbling and refusing to snap. (XML will also have to change to accommodate MyNestedSrollView. The following only applies to support lib 26.0.0-beta2 and above.)
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
ScrollingActivity.java
public class ScrollingActivity extends AppCompatActivity implements AppBarTracking {
private int mAppBarOffset;
private int mAppBarMaxOffset;
private MyNestedScrollView mNestedView;
private boolean mAppBarIdle = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
AppBarLayout appBar;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
final Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
appBar = findViewById(R.id.app_bar);
mNestedView = findViewById(R.id.nestedScrollView);
mNestedView.setAppBarTracking(this);
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
}
});
appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
// mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
// mAppBarOffset = mAppBarMaxOffset
// mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
// mAppBarOffset should never be > zero or less than mAppBarMaxOffset
mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
}
});
mNestedView.post(new Runnable() {
#Override
public void run() {
mAppBarMaxOffset = mNestedView.getMaxScrollAmount();
}
});
}
#Override
public boolean isAppBarIdle() {
return mAppBarIdle;
}
#Override
public boolean isAppBarExpanded() {
return mAppBarOffset == 0;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_scrolling, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
#SuppressWarnings("unused")
private static final String TAG = "ScrollingActivity";
}
MyNestedScrollView.java
public class MyNestedScrollView extends NestedScrollView {
public MyNestedScrollView(Context context) {
this(context, null);
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setOnScrollChangeListener(new View.OnScrollChangeListener() {
#Override
public void onScrollChange(View view, int x, int y, int oldx, int oldy) {
mScrollPosition = y;
}
});
}
private AppBarTracking mAppBarTracking;
private int mScrollPosition;
#Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
// App bar latching trouble is only with this type of movement when app bar is expanded
// or collapsed. In touch mode, everything is OK regardless of the open/closed status
// of the app bar.
if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
&& isNestedScrollingEnabled()) {
// Make sure the AppBar stays expanded when it should.
if (dy > 0) { // swiped up
if (mAppBarTracking.isAppBarExpanded()) {
// Appbar can only leave its expanded state under the power of touch...
consumed[1] = dy;
return true;
}
} else { // swiped down (or no change)
// Make sure the AppBar stays collapsed when it should.
if (mScrollPosition + dy < 0) {
// Scroll until scroll position = 0 and AppBar is still collapsed.
consumed[1] = dy + mScrollPosition;
return true;
}
}
}
boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
// Fix the scrolling problems when scrolling is disabled. This issue existed prior
// to 26.0.0-beta2. (Not sure that this is a problem for 26.0.0-beta2 and later.)
if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
Log.d(TAG, "<<<<offsetInWindow[1] forced to zero");
offsetInWindow[1] = 0;
}
return returnValue;
}
public void setAppBarTracking(AppBarTracking appBarTracking) {
mAppBarTracking = appBarTracking;
}
#SuppressWarnings("unused")
private static final String TAG = "MyNestedScrollView";
}
Since the issue is still not fixed as of February 2020 (latest material library version is 1.2.0-alpha5) I want to share my solution to the buggy AppBar animation.
The idea is to implmenet custom snapping logic by extending AppBarLayout.Behavior (Kotlin version):
package com.example
import android.content.Context
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams
#Suppress("unused")
class AppBarBehaviorFixed(context: Context?, attrs: AttributeSet?) :
AppBarLayout.Behavior(context, attrs) {
private var view: AppBarLayout? = null
private var snapEnabled = false
private var isUpdating = false
private var isScrolling = false
private var isTouching = false
private var lastOffset = 0
private val handler = Handler()
private val snapAction = Runnable {
val view = view ?: return#Runnable
val offset = -lastOffset
val height = view.run { height - paddingTop - paddingBottom - getChildAt(0).minimumHeight }
if (offset > 1 && offset < height - 1) view.setExpanded(offset < height / 2)
}
private val updateFinishDetector = Runnable {
isUpdating = false
scheduleSnapping()
}
private fun initView(view: AppBarLayout) {
if (this.view != null) return
this.view = view
// Checking "snap" flag existence (applied through child view) and removing it
val child = view.getChildAt(0)
val params = child.layoutParams as LayoutParams
snapEnabled = params.scrollFlags hasFlag LayoutParams.SCROLL_FLAG_SNAP
params.scrollFlags = params.scrollFlags removeFlag LayoutParams.SCROLL_FLAG_SNAP
child.layoutParams = params
// Listening for offset changes
view.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, offset ->
lastOffset = offset
isUpdating = true
scheduleSnapping()
handler.removeCallbacks(updateFinishDetector)
handler.postDelayed(updateFinishDetector, 50L)
})
}
private fun scheduleSnapping() {
handler.removeCallbacks(snapAction)
if (snapEnabled && !isUpdating && !isScrolling && !isTouching) {
handler.postDelayed(snapAction, 50L)
}
}
override fun onLayoutChild(
parent: CoordinatorLayout,
abl: AppBarLayout,
layoutDirection: Int
): Boolean {
initView(abl)
return super.onLayoutChild(parent, abl, layoutDirection)
}
override fun onTouchEvent(
parent: CoordinatorLayout,
child: AppBarLayout,
ev: MotionEvent
): Boolean {
isTouching =
ev.actionMasked != MotionEvent.ACTION_UP && ev.actionMasked != MotionEvent.ACTION_CANCEL
scheduleSnapping()
return super.onTouchEvent(parent, child, ev)
}
override fun onStartNestedScroll(
parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int
): Boolean {
val started = super.onStartNestedScroll(
parent, child, directTargetChild, target, nestedScrollAxes, type
)
if (started) {
isScrolling = true
scheduleSnapping()
}
return started
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
abl: AppBarLayout,
target: View,
type: Int
) {
isScrolling = false
scheduleSnapping()
super.onStopNestedScroll(coordinatorLayout, abl, target, type)
}
private infix fun Int.hasFlag(flag: Int) = flag and this == flag
private infix fun Int.removeFlag(flag: Int) = this and flag.inv()
}
And now apply this behavior to the AppBarLayout in xml:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.example.AppBarBehaviorFixed">
<com.google.android.material.appbar.CollapsingToolbarLayout
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<!-- Toolbar declaration -->
</com.google.android.material.appbar.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<!-- Scrolling view (RecyclerView, NestedScrollView) -->
</android.support.design.widget.CoordinatorLayout>
That is still a hack but it seems to work quite well, and it does not require to put dirty code into your activity or extend RecyclerView and NestedScrollView widgets (thanks to #vyndor for this idea).

How to sync scrolling of all horizontal nested RecyclerViews within a vertical RecyclerViews?

Background
Suppose I have a vertical RecyclerView, where each row is a horizontal RecyclerView.
What I'd like to do is that no matter which horizontal RecyclerViews you scroll, all of the others will scroll accordingly, and always be synced with the exact same scroll X coordinate
The problem
I actually did ok for the basic operation :
It works by having a scrolling listener that all horizontal RecyclerViews have, yet when one starts to scroll, it is the only one that will have it, while it also affects the others to scroll with it.
However, I have 2 main issues with what I did:
In some (horizontal) scrolling operations (maybe some gestures, like fling), the scrolling of the multiple RecyclerViews is out of sync, so some are in X coordinate that is different from the others.
When scrolling vertically, I couldn't succeed setting the X coordinate correctly. Not only that, but onBindViewHolder of the vertical RecyclerView doesn't get called when I expected it to be called (called when I scroll a lot, and not just when I see a used one being re-shown).
What I've tried
Here's the current code:
MainActivity.java
public class MainActivity extends AppCompatActivity {
int mCurX = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final RecyclerView mainRecyclerView = (RecyclerView) findViewById(R.id.activity_main);
final LinearLayoutManager verticalLinearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mainRecyclerView.setLayoutManager(verticalLinearLayoutManager);
final LayoutInflater layoutInflater = LayoutInflater.from(this);
final OnScrollListener masterOnScrollListener = new OnScrollListener() {
RecyclerView masterRecyclerView = null;
#Override
public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
if (masterRecyclerView != null) {
masterRecyclerView = null;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.addOnScrollListener(this);
}
}
break;
case RecyclerView.SCROLL_STATE_SETTLING:
//TODO fix out-of-sync scrolling issues, probably here
case RecyclerView.SCROLL_STATE_DRAGGING:
if (masterRecyclerView == null) {
masterRecyclerView = recyclerView;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.removeOnScrollListener(this);
}
}
}
}
#Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
mCurX += dx;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.scrollBy(dx, dy);
}
}
};
mainRecyclerView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
RecyclerView horizontalRecyclerView = (RecyclerView) layoutInflater.inflate(R.layout.horizontal_recycler_view, parent, false);
horizontalRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this, LinearLayoutManager.HORIZONTAL, false));
horizontalRecyclerView.addOnScrollListener(masterOnScrollListener);
final ViewHolder horizontalViewHolder = new ViewHolder(horizontalRecyclerView) {
};
horizontalRecyclerView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(layoutInflater.inflate(R.layout.single_item, parent, false)) {
};
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
((TextView) holder.itemView).setText("horizontalRecyclerView:" + horizontalViewHolder.getAdapterPosition() + "\nitem:" + position);
}
#Override
public int getItemCount() {
return 100;
}
});
return horizontalViewHolder;
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
//TODO check why this isn't called for some cases
RecyclerView recyclerView = (RecyclerView) holder.itemView;
recyclerView.removeOnScrollListener(masterOnScrollListener);
//TODO scroll to correct location here. The below code doesn't seem to work at all
recyclerView.scrollToPosition(0);
recyclerView.scrollBy(mCurX,0);
recyclerView.addOnScrollListener(masterOnScrollListener);
recyclerView.getAdapter().notifyDataSetChanged();
}
#Override
public int getItemCount() {
return 40;
}
});
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
android:id="#+id/activity_main"
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"
tools:context="lb.com.nestedallscrollingrecyclerviewtest.MainActivity"/>
horizontal_recycler_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"/>
single_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"/>
The questions
What is wrong in the code that causes it to be out-of-scrolling sync?
Is it possible I also don't get a hold of all the RecyclerViews that I should?
How come the onBindViewHolder of the vertical RecyclerView doesn't get called when I expect it to?
How do I set the x-coordinate scrolling of a horizontal RecyclerView to be as the others, in onBindViewHolder of the vertical one?
I'm not sure if this could be a problem, but what should I do in case each item in each horizontal RecyclerView could be with a different width than the others ?
A bit late to the party but just putting it here in case anyone else stumbles upon the same issue. Please Note that this solution is written in Kotlin and you might have to convert it to Java if that is your language of choice.
Solution
There are a couple of issues that need to be taken into account.
Synchronise scrolling of horizontal recycler views
Retain offset when scrolling vertical recycler view
Add this code in the Adapter for your vertical recycler view
var horizontalRecyclerViews = mutableListOf<RecyclerView>()
var absoluteOffset: Int? = null //Used to solve issue number 2
// matchOffset is used to synchronise the offset of each horizontal recyclerview.
// It is called when a horizontal recyclerview is scrolled with that recyclerview's
// offset. It is also called when the vertical recycler view is scrolled but without
// an offset value (in which case, it uses the absoluteOffset which is set when
// the horizontal scrolling is stopped)
fun matchOffset(offset: Int? = absoluteOffset) {
offset?.let { offsetValue ->
horizontalRecyclerViews.forEach { recyclerView ->
val currentOffset = recyclerView.computeHorizontalScrollOffset()
if (offsetValue != currentOffset) {
recyclerView.scrollBy(offsetValue-currentOffset, 0)
}
}
}
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
...
...
...
val onTouchListener = object: RecyclerView.OnItemTouchListener {
override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {
}
override fun onInterceptTouchEvent(p0: RecyclerView, p1: MotionEvent): Boolean {
if (p1.action == MotionEvent.ACTION_UP) {
// This value is used by the vertical recycler view
absoluteOffset = p0.computeHorizontalScrollOffset()
// Disable the fling scroll to make life easier
return true
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
}
}
val onScrollListener = object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val value = recyclerView.computeHorizontalScrollOffset()
matchOffset(value)
}
}
...
...
...
//Clear scroll listeners on each bind to stop them from accumulating
horizontalRecyclerView.clearOnScrollListeners()
//Add touch and scroll listeners to horizontalRecyclerView
horizontalRecyclerView.addOnItemTouchListener(onTouchListener)
horizontalRecyclerView.addOnScrollListener(onScrollListener)
//Add each horizontal recyclerView into the mutableList
horizontalRecyclerViews.add(horizontalRecyclerView)
...
...
...
}
To wrap it up for the resolution of issue number 2, add the following scroll listener to your vertical recycler view
val onScrollListener = object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
//Cast the Adapter to access the matchOffset method
(recyclerView.adapter as? Adapter)?.matchOffset()
}
}
verticalRecyclerView.addOnScrollListener(onScrollListener)

Hide FAB in NestedScrollView when scrolling

I am having a nestedscrollview with content like some linearlayouts and textviews.
I am using a floatingactionbutton library for some reasons, as well. So I can't use any behavior for it.
I don't know how I should handle the scrollchangelistener from scrollview to hide and show the fab dynamically like a behavior.
Any suggestions how to hide and show the fab while scrolling?
Simple add this code below to your NestedScrollView ScrollChangeListener:
NestedScrollView nsv = v.findViewById(R.id.nsv);
nsv.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (scrollY > oldScrollY) {
fab.hide();
} else {
fab.show();
}
}
});
Create FabScrollBehavior class
public class FabScrollBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
private int toolbarHeight;
public FabScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
this.toolbarHeight = AppUtil.getToolbarHeight(context);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton fab, View dependency) {
return dependency instanceof AppBarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton fab, View dependency) {
if (dependency instanceof AppBarLayout) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
int distanceToScroll = fab.getHeight() + fabBottomMargin;
float ratio = (float)dependency.getY()/(float)toolbarHeight;
fab.setTranslationY(-distanceToScroll * ratio);
}
return true;
}
}
Where AppUtil.getToolbarHeight(context) is -
public static int getToolbarHeight(Context context) {
final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.actionBarSize});
int toolbarHeight = (int) styledAttributes.getDimension(0, 0);
styledAttributes.recycle();
return toolbarHeight;
}
then in your layout add to FloatingActionButton layout_behavior:
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab_task_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="#dimen/fab_margin"
android:src="#drawable/ic_accepted"
app:layout_behavior="pass.to.your.FabScrollBehavior.Class"
app:theme="#style/Widget.AppTheme.Fab"/>
The whole layout looks like
<?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"
android:animateLayoutChanges="true"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/Widget.AppTheme.AppBarOverlay">
<include
layout="#layout/include_layout_toolbar_scroll"/>
</android.support.design.widget.AppBarLayout>
<include layout="#layout/include_layout_content_with_nestedscroll"/>
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab_task_accept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="#dimen/fab_margin"
android:src="#drawable/ic_accepted"
app:layout_behavior="pass.to.FabScrollBehavior.Class"
app:theme="#style/Widget.AppTheme.Fab"/>
</android.support.design.widget.CoordinatorLayout>
Implemented from https://mzgreen.github.io/2015/02/15/How-to-hideshow-Toolbar-when-list-is-scroling(part1)/
define variable type int in your Activity or fragment to set previous Scroll from ScrollView then use this method to listen change scroll in ScrollView Class
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
// previousScrollY this variable is define in your Activity or Fragment
if (scrollView.getScrollY() > previousScrollY && floatingActionButton.getVisibility() == View.VISIBLE) {
floatingActionButton.hide();
} else if (scrollView.getScrollY() < previousScrollY && floatingActionButton.getVisibility() != View.VISIBLE) {
floatingActionButton.show();
}
previousScrollY = scrollView.getScrollY();
}
});
will work done all version of android
After spending such time i have found the solution for it.
It may work in all situations. Though it is a hack not the proper solution but you can apply it to make this thing work.
As we know setOnScrollChangeListener will only work if minimum api 23, so what if my minimum api level is less then 23.
So I found out solution from stack overflow that we can use getViewTreeObserver().addOnScrollChangedListener for that so this will be compatible solution for all devices.
Now let's move to the final solution of over problem "Hide fab button when nested scroll view scrolling and Show fab button when nested scroll view in ideal state"
So for that we can use Handler with postDelayed to slove this issue.
Define on variable in you context private int previousScrollY = 0;
Then use getViewTreeObserver().addOnScrollChangedListener to your nested scroll view like this.
NESTEDSCROLLVIEW.getViewTreeObserver().addOnScrollChangedListener(new
ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
if (NESTEDSCROLLVIEW.getScrollY() == previousScrollY) {
FABBUTTON.setVisibility(View.VISIBLE);
} else {
FABBUTTON.setVisibility(View.INVISIBLE);
}
}
}, 10);
previousScrollY = NESTEDSCROLLVIEW.getScrollY();
}
});
Now you are ready to go....
You can use this listener to observe and hide FAB when scrolling.
nestedScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
if (nestedScrollView != null) {
if (nestedScrollView.getChildAt(0).getBottom() <= (nestedScrollView.getHeight() + nestedScrollView.getScrollY())) {
fab.setVisibility(View.INVISIBLE);
} else {
fab.setVisibility(View.VISIBLE);
}
}
}
});

How to hide or show the partially hidden toolbar/actionbar when you stop scrolling the listview/recyclerview in android

So to show and hide the action bar when scrolling I am using this method
using coordinator layout and appbarlayout
But when I stop scrolling the list in the middle of toolbar being hidden or shown it stays there and only part of toolbar is visible.
What I want to do is make the toolbar show or hide completely based on the percentage of toolbar visible.
Is there a way I can acheive this using the coordinator layout and appbarlayout?
EDIT - Just get 23.1.0 design library and add "|snap" attribute to your ToolBar layout:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways|snap />
-----
-----
SO YOU DON'T NEED TO USE THE FOLLOWING CODE:
Have you tried to add the layout_behaviour attribute to AppBarLayout?
app:layout_behavior="AppBarLayoutSnapBehavior"
Then you need to create class "AppBarLayoutSnapBehavior":
public class AppBarLayoutSnapBehavior extends AppBarLayout.Behavior {
private ValueAnimator mAnimator;
private boolean mNestedScrollStarted = false;
public AppBarLayoutSnapBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
mNestedScrollStarted = super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
if (mNestedScrollStarted && mAnimator != null) {
mAnimator.cancel();
}
return mNestedScrollStarted;
}
#Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target) {
super.onStopNestedScroll(coordinatorLayout, child, target);
if (!mNestedScrollStarted) {
return;
}
mNestedScrollStarted = false;
int scrollRange = child.getTotalScrollRange();
int topOffset = getTopAndBottomOffset();
if (topOffset <= -scrollRange || topOffset >= 0) {
// Already fully visible or fully invisible
return;
}
if (topOffset < -(scrollRange / 2f)) {
// Snap up (to fully invisible)
animateOffsetTo(-scrollRange);
} else {
// Snap down (to fully visible)
animateOffsetTo(0);
}
}
private void animateOffsetTo(int offset) {
if (mAnimator == null) {
mAnimator = new ValueAnimator();
mAnimator.setInterpolator(new DecelerateInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
setTopAndBottomOffset((int) animation.getAnimatedValue());
}
});
} else {
mAnimator.cancel();
}
mAnimator.setIntValues(getTopAndBottomOffset(), offset);
mAnimator.start();
}
Should work for you.
I've got partial solution. To your recyclerview add onScrollListener bellow, with AppBarLayout in param
public class ImprovedScrollListener extends RecyclerView.OnScrollListener {
private WeakReference<AppBarLayout> mAppBarLayout;
public ImprovedScrollListener(AppBarLayout appBarLayout) {
mAppBarLayout = new WeakReference<>(appBarLayout);
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mAppBarLayout.get() != null) {
if (!recyclerView.canScrollVertically(-1) && !recyclerView.canScrollVertically(1)) {
/// TODO: 21.10.2015 handle this state
}else if(!recyclerView.canScrollVertically(-1)){
mAppBarLayout.get().setExpanded(true, true);
} else {
mAppBarLayout.get().setExpanded(false, true);
}
}
}
}
This will work for recyclerviews that can scroll, this still behaves ugly when recyclerview is not scrollable (see TODO in code). If somebody find solution for this case let me know
Setting Toolbar within AppBarLayout and cordinationLayout will itself take care of hiding and showing the actionBar
<?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:id="#+id/cordinationLayout"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>

Need to disable expand on CollapsingToolbarLayout for certain fragments

I have an AppCompatActivity that controls replacing many fragments. Here is my layout for it.
activity_main.xml
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true">
<include layout="#layout/activity_main_frame"/>
<android.support.design.widget.NavigationView
android:id="#+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
android:background="#color/white"
app:headerLayout="#layout/drawer_header"
app:menu="#menu/drawer"/>
</android.support.v4.widget.DrawerLayout>
activity_main_frame.xml
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true">
<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"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp">
<ImageView
android:id="#+id/backdrop"
android:layout_width="match_parent"
android:layout_height="256dp"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax" />
<include layout="#layout/activity_main_items"/>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin"/>
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="bottom"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" >
</FrameLayout>
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
app:layout_anchor="#id/appbar"
app:layout_anchorGravity="bottom|right|end"
app:borderWidth="0dp"
android:src="#drawable/app_ic_slide_wallpaper_dark"
android:layout_margin="#dimen/big_padding"
android:clickable="true"/>
</android.support.design.widget.CoordinatorLayout>
My home fragment is set initially and that is where I want the collapsing toolbar expanded and that works fine. However when I change fragments from side drawer I want to disable the expanding toolbar.
I have figured out how to collapse it when a drawer item is selected but I also need to make sure it doesn't expand unless the home fragment is displayed. is this possible?
public void collapseToolbar(){
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appbar.getLayoutParams();
behavior = (AppBarLayout.Behavior) params.getBehavior();
if(behavior!=null) {
behavior.onNestedFling(coordinator, appbar, null, 0, 10000, true);
}
}
Disable nested scrolling on the scrolling fragment content:
recyclerView.setNestedScrollingEnabled(false);
Use this if you're using the support library:
ViewCompat.setNestedScrollingEnabled(recyclerView, false);
This class will let you disable/re-enable the expansion behavior.
public class DisableableAppBarLayoutBehavior extends AppBarLayout.Behavior {
private boolean mEnabled;
public DisableableAppBarLayoutBehavior() {
super();
}
public DisableableAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
return mEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
public boolean isEnabled() {
return mEnabled;
}
}
Use it in your layout like so:
<android.support.design.widget.AppBarLayout
... other attributes ...
app:layout_behavior="com.yourpackage.DisableableAppBarLayoutBehavior"
>
<!-- your app bar contents -->
</android.support.design.widget.AppBarLayout>
Then, when you want to disable the behavior:
AppBarLayout myAppBar = ....;
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) myAppBar.getLayoutParams();
((DisableableAppBarLayoutBehavior) layoutParams.getBehavior()).setEnabled(false);
Now, in v23 of support library you can easily control your appbar visibility.
Just get a reference to your AppBarLayout and hide/show it depending on the fragment you want to load:
private AppBarLayout appBarLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
[...]
appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
[...]
}
public void switchToFragment(Fragment fragment, String tag, boolean expandToolbar){
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment currentFragment = fragmentManager.findFragmentByTag(currentFragmentTag);
if(currentFragment == null || !TextUtils.equals(tag, currentFragmentTag) ){
currentFragmentTag = tag;
fragmentManager
.beginTransaction()
.replace(R.id.flContent, fragment, currentFragmentTag)
.commit();
if(expandToolbar){
appBarLayout.setExpanded(true,true);
}else{
appBarLayout.setExpanded(false,true);
}
}
}
P.S. don't forget to add the required dependencies in your build.gradle:
dependencies {
compile 'com.android.support:design:23.2.1'
compile 'com.android.support:appcompat-v7:23.2.1'
compile 'com.android.support:recyclerview-v7:23.2.1'
}
EDIT:
If you also want to lock your toolbar in certain fragments (apart from collapsing) you have to resort to workarounds as this feature is not provided by CollapsingToolbarLayout until now (v23.2.1 of support design). Here you can find my proposed workaround.
All you have to do is replace CoordinatorLayout with custom implementation of CoordinatorLayout which will cheat that nested scrolling has been handled.
MyCoordinatorLayout implementation:
public class MyCoordinatorLayout extends CoordinatorLayout {
private boolean allowForScroll = false;
public MyCoordinatorLayout(Context context) {
super(context);
}
public MyCoordinatorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return allowForScroll && super.onStartNestedScroll(child, target, nestedScrollAxes);
}
public boolean isAllowForScroll() {
return allowForScroll;
}
public void setAllowForScroll(boolean allowForScroll) {
this.allowForScroll = allowForScroll;
}
}
activity view xml:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<android.support.v4.widget.DrawerLayout
android:id="#+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!--CONTENT-->
<com.example.views.MyCoordinatorLayout
android:id="#+id/coordinator"
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:fitsSystemWindows="true"
>
<com.example.views.ControllableAppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="192dp"
android:fitsSystemWindows="true"
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"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginBottom="32dp"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/primary"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<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/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.CollapsingToolbarLayout>
</com.example.views.ControllableAppBarLayout>
<FrameLayout
android:id="#+id/flContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</com.example.views.MyCoordinatorLayout>
<!-- DRAWER -->
<fragment
android:id="#+id/fDrawer"
android:name="com.example.fragment.DrawerFragment"
android:layout_width="#dimen/drawer_width"
android:layout_height="match_parent"
android:layout_gravity="left|start"
android:fitsSystemWindows="true"
android:clickable="true"
/>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
I encourage you to use custom AppBarLayout implementation with helper methods to collapse/expand toolbar. On this gist you can find one.
Ok, now it's time to configure our toolbar in activity.
public class ToolbarAppcompatActivity extends AppCompatActivity
implements AppBarLayout.OnOffsetChangedListener {
protected Toolbar toolbar;
protected MyCoordinatorLayout coordinator;
protected ControllableAppBarLayout appbar;
#Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
configureToolbar();
switchFragment(new FooFragment(), "FOO", true);
}
protected void configureToolbar() {
toolbar = (Toolbar) findViewById(R.id.toolbar);
coordinator = (MyCoordinatorLayout) findViewById(R.id.coordinator);
appbar = (ControllableAppBarLayout) findViewById(R.id.appbar);
appbar.addOnOffsetChangedListener(this);
getDelegate().setSupportActionBar(toolbar);
}
public void switchToFragment(Fragment fragment, String tag, boolean expandToolbar){
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment currentFragment = fragmentManager.findFragmentByTag(currentFragmentTag);
if(currentFragment == null || !TextUtils.equals(tag, currentFragmentTag) ){
currentFragmentTag = tag;
fragmentManager
.beginTransaction()
.replace(R.id.flContent, fragment, currentFragmentTag)
.commit();
if(expandToolbar){
expandToolbar();
}else{
collapseToolbar();
}
}
}
protected void addFragment(Fragment fragment, String tag, boolean expandToolbar) {
FragmentManager fragmentManager = getSupportFragmentManager();
currentFragmentTag = tag;
fragmentManager
.beginTransaction()
.add(R.id.flContent, fragment, currentFragmentTag)
.addToBackStack(tag)
.commit();
if(expandToolbar){
expandToolbar();
}else{
collapseToolbar();
}
}
protected void collapseToolbar(){
appbar.collapseToolbar();
coordinator.setAllowForScroll(false);
}
public void expandToolbar(){
appbar.expandToolbar();
coordinator.setAllowForScroll(true);
}
}
Every time you want to switch fragment and collapse/expand toolbar just
call method switchFragment/addFragment with proper boolean parameter.
Just one last note. Make sure you use latest support libraries.
dependencies {
// android support
compile 'com.android.support:appcompat-v7:22.2.1'
compile 'com.android.support:recyclerview-v7:22.2.1'
compile 'com.android.support:design:22.2.1'
}
Do not use include tag in AppBarLayout. It does not work
The following code achieves 3 objectives:
Disable CollapsingToolbarLayout expand or collapse by the user, but still allow AppBarLayout.setExpanded.
Prevent scrolling of RecyclerView or NestedScrollView from expanding or collapsing the CollapsingToolbarLayout.
// scrollView can be RecyclerView or NestedScrollView
ViewCompat.setNestedScrollingEnabled(scrollView, false)
Prevent the user from expanding or collapsing the CollapsingToolbarLayout by flicking the AppBar.
val params = appBar.layoutParams as CoordinatorLayout.LayoutParams
if (params.behavior == null)
params.behavior = AppBarLayout.Behavior()
val behaviour = params.behavior as AppBarLayout.Behavior
behaviour.setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return false
}
})
https://code.luasoftware.com/tutorials/android/how-to-disable-or-lock-collapsingtoolbarlayout-collapse-or-expand/
With Android Design Library v23.1.1, the method described by #LucyFair does not work. I managed to get it to work by setting the app:layout_scrollFlags to enterAlwaysCollapsed only, and the appbar stays "locked".
Hope this helps. :)
I have used #JasonWyatt's solution and added DragCallback to behavior class to prevent touch and drag CollapsingToolbarLayout to expand it
private void setDragCallback() {
setDragCallback(new DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return mEnabled;
}
});
}
I have found a workaround solution that works with activity and various fragments. You implement the CollapsingToolbarLayout with AppBar etc in your activity and then each time you call a new fragment you can call these 2 functions.
When I want my appbar to keep collapsed:
public void lockAppBarClosed() {
mAppBarLayout.setExpanded(false, false);
mAppBarLayout.setActivated(false);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)mAppBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_height);
}
When I want my appbar to be expanded and scrollable again
public void unlockAppBarOpen() {
mAppBarLayout.setExpanded(true, false);
mAppBarLayout.setActivated(true);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)mAppBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_expand_height);
}
You can call thoses functions from your fragments by implementing an interface. Here's a quick example, for your case (toolbar expands only in homeFragment)
public interface CustomListener() {
void unlockAppBarOpen();
void lockAppBarClosed()
}
public class MainActivity extends BaseActivity implements CustomListener {
#Override
public void unlockAppBarOpen() {
mAppBarLayout.setExpanded(true, false);
mAppBarLayout.setActivated(true);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)mAppBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_expand_height);
}
#Override
public void lockAppBarClosed() {
mAppBarLayout.setExpanded(false, false);
mAppBarLayout.setActivated(false);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)mAppBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbar_height);
}
}
public class MainFragment extends BaseFragment {
#Override
public void onResume() {
super.onPause();
((MainActivity) getContext()).unlockAppBarOpen();
}
}
public class SecondFragment extends BaseFragment {
#Override
public void onResume() {
super.onPause();
((MainActivity) getContext()).lockAppBarClosed();
}
}
With this example:
each time the MainFragment will be displayed -> it will extends the toolbar and make it collapsable and expandable
each time the SecondFragment is displayed -> il will collapsed the toolbar to a standard size and prevent it to expand again
Hope it will help you !
I found Simple solution to enable/disable collapse in CollapsingToolbarLayout:
private void setExpandEnabled(boolean enabled) {
mAppBarLayout.setExpanded(enabled, false);
mAppBarLayout.setActivated(enabled);
final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams();
if (enabled)
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
else
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
collapsingToolbarLayout.setLayoutParams(params);
}
I can't comment, so I will post my additions to JasonWyatt's DisableableAppBarLayoutBehavior solution as independent answer.
public class DisableableAppBarLayoutBehavior extends AppBarLayout.Behavior {
private boolean mEnabled = true; // enabled by default
public DisableableAppBarLayoutBehavior() {
super();
}
public DisableableAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
return mEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
#Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (!isEnabled()) return;
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
#Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (!isEnabled()) return;
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
public boolean isEnabled() {
return mEnabled;
}
}
In addition to onStartNestedScroll also lock onNestedPreScroll and onNestedScroll itself to avoid unexpected behaviour. For example, in my case - calling setExpanded(false, true) on my app bar was braking expected behaviour and still was expanding with lags. Now it works:
LayoutParams layoutParams = (LayoutParams) context.appBarLayout.getLayoutParams();
((DisableableAppBarLayoutBehavior)layoutParams.getBehavior()).setEnabled(false);
context.appBarLayout.setLayoutParams(layoutParams);
context.appBarLayout.setExpanded(false, true); // collapse app bar
None of the provided solutions worked for me except this one. With this solution, i can easily manage the state of collapsing toolbar. This will prevent expanding of collapsing toolbar and set title for it.
public void lockAppBar(boolean locked,String title) {
if(locked){
appBarLayout.setExpanded(false, true);
int px = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80, getResources().getDisplayMetrics());
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)appBarLayout.getLayoutParams();
lp.height = px;
appBarLayout.setLayoutParams(lp);
collapsingToolbarLayout.setTitleEnabled(false);
toolbar.setTitle(title);
}else{
appBarLayout.setExpanded(true, false);
appBarLayout.setActivated(true);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
lp.height = (int) getResources().getDimension(R.dimen.toolbarExpandHeight);
collapsingToolbarLayout.setTitleEnabled(true);
collapsingToolbarLayout.setTitle(title);
}
}
Find the AppBarLayout id as like this.
appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
Disable expand on CollapsingToolbarLayout for certain fragments
appBarLayout.setExpanded(true,true);
Enable expand on CollapsingToolbarLayout for certain fragments
appBarLayout.setExpanded(false,true);
Hope it will help you !!
Locking and unlocking was not enough, simple lockings keeps the image shrinked; here my solution
Call this on resume that requires toolbar in collapsed mode.
private void lockAppBarClosed() {
appBarLayout.setExpanded(false,true);
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
((CustomAppBarLayoutBehavior)layoutParams.getBehavior()).setScrollBehavior(false);
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams();
params.setScrollFlags(0);
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
}
This is for a fragment that requires Snap
public void unlockAppBarOpen() {
appBarLayout.setExpanded(true,true);
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
((CustomAppBarLayoutBehavior)layoutParams.getBehavior()).setScrollBehavior(true);
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams();
params.setScrollFlags(0);
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
}
This is for a fragment requires scroll with SCROLL_FLAG_EXIT_UNTIL_COLLAPSED mode.
public void unlockAppBarOpen() {
appBarLayout.setExpanded(true,true);
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
((CustomAppBarLayoutBehavior)layoutParams.getBehavior()).setScrollBehavior(true);
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) collapsingToolbarLayout.getLayoutParams();
params.setScrollFlags(0);
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
}
And the CustomAppBarLayoutBehavior.java
public class CustomAppBarLayoutBehavior extends AppBarLayout.Behavior {
private boolean shouldScroll = false;
public CustomAppBarLayoutBehavior() {
super();
}
public CustomAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
return shouldScroll;
}
#Override
public boolean onTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
if(shouldScroll){
return super.onTouchEvent(parent, child, ev);
}else{
return false;
}
}
public void setScrollBehavior(boolean shouldScroll){
this.shouldScroll = shouldScroll;
}
public boolean isShouldScroll(){
return shouldScroll;
}
}
provide this behavior to AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="210dp"
app:layout_behavior=".behaviors.CustomAppBarLayoutBehavior"
android:theme="#style/ThemeOverlay.MaterialComponents.ActionBar.Primary">
Thanks to #Desmond Lua's great answer I was finally able to fix this problem in my code. Here is my adapted solution using Java and Data Binding.
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar"
bind:expanded="#{myCondition? false : true}"
app:disableCollapsingScroll="#{myCondition? false : true}"
... >
<com.google.android.material.appbar.CollapsingToolbarLayout
... >
...
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:nestedScrollingEnabled="#{myCondition? false : true}"
... >
...
</NestedScrollView>
#BindingAdapter("disableCollapsingScroll")
public static void bindDisableCollapsingScroll(AppBarLayout appBarLayout, boolean disabled) {
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
if (params.getBehavior() == null) {
params.setBehavior(new AppBarLayout.Behavior());
}
AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return disabled;
}
});
}
You can lock appbarlayout expansion by resetting collapsing toolbar height to toolbar height
toolbarHeight = toolbar.getLayoutParams().height;
if (expand) {
collapsingToolbar.getLayoutParams().height = getResources().getDimensionPixelOffset(R.dimen.collapsingToolbarDefaultHeight);
appBarLayout.setExpanded(true, true);
} else {
//collapse
//** it is important you do this before resetting **
appBarLayout.setExpanded(false, true);
appBarLayout.postDelayed(new Runnable() {
#Override
public void run() {
collapsingToolbar.getLayoutParams().height = toolbarHeight;
}
}, 700/* 600 is default animation time to collapse */);
}
The answer of #JasonWyatt works when dragging the scrolling view; but when dragging the appBarLayout it doesn't.
To fix this register a BaseDragCallback listener in the constructors of the custom class to return the mEnabled boolean in the callback:
public class DisableableAppBarLayoutBehavior extends AppBarLayout.Behavior {
private boolean mEnabled;
public DisableableAppBarLayoutBehavior() {
super();
super.setDragCallback(new BaseDragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return mEnabled;
}
});
}
public DisableableAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
super.setDragCallback(new BaseDragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return mEnabled;
}
});
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout parent, #NonNull AppBarLayout child, #NonNull View directTargetChild, View target, int nestedScrollAxes, int type) {
return mEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
public boolean isEnabled() {
return mEnabled;
}
}
In order to lock the app bar just dynamically set the minHeight of the CollapsingToolbarLayout within it. That will prevent it from being collapsed when you drag on it.

Categories

Resources