I'm working on a project in which I want to use the Circular Reveal effect as per the Material Design. Project has minSDK = 11, so for compatibility with pre-Lollipop devices, I'm using this library https://github.com/ozodrukh/CircularReveal
I've a fragment with a FloatingActionButton that, when tapped, will transform itself in the CardView, like described here FAB transformations.
Once the card is revealed it has a button to revert the animation, re-transforming the card into the FAB. Now my problem is this: let's say that a user tap the FAB and the CardView is revealed. Now the user rotate his device, so the activity resets the fragment. What I want to achieve is the card stay visible and revealed, while the FAB should be disabled and invisible. The problem is that if I simply use setVisibility(View.INVISIBLE) on my FAB it doesn't work (note that if I use getVisibility() on it just after setting it invisible, it correctly returns me the value 4 == View.INVISIBLE, but the fab is still visible). I've to wrap the setVisibility(...) call inside a postDelayed() with at least 50-100 ms of delay to make the fab invisible.
So my question is: am I doing things right or there's a better way to accomplish what I want (because it seems very ugly to me)?
Here's some code. This is my XML layout of the fragment:
<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.support.design.widget.AppBarLayout
android:id="#+id/my_appbar"
android:layout_width="match_parent"
android:layout_height="#dimen/toolbar_expanded_height"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="70dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="#style/ToolbarPopupTheme"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
...
<io.codetail.widget.RevealFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="#layout/my_fragment" />
</io.codetail.widget.RevealFrameLayout>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/fab_margin"
app:fabSize="mini"
app:layout_anchor="#+id/my_appbar"
app:layout_anchorGravity="bottom|left|start" />
</android.support.design.widget.CoordinatorLayout>
This is the XML layout of the included CardView:
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="center"
card_view:cardCornerRadius="2dp"
android:visibility="invisible"
android:focusableInTouchMode="true">
<RelativeLayout android:layout_width="match_parent"
android:layout_height="match_parent">
...
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Ok" />
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel" />
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
And this is the code of my Fragment:
public class MyFragment extends Fragment {
private static final String CARD_OPEN_TAG = "CARD_OPEN_TAG";
public static MyFragment newInstance(){
return new MyFragment();
}
private int cardOpen;
private FloatingActionButton fabAddPublication;
private CardView card;
private Button cardCancel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_magazines, container, false);
toolbar = (Toolbar) rootView.findViewById(R.id.layout);
...
// Initialize view status
if (savedInstanceState != null){
cardOpen = savedInstanceState.getInt(CARD_OPEN_TAG);
} else {
cardOpen = -1;
}
...
// Get FAB reference
fab = (FloatingActionButton) rootView.findViewById(R.id.fab_id);
// Get card reference
card = (CardView) rootView.findViewById(R.id.card_id);
editorPublication.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// Using this event because I need my card to be measured to move correctly fab at his center
if (cardOpen != -1){
// Move FAB to center of card
fab.setTranslationX(coordX); // WORKS
fab.setTranslationY(coordY); // WORKS
// fab.setVisibility(View.INVISIBLE) -> DOESN'T WORK, fab remain visible on top and at center of my card
// Ugly workaround
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
// Hide FAB
fab.setVisibility(View.INVISIBLE);
}
}, 50); // Sometimes fails: if device/emulator use too much time to "rotate" screen, fab stay visible
// Remove listener
ViewTreeObserver obs = card.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
obs.removeOnGlobalLayoutListener(this);
else obs.removeGlobalOnLayoutListener(this);
}
}
});
if (editorOpen != -1){
fab.setEnabled(false); // WORKS
card.setVisibility(View.VISIBLE); // WORKS
}
// Get editors buttons reference
cardCancel = (Button) card.findViewById(R.id.card_cancel_id);
// Set FAB listener
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Explode FAB
explodeFab(fab, card); // This method trigger the reveal animation
cardOpen = card.getId();
}
});
// Set editors button listeners
cardCancel.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Implode FAB
implodeFAB(fab, card); // This card reverts the reveal animation
cardOpen = -1;
}
});
...
return rootView;
}
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
...
outState.putInt(CARD_OPEN_TAG, cardOpen);
}
}
I don't think that what you're doing is correct and here's why:
1.
That circular reveal library you're using is incompatible with hardware acceleration on Android 11 to 17 (3.0 to 4.2). It's using Canvas.clipPath() - a method which was implemented in software only up to Android 4.3. It means that you have to turn hardware acceleration off or your app will crash on not supported OpenGL call.
The best way to clip layouts is to use Canvas.saveLayer()/restoreLayer(). It's the only way to support all devices, get antialiased image and support invalidate/layout/draw flow correctly.
2.
Relying on timers and layout listeners to change layouts means that something is not really working for you. Each change can be executed directly without waiting. You just have to find the right place for that piece of code.
Timers can also trigger when your app is in background. It means that it will crash while trying to access UI from Timer thread.
Maybe you're calling setVisibility(true) somewhere in the code and that's why your FAB is visible? Setting visibility is ok and should work without any delayed calls. Just save visibility as FAB's state and restore it after orientation change.
If you wish to position the FAB at the center of the toolbar, wrap them both in a FrameLayout (wrap_content) and position the FAB with layout_gravity="center". That should allow you to remove layout listener.
3.
support.CardView is broken and shouldn't be used at all. It looks and works a little bit different on Lollipop and on older systems. The shadow is different, the padding is different, content clipping doesn't work on pre-Lollipop devices, etc. That's why it's hard to get consistent, good results on all platforms.
You should consider using plain layouts for that purpose.
4.
Some animations are cool-looking, but hard to implement, doesn't give any value and quickly become irritating, because the user has to wait for the animation to finish each time he/she clicks the button.
I'm not saying no for your case, but you may consider removing the transformation and using a drop down menu, a bottom sheet, a static toolbar or a dialog for that purpose.
Related
I have a wearable drawer layout in my Android app. The behavior was that the peek view would display the peek fragment and when the user swiped up, the peek fragment would fade to the content fragment.
I recently attempted to update my android wear project from wearable 2.0.3 to wearable 2.0.5. A lot of components need to be changed from wearable.view to wear.widget.
After the update, both fragments display weather the drawer is open or closed. I tried fading these in and out manually, but there doesn't seem a way to do this smoothly or easily. Is the previous behavior possible anymore? I had to re-arrange things a bit as well to get it to work. Is there a different format I should be using that will get my previous behavior? Thanks!
Previous Drawer Layout:
<android.support.wearable.view.drawer.WearableActionDrawer
android:id="#+id/bottom_action_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorPrimary"
app:drawer_content="#+id/nowPlayingFragment">
<!-- Peek View -->
<fragment
android:id="#+id/peekFragment"
android:name="com.turndapage.navmusic.ui.fragment.PeekFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
android:id="#+id/nowPlayingFragment"
android:name="com.turndapage.navmusic.ui.fragment.NowPlayingFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.wearable.view.drawer.WearableActionDrawer>
New Drawer Layout:
<android.support.wear.widget.drawer.WearableActionDrawerView
android:id="#+id/bottom_action_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorPrimary"
app:drawer_content="#+id/nowPlayingFragment"
app:peekView="#+id/peek_view">
<fragment
android:id="#+id/nowPlayingFragment"
android:name="com.turndapage.navmusic.ui.fragment.NowPlayingFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- Peek View -->
<fragment
android:id="#+id/peekFragment"
android:name="com.turndapage.navmusic.ui.fragment.PeekFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.wear.widget.drawer.WearableActionDrawerView>
Never found a solution I really liked, but here is one that works.
wearableDrawerLayout.setDrawerStateCallback(new WearableDrawerLayout.DrawerStateCallback() {
#Override
public void onDrawerStateChanged(WearableDrawerLayout layout, int newState) {
super.onDrawerStateChanged(layout, newState);
if(nowPlayingUtil != null) {
// If the drawer is settling into position and it was opened all the way.
if (wearableActionDrawer.getDrawerState() == WearableDrawerView.STATE_SETTLING && nowPlayingUtil != null) {
// Transitioning from peek view
if (drawerState == "drawerPeeking")
nowPlayingUtil.fadeToOpen();
// Transitioning from open view
else if (drawerState == "drawerOpen")
nowPlayingUtil.fadeToPeek();
else if (drawerState == "drawerClosed")
nowPlayingUtil.fadeToPeek();
}
if (wearableActionDrawer.getDrawerState() == WearableDrawerView.STATE_IDLE) {
// update the previous State
if (wearableActionDrawer.isOpened()) {
drawerState = "drawerOpen";
nowPlayingUtil.fadeToOpen();
} else if (wearableActionDrawer.isPeeking()) {
drawerState = "drawerPeeking";
nowPlayingUtil.fadeToPeek();
} else if (wearableActionDrawer.isClosed()) {
drawerState = "drawerClosed";
nowPlayingUtil.fadeToPeek();
}
}
}
}
});
So in my example, I have two functions, one for fadeToOpen and one for fadeToPeek which will animate the two views to make them visible or invisible. I can get about the same effect.
Still interested if there is a built in way to do this, but this will do for now.
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
I am trying to implement Bottom sheet in one of my activities and I am kind of confused by the way it is behaving!
So here is the problem, I have an activity in which I am trying to show Bottom sheet and I see that:
if we dont set the app:behavior_peekHeight property then the Bottom sheet never works
If you set the PeekHeight to something less than 30dp (basically just to hide it from screen)
If you set app:behavior_peekHeight to more than 30dp in layout file and try to set the state of bottomSheetBehavior to STATE_HIDDEN in you onCreate method your app crashes with this error
caused by:
java.lang.NullPointerException: Attempt to invoke virtual method
'java.lang.Object java.lang.ref.WeakReference.get()' on a null object reference at android.support.design.widget.BottomSheetBehavior.setState(BottomSheetBehavior.jav a:440)
at myapp.activity.SomeActivity.onCreate(SomeActivity.java:75)
I am really confused on why is it not allowing me to hide it in onCreate? or why cant we just set the peekHeight to 0 so that it is not visible on screen unless we call the STATE_EXPANDED or even not setting that property should default it to hide! or atleast I should be able to set it as hidden in my onCreate!
am I missing something? or is the behavior of the BottomSheet rigid?
my layout file for the BottomSheet is something like this:
<LinearLayout 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:background="#android:color/white"
android:layout_height="100dp"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="40dp" <!-- I cant set this less than 30dp just to hide-->
app:layout_behavior="#string/bottom_sheet_behavior"
tools:context="someActivity"
android:id="#+id/addressbottomSheet"
tools:showIn="#layout/some_activity">
in my activity I am doing something like this:
#InjectView(R.id.addressbottomSheet)
View bottomSheetView;
#Override
protected void onCreate(Bundle savedInstanceState) {
....
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
// only if I have set peek_height to more than 30dp
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
In my onclick I am doing this:
#Override
public void onItemClick(View view, int position) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
After working on this issue for few more days I found one alternate solution for this:
Instead of using the Bottom_sheet directly inside your layout, if we create a Bottom_Sheet fragment and then instantiate it in the activity this issue will not occur and the bottom sheet will be hidden and we dont need to specify the peek_height
here is what I did
public class BottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_bottom_sheet, container, false);
}
Then in my activity
bottomSheetDialog = BottomSheetDialog.newInstance(addressList.get(position), position);
bottomSheetDialog.show(getSupportFragmentManager(), AddressActivity.class.getSimpleName());
This actually solved my problem of bottom sheet being not hidden when the activity starts but I am still not able to understand why if bottom_sheet is included directly we face that problem!
(Referring to the question) Suzzi bro the issue with your code is you are trying to call the setState method directly inside onCreate. This is will throw a nullPointer because the WeakReference is not initialized yet. It will get initialized when the Coordinator layout is about to lay its child view.
onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)
Called when the parent CoordinatorLayout is about the lay out the
given child view.
So the best approach is set the peek height to 0 and show/hide inside the onItemClick listener.
Here is my code:
bottom_sheet.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#mipmap/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Bottom sheet"
android:textColor="#android:color/black" />
</LinearLayout>
activity_main.xml
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show hide bottom sheet" />
<include
android:id="#+id/gmail_bottom_sheet"
layout="#layout/bottom_sheet" />
MainActivity.java
public class MainActivity extends AppCompatActivity {
boolean isExpanded;
Button button;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.gmail_coordinator);
final View bottomSheet = coordinatorLayout.findViewById(R.id.gmail_bottom_sheet);
final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (isExpanded) {
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
isExpanded = !isExpanded;
}
});
}
}
Here initially the bottom sheet is not visible. On clicking the button we will be set the state to STATE_COLLAPSED/STATE_EXPANDED.
The tutorial I followed to make this demo app is listed below:
Bottom Sheet with Android Design Support Library
The reason its crashing is due to the fact that the weak reference is not being set until one of the last lines in onLayoutChild, which gives you your null ptr exception.
What you can do is create a custom BottomSheet Behavior and override onLayoutChild, setting the expanded state there.
An example can be found here:
NullPointerExeption with AppCompat BottomSheets
To avoid the Null pointer exception, set the state to HIDDEN like this in onCreate()
View bottomSheetView = findViewById(R.id.bottomsheet_review_detail_id);
mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
bottomSheetView.post(new Runnable() {
#Override
public void run() {
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
});
TL;DR: TextView in Bottomsheet not showing wrapped multi-line text the first time Bottomsheet is expanded, but adjust itself after it collapsed.
So I am using the Bottomsheet from design-23.2.1 library.
My layout file looks like this:
<android.support.design.widget.CoordinatorLayout>
......
<LinearLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>
</android.support.design.widget.CoordinatorLayout>
The content of the Bottomsheet is basically a list:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="false" />
...
</LinearLayout>
The issue is whenever the Bottomsheet is set to STATE_EXPANDED the first time, the TextView is single line and text is wrapped, and there is no ellipsis … at line end.
Then after it's set to STATE_COLLAPSED, TextView's height is fine and multi-lined properly.
I know height re-layout happened after set to STATE_COLLAPSED because I slide it from collapse and the multi-line is already there.
A work around is provided here. I followed it and added this:
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetBehavior.onLayoutChild(coordinatorLayout,
bottomSheetView,
ViewCompat.LAYOUT_DIRECTION_LTR);
}
}
........
}
It did actually make the height re-adjust when Bottomsheet is expanded the first time. However it occurred abruptly right after the expanding animation is done.
Is there any way to adjust the height ahead of the expanding animation just like how Google Map does?
Update
I found that this issue is because I have set Bottomsheet to STATE_COLLAPSED before it was expanded. If that was not set then the problem is gone and height is adjusted properly the first time.
Now my question is: why set it to STATE_COLLAPSED before expanding will cause that issue?
if for some reason you still have to use old support library, here is the workaround for this.
mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull final View bottomSheet, int newState) {
bottomSheet.post(new Runnable() {
#Override
public void run() {
//workaround for the bottomsheet bug
bottomSheet.requestLayout();
bottomSheet.invalidate();
}
});
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
After switching to design library 24.0.0, the issue cannot be reproduced anymore.
Thanks for the efforts from the Android team to make our life easier and easier.
I'm trying to figure out how the expand/collapse animation of the toolbar is done. If you have a look at the Telegram app settings, you will see that there is a listview and the toolbar. When you scroll down, the toolbar collapse, and when you scroll up it expands. There is also the animation of the profile pic and the FAB. Does anyone have any clue on that? Do you think they built all the animations on top of it? Maybe I'm missing something from the new APIs or the support library.
I noticed the same behaviour on the Google calendar app, when you open the Spinner (I don't think it's a spinner, but it looks like): The toolbar expands and when you scroll up, it collapse.
Just to clearify: I don't need the QuickReturn method. I know that probably Telegram app is using something similar. The exact method that I need is the Google Calendar app effect. I've tried with
android:animateLayoutChanges="true"
and the expand method works pretty well. But obviously, If I scroll up the ListView, the toolbar doesn't collapse.
I've also thought about adding a GestureListener but I want to know if there are any APIs or simpler methods of achieving this.
If there are none, I think I will go with the GestureListener. Hopefully to have a smooth effect of the Animation.
Thanks!
Edit :
Since the release of the Android Design support library, there's an easier solution. Check joaquin's answer
--
Here's how I did it, there probably are many other solutions but this one worked for me.
First of all, you have to use a Toolbar with a transparent background. The expanding & collapsing Toolbar is actually a fake one that's under the transparent Toolbar. (you can see on the first screenshot below - the one with the margins - that this is also how they did it in Telegram).
We only keep the actual Toolbar for the NavigationIcon and the overflow MenuItem.
Everything that's in the red rectangle on the second screenshot (ie the fake Toolbar and the FloatingActionButton) is actually a header that you add to the settings ListView (or ScrollView).
So you have to create a layout for this header in a separate file that could look like this :
<!-- The headerView layout. Includes the fake Toolbar & the FloatingActionButton -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="#+id/header_container"
android:layout_width="match_parent"
android:layout_height="#dimen/header_height"
android:layout_marginBottom="3dp"
android:background="#android:color/holo_blue_dark">
<RelativeLayout
android:id="#+id/header_infos_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:padding="16dp">
<ImageView
android:id="#+id/header_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="8dp"
android:src="#android:drawable/ic_dialog_info" />
<TextView
android:id="#+id/header_title"
style="#style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#+id/header_picture"
android:text="Toolbar Title"
android:textColor="#android:color/white" />
<TextView
android:id="#+id/header_subtitle"
style="#style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/header_title"
android:layout_toRightOf="#+id/header_picture"
android:text="Toolbar Subtitle"
android:textColor="#android:color/white" />
</RelativeLayout>
</RelativeLayout>
<FloatingActionButton
android:id="#+id/header_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="10dp"
android:src="#drawable/ic_open_in_browser"/>
</FrameLayout>
(Note that you can use negative margins/padding for the fab to be straddling on 2 Views)
Now comes the interesting part. In order to animate the expansion of our fake Toolbar, we implement the ListView onScrollListener.
// The height of your fully expanded header view (same than in the xml layout)
int headerHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
// The height of your fully collapsed header view. Actually the Toolbar height (56dp)
int minHeaderHeight = getResources().getDimensionPixelSize(R.dimen.action_bar_height);
// The left margin of the Toolbar title (according to specs, 72dp)
int toolbarTitleLeftMargin = getResources().getDimensionPixelSize(R.dimen.toolbar_left_margin);
// Added after edit
int minHeaderTranslation;
private ListView listView;
// Header views
private View headerView;
private RelativeLayout headerContainer;
private TextView headerTitle;
private TextView headerSubtitle;
private FloatingActionButton headerFab;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.listview_fragment, container, false);
listView = rootView.findViewById(R.id.listview);
// Init the headerHeight and minHeaderTranslation values
headerHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
minHeaderTranslation = -headerHeight +
getResources().getDimensionPixelOffset(R.dimen.action_bar_height);
// Inflate your header view
headerView = inflater.inflate(R.layout.header_view, listview, false);
// Retrieve the header views
headerContainer = (RelativeLayout) headerView.findViewById(R.id.header_container);
headerTitle = (TextView) headerView.findViewById(R.id.header_title);
headerSubtitle = (TextView) headerView.findViewById(R.id.header_subtitle);
headerFab = (TextView) headerView.findViewById(R.id.header_fab);;
// Add the headerView to your listView
listView.addHeaderView(headerView, null, false);
// Set the onScrollListener
listView.setOnScrollListener(this);
// ...
return rootView;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
// Do nothing
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
Integer scrollY = getScrollY(view);
// This will collapse the header when scrolling, until its height reaches
// the toolbar height
headerView.setTranslationY(Math.max(0, scrollY + minHeaderTranslation));
// Scroll ratio (0 <= ratio <= 1).
// The ratio value is 0 when the header is completely expanded,
// 1 when it is completely collapsed
float offset = 1 - Math.max(
(float) (-minHeaderTranslation - scrollY) / -minHeaderTranslation, 0f);
// Now that we have this ratio, we only have to apply translations, scales,
// alpha, etc. to the header views
// For instance, this will move the toolbar title & subtitle on the X axis
// from its original position when the ListView will be completely scrolled
// down, to the Toolbar title position when it will be scrolled up.
headerTitle.setTranslationX(toolbarTitleLeftMargin * offset);
headerSubtitle.setTranslationX(toolbarTitleLeftMargin * offset);
// Or we can make the FAB disappear when the ListView is scrolled
headerFab.setAlpha(1 - offset);
}
// Method that allows us to get the scroll Y position of the ListView
public int getScrollY(AbsListView view)
{
View c = view.getChildAt(0);
if (c == null)
return 0;
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = c.getTop();
int headerHeight = 0;
if (firstVisiblePosition >= 1)
headerHeight = this.headerHeight;
return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}
Note that there are some parts of this code I didn't test, so feel free to highlight mistakes. But overall, I'm know that this solution works, even though I'm sure it can be improved.
EDIT 2:
There were some mistakes in the code above (that I didn't test until today...), so I changed a few lines to make it work :
I introduced another variable, minHeaderTranslation, which replaced minHeaderHeight;
I changed the Y translation value applied to the header View from :
headerView.setTranslationY(Math.max(-scrollY, minHeaderTranslation));
to :
headerView.setTranslationY(Math.max(0, scrollY + minHeaderTranslation));
Previous expression wasn't working at all, I'm sorry about that...
The ratio calculation also changed, so that it now evolves from the bottom the toolbar (instead of the top of the screen) to the full expanded header.
Also check out CollapsingTitleLayout written by Chris Banes in Android team:
https://plus.google.com/+ChrisBanes/posts/J9Fwbc15BHN
Code: https://gist.github.com/chrisbanes/91ac8a20acfbdc410a68
Use design support library http://android-developers.blogspot.in/2015/05/android-design-support-library.html
include this in build.gradle
compile 'com.android.support:design:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.+'
for recycler view include this also
compile 'com.android.support:recyclerview-v7:22.2.0'
<!-- AppBarLayout allows your Toolbar and other views (such as tabs provided by TabLayout)
to react to scroll events in a sibling view marked with a ScrollingViewBehavior.-->
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<!-- specify tag app:layout_scrollFlags -->
<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"/>
<!-- specify tag app:layout_scrollFlags -->
<android.support.design.widget.TabLayout
android:id="#+id/tabLayout"
android:scrollbars="horizontal"
android:layout_below="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"/>
<!-- app:layout_collapseMode="pin" will help to pin this view at top when scroll -->
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="Title"
android:gravity="center"
app:layout_collapseMode="pin" />
</android.support.design.widget.AppBarLayout>
<!-- This will be your scrolling view.
app:layout_behavior="#string/appbar_scrolling_view_behavior" tag connects this features -->
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>
Your activity should extend AppCompatActivity
public class YourActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.your_layout);
//set toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
}
Your app theme should be like this
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
</style>
</resources>
This is my implementation:
collapsedHeaderHeight and expandedHeaderHeight are defined somewhere else, with the function getAnimationProgress I can get the Expand/Collapse progress, base on this value I do my animation and show/hide the real header.
listForumPosts.setOnScrollListener(new AbsListView.OnScrollListener() {
/**
* #return [0,1], 0 means header expanded, 1 means header collapsed
*/
private float getAnimationProgress(AbsListView view, int firstVisibleItem) {
if (firstVisibleItem > 0)
return 1;
// should not exceed 1
return Math.min(
-view.getChildAt(0).getTop() / (float) (expandedHeaderHeight - collapsedHeaderHeight), 1);
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// at render beginning, the view could be empty!
if (view.getChildCount() > 0) {
float animationProgress = getAnimationProgress(view, firstVisibleItem);
imgForumHeaderAvatar.setAlpha(1-animationProgress);
if (animationProgress == 1) {
layoutForumHeader.setVisibility(View.VISIBLE);
} else {
layoutForumHeader.setVisibility(View.GONE);
}
}
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// do nothing
}
}