I have to hide bottom navigation view on up scroll and show on down scroll .how to implement this?
my layout is like this
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_above="#+id/navigation"
android:layout_alignParentTop="true"
android:layout_marginBottom="5dp">
<FrameLayout
android:id="#+id/container1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
<android.support.design.widget.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="?android:attr/windowBackground"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:menu="#menu/dashboard_slider_menu" />
</RelativeLayout>
I have attached screenshot of view. Kindly check it.
UPDATE
Just add one attribute to BottomNavigationView
Material Library AndroidX
<com.google.android.material.bottomnavigation.BottomNavigationView
....
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"/>
Support Library Version 28.0.0 or higher version
<android.support.design.widget.BottomNavigationView
....
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"/>
Note:- Your XML should follow the structure of XML given below in old answer.
**OLD ANSWER(Still Works)**
You need a helper class to do this .This solution works like Google Material Design Guideline.
Create a class BottomNavigationViewBehavior
public class BottomNavigationViewBehavior extends CoordinatorLayout.Behavior<BottomNavigationView> {
private int height;
#Override
public boolean onLayoutChild(CoordinatorLayout parent, BottomNavigationView child, int layoutDirection) {
height = child.getHeight();
return super.onLayoutChild(parent, child, layoutDirection);
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout coordinatorLayout,
BottomNavigationView child, #NonNull
View directTargetChild, #NonNull View target,
int axes, int type)
{
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
#Override
public void onNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull BottomNavigationView child,
#NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed,
#ViewCompat.NestedScrollType int type)
{
if (dyConsumed > 0) {
slideDown(child);
} else if (dyConsumed < 0) {
slideUp(child);
}
}
private void slideUp(BottomNavigationView child) {
child.clearAnimation();
child.animate().translationY(0).setDuration(200);
}
private void slideDown(BottomNavigationView child) {
child.clearAnimation();
child.animate().translationY(height).setDuration(200);
}
}
For using this behavior you need to use cooradinator layout...
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.kliff.digitaldwarka.activity.MainActivity">
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="#+id/myAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:theme="#style/AppTheme.AppBarOverlay"
app:elevation="0dp">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:contentInsetStart="0dp"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
<!---your RecyclerView/Fragment Container Layout-->
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:itemBackground="#color/white"
app:menu="#menu/bottom_nav_menu" />
</android.support.design.widget.CoordinatorLayout>
<!---NavigationView-->
</android.support.v4.widget.DrawerLayout>
Add this code to your Activity that contains bottom nav..
mBottomNavigationView = (BottomNavigationView) findViewById(R.id.bottom_nav);
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mBottomNavigationView.getLayoutParams();
layoutParams.setBehavior(new BottomNavigationViewBehavior());
Try this,
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0 && bottom_navigation.isShown()) {
bottom_navigation.setVisibility(View.GONE);
} else if (dy < 0 ) {
bottom_navigation.setVisibility(View.VISIBLE);
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
Image while scrolling up :-
Image while scrolling down:
Updated answer after the latest library updates:
Hiding the BottomNavigationView on scrolling is now available with just one flag in the layout! Starting from version 28.0.0-alpha1 or the material/androidX 1.0.0-alpha1.
I updated my project using the latter approach since the version now is a stable release candidate. Update: Use fully released version "1.0.0"!
The new out of the box available behaviour is called HideBottomViewOnScrollBehavior. Set it on the BottomNavigationView as
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior" as described in the latest docs.
Here is a full example:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:labelVisibilityMode="selected"
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"
android:layout_gravity="bottom"
app:layout_insetEdge="bottom"
app:menu="#menu/navigation" />
As with the hiding of the Toolbar on scrolling, you have to ensure that the content is a class that supports the latest scrolling like RecyclerView and NestedScrollView.
This ensures all is working as shown in the animation on the design specs
PS: labelVisibilityMode is another cool addition you get for free for taking the trouble of updating and that is described in depth in the design specs.
Update your project to Androidx i.e Refactor >> Migrate to androidx (Minimum Android studio version 3.4)
Using the default Bottom Navigation Menu xml file, replace the parent Constraint Layout with Coordinator layout.
Add the line app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
i.e
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".dashboards.Admin_dashboard_main">
<include layout="#layout/toolbar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/main_area"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="0dp"
android:padding="0dp">
<!-- Fragments Container -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="MainActivity"
tools:showIn="#layout/activity_tenant_dashboard"
android:id="#+id/fragment_container">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Bottom Navigation View -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
android:layout_gravity="bottom"
app:menu="#menu/menu_admin_dashboard_main"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Use this
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener()
{
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
if (dy > 0 ||dy<0 && csButtonLay.isShown())
{
bottomBar.setVisibility(View.GONE);
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
{
if (newState == RecyclerView.SCROLL_STATE_IDLE)
{
bottomBar.setVisibility(View.VISIBLE);
}
super.onScrollStateChanged(recyclerView, newState);
}
});
Just use CoordinatorLayout as a parent container and add the app:layout_behavior
in the child View and set the behavior #string/hide_bottom_view_on_scroll_behavior
this is the solution.
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:orientation="vertical"
tools:context=".Main2Activity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_above="#id/nav_view"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"
android:background="?android:attr/windowBackground"
app:menu="#menu/bottom_nav_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Happy Coding.
I encountered this issue working with the Recyclerview.
The attribute:
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"/>
only partially worked for me, so I had to implement another solution.
I defined the BottomNavigationView inside the MainActivity so I had to set a couple of methods to animate it during the scrolling.
class MainActivity : AppCompatActivity() {
private var animator: ObjectAnimator? = null
.
.
.
fun slideDown() {
nav_view?.let {
if (animator == null && it.translationY == 0f) {
animator = translationObjectY(it, 0f, it.height.toFloat() + it.marginBottom.toFloat()).apply {
doOnEnd {
animator = null
}
}
}
}
}
fun slideUp() {
nav_view?.let {
if (animator == null && it.translationY == it.height.toFloat() + it.marginBottom.toFloat()) {
animator = translationObjectY(it, it.height.toFloat() + it.marginBottom.toFloat(), 0f).apply {
doOnEnd {
animator = null
}
}
}
}
}
}
The translationObjectY is an extended function:
fun translationObjectY(
targetView: View?,
startY: Float,
endY: Float,
duration: Long = 200L
) : ObjectAnimator {
return ObjectAnimator.ofFloat(targetView, "translationY", startY, endY).apply {
this.duration = duration
interpolator = LinearOutSlowInInterpolator()
start()
}
}
And I finally create a custom Recyclerview:
class CustomRecyclerView(
context: Context,
attrs: AttributeSet?,
defStyle: Int,
) : RecyclerView(context, attrs, defStyle) {
constructor(context: Context)
: this(context, null, 0)
constructor(context: Context, attrs: AttributeSet)
: this(context, attrs, 0)
init {
this.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
// Scrolling up
hideBottomMenu()
} else {
// Scrolling down
showBottomMenu()
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
})
}
private fun hideBottomMenu() {
(context as? MainActivity)?.slideDown()
}
private fun showBottomMenu() {
(context as? MainActivity)?.slideUp()
}
}
You can then implement it in your fragment like this:
<com.studio.mattiaferigutti.kamasutra.custom.CustomRecyclerView
android:id="#+id/searchRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Just simply add this in your xml
<BottomNavigationView
....
....
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"/>
This can help somebody
read more:https://material.io/develop/android/components/app-bars-bottom
add
app:hideOnScroll="true"
inside BottomAppBar Like below:
<androidx.coordinatorlayout.widget.CoordinatorLayout
...>
...
<com.google.android.material.bottomappbar.BottomAppBar
...
app:hideOnScroll="true"
/>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Use this code : when Scrolling down the Recyclerview to your fragment will hide the bottom navigation. then when Scrolled Up it will show the Bottom nav.
private View view;
private AppCompatActivity activity;
private ChipNavigationBar chipNavigationBar;
//...............................................
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (view == null) {
view = inflater.inflate(R.layout.list_fragment, container, false);
hide_NavigationBar_adwhen_Scrolling();
}
return view;
}
//...........................................................
private void hide_NavigationBar_adwhen_Scrolling() {
activity = (AppCompatActivity) view.getContext();
chipNavigationBar = activity.findViewById(R.id.chipNavigation);
RecyclerView recyclerView = view.findViewById(R.id.recylerView);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {//on_Scrolled_down
// chipNavigationBar.animate().translationY(200).setDuration(500);
chipNavigationBar.animate().translationY(banner_ad_card_1.getHeight()).setDuration(1000);
} else {//on_Scrolled_up
chipNavigationBar.setVisibility(View.VISIBLE);
chipNavigationBar.animate().translationY(0).setDuration(1000);
// chipNavigationBar.setItemSelected(R.id.home, true);
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
}
Related
I made a viewpager2 which has two Fragments, inside each Fragment there is a Recyclerview. The viewpager itself is inside a Nestedscrollview in order to hide the toolbar when scroll up. Here is my code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:orientation="vertical"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbarlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
app:tabGravity="fill"
app:tabMode="fixed"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<include layout="#layout/material_design_floating_action_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
As I said the viewPager2 have two fragment each of them have a recyclerview. Here problem is, fragment 2 recyclerView take the same height of fragment 1 recyclerView though both recyclerView have different list items and their height should be depends on the list items. I mean, I am expecting these recyclerViews height should act separately based on the list. How can I solve this issue? Please let me know if you need fragment code.
Edit:
Activity code which holds the viewPager2
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolbar();
init();
viewPager.setAdapter(createCardAdapter());
new TabLayoutMediator(tabLayout, viewPager,
new TabLayoutMediator.TabConfigurationStrategy() {
#Override public void onConfigureTab(#NonNull TabLayout.Tab tab, int position) {
//tab.setText("Tab " + (position + 1));
if(position == 0){
tab.setText("Home");
}else if(position == 1){
tab.setText("Events");
}
}
}).attach();
RunnTimePermissions.requestForAllRuntimePermissions(this);
showNotifyDialog();
}
ViewPager Adapter Code:
public class ViewPagerAdapter extends FragmentStateAdapter {
private static final int CARD_ITEM_SIZE = 2;
public ViewPagerAdapter(#NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
#NonNull #Override public Fragment createFragment(int position) {
switch (position){
case 0:
return HomeFragment.newInstance("abc","abc");
// break;
case 1:
return EventListFragment.newInstance("abc","abc");
// break;
}
return HomeFragment.newInstance("abc","abc");
}
#Override public int getItemCount() {
return CARD_ITEM_SIZE;
}
}
Fragment 1 layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2 layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Alright buddy. I did it! I have the solution to this issue.
First off, here's the official response from Google after I opened an issue.
https://issuetracker.google.com/issues/188474850?pli=1
Anyway, on to the fix. Replace the NestedScrollView with this class:
https://gist.github.com/AfzalivE/fdce03eeee8e16203bcc37ba26d7abf3
The idea is to basically create a very light-weight version of NestedScrollView. Instead of messing with child heights, we listen to the children's scroll and either let them scroll, or forward the scrolling to the BottomSheetBehavior. For example when the RecyclerView has been scrolled all the way to the top, then it doesn't consume any of the scrolling, so we can forward that to the bottom sheet so it can scroll. And we only allow the RecyclerView to scroll when the Bottom sheet is expanded.
Also, I must add that this scenario works out of the box with Jetpack Compose + Accompanist-pager so just do that if you really need perfect functionality.
class BottomSheetScrollView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
NestedScrollingParent2 {
private val TAG = "NestedScroll3"
private val childHelper = NestedScrollingChildHelper(this).apply {
isNestedScrollingEnabled = true
}
private var behavior: BottomSheetBehavior<*>? = null
var started = false
var canScroll = false
var pendingCanScroll = false
var dyPreScroll = 0
init {
ViewCompat.setNestedScrollingEnabled(this, true)
}
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onNextScrollStop(newState == BottomSheetBehavior.STATE_EXPANDED)
Log.d(
"BottomSheet",
"Can scroll CHANGED to: $canScroll, because bottom sheet state is ${
getBottomSheetStateString(newState)
}"
)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
}
fun onNextScrollStop(canScroll: Boolean) {
pendingCanScroll = canScroll
}
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
// ViewPager2's RecyclerView does not participate in this nested scrolling.
// This allows it to show it's overscroll indicator.
if (target is RecyclerView) {
val layoutManager = target.layoutManager as LinearLayoutManager
if (layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
target.isNestedScrollingEnabled = false
}
}
if (!started) {
Log.d(TAG, "started nested scroll from $target")
childHelper.startNestedScroll(axes, type)
started = true
}
return true
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
Log.d(TAG, "accepted nested scroll from $target")
}
override fun onStopNestedScroll(target: View, type: Int) {
if (started) {
childHelper.stopNestedScroll(type)
started = false
Log.d(
TAG,
"stopped nested scroll from $target, changing canScroll from $canScroll to $pendingCanScroll"
)
canScroll = pendingCanScroll
}
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
Log.d(
TAG,
"onNestedScroll: dxC: $dxConsumed, dyC: $dyConsumed, dxU: $dxUnconsumed, dyU: $dyUnconsumed"
)
if (dyUnconsumed == dyPreScroll && dyPreScroll < 0) {
canScroll = false
Log.d(TAG, "Can scroll CHANGED to: $canScroll, because scrolled to the top of the list")
}
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
Log.d(
TAG,
"onNestedPreScroll: dx: $dx, dy: $dy, consumed: [ ${consumed.joinToString(", ")} ]"
)
if (!canScroll) {
childHelper.dispatchNestedPreScroll(dx, dy, consumed, null, type)
// Ensure all dy is consumed to prevent premature scrolling when not allowed.
consumed[1] = dy
} else {
dyPreScroll = dy
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
behavior = findBottomSheetBehaviorParent(parent) as BottomSheetBehavior<*>?
behavior?.addBottomSheetCallback(bottomSheetCallback)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
behavior?.removeBottomSheetCallback(bottomSheetCallback)
}
private fun findBottomSheetBehaviorParent(parent: ViewParent?): CoordinatorLayout.Behavior<*>? {
if (parent !is View) {
throw IllegalArgumentException(
"None of this view's ancestors are associated with BottomSheetBehavior"
)
}
val layoutParams = parent.layoutParams
return if (layoutParams is CoordinatorLayout.LayoutParams && layoutParams.behavior != null) {
layoutParams.behavior
} else {
findBottomSheetBehaviorParent((parent as View).parent)
}
}
private fun getBottomSheetStateString(state: Int): String {
return when (state) {
1 -> "STATE_DRAGGING"
2 -> "STATE_SETTLING"
3 -> "STATE_EXPANDED"
4 -> "STATE_COLLAPSED"
5 -> "STATE_HIDDEN"
6 -> "STATE_HALF_EXPANDED"
else -> "0"
}
}
}
I do not understand why you have recyclerview inside FrameLayout .
Here problem is, fragment 2 recyclerView take the same height of
fragment 1 recyclerView though both recyclerView have different list
items and their height should be depends on the list items
RecyclerView generally always depends on the item_layout until we have not fixed its height n width to match_parent.
In your case you have fixed RecyclerView android:layout_width and android:layout_width to match_parent in both the Fragment .
Try this:
Fragment 1
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
P.S: assuming each reacyclerView's item_layout would be having height as wrap_content
I am trying to achieve the coordinator layout behaviour where on scrolling the recycler view can hide both the toolbar and bottom navigation view. So far I have achieved one success i.e bottom navigation bottom bar does hide but with one caveat that it remains active even when the keyboard is on(how do I fix that too?)
My main concern here is how do I achieve the same feature of bottom navigation view of hiding into the toolbar?
I have included the custom toolbar in Appbar layout, but I have tried to add the Toolbar layout tag too in the AppBar nothing works, It just remains the same.
And for the bottomnavigation jumping up on top I don't know what to do? till now I have added snap scroll flags on the bottomnavigation view to stop this behaviour and also snap flag didn't work, I think so, coz it remains in halfway position while going up on search tap.
Got this BottomNavigationBehavior from the wonderful article.
reference
video showing behavior
image for snap behavior
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:elevation="0dp"
android:background="#android:color/transparent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<include
app:layout_scrollFlags="scroll|enterAlways|snap"
layout="#layout/browser_search_tap_tb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/browser_tb"
/>
</com.google.android.material.appbar.AppBarLayout>
<!--Scrolling effect for the bottom nav menu-->
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/rv_test_items"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:id="#+id/rv_test"
/>
<!--Bottom navigation view for the Selection of the Tabs and Items in Menu-->
<com.google.android.material.bottomnavigation.BottomNavigationView
app:layout_scrollFlags="scroll|enterAlways|snap"
android:id="#+id/browser_bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#ffff"
app:layout_behavior="com.example.android.browserui.BottomNavigationBehavior"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_nav_menu"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
browser_search_tap_tb.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
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="?actionBarSize"
android:background="#android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat"
app:contentInsetStart="8dp"
app:contentInsetEnd="8dp"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="0dp"
android:layout_height="match_parent"
android:hint="Search or type new address"
android:padding="8dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:drawableEnd="#drawable/ic_mic"
android:inputType="textWebEditText"
android:background="#drawable/rounded_et_search"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" android:id="#+id/et_search_bar_tap"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
BottomNavigationBehavior.kt
class BottomNavigationBehavior<V : View>(context: Context, attrs: AttributeSet) :
CoordinatorLayout.Behavior<V>(context, attrs) {
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
var isSnappingEnabled = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency is Snackbar.SnackbarLayout) {
updateSnackbar(child, dependency)
}
return super.layoutDependsOn(parent, child, dependency)
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout, child: V, directTargetChild: View, target: View, axes: Int, type: Int
): Boolean {
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL)
return false
lastStartedType = type
offsetAnimator?.cancel()
return true
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout, child: V, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int
) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = max(0f, min(child.height.toFloat(), child.translationY + dy))
}
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) {
if (!isSnappingEnabled)
return
// add snap behaviour
// Logic here borrowed from AppBarLayout onStopNestedScroll code
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// find nearest seam
val currTranslation = child.translationY
val childHalfHeight = child.height * 0.5f
// translate down
if (currTranslation >= childHalfHeight) {
animateBarVisibility(child, isVisible = false)
}
// translate up
else {
animateBarVisibility(child, isVisible = true)
}
}
}
private fun animateBarVisibility(child: View, isVisible: Boolean) {
if (offsetAnimator == null) {
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = 150L
}
offsetAnimator?.addUpdateListener {
child.translationY = it.animatedValue as Float
}
} else {
offsetAnimator?.cancel()
}
val targetTranslation = if (isVisible) 0f else child.height.toFloat()
offsetAnimator?.setFloatValues(child.translationY, targetTranslation)
offsetAnimator?.start()
}
private fun updateSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) {
if (snackbarLayout.layoutParams is CoordinatorLayout.LayoutParams) {
val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams
params.anchorId = child.id
params.anchorGravity = Gravity.TOP
params.gravity = Gravity.TOP
snackbarLayout.layoutParams = params
}
}
}
So Finally Resolved My issue after getting my head around for 4 days here are some changes which I did and solved the issues :
activity_main.xml
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:elevation="0dp"
android:background="#android:color/transparent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"<!--Removed this line-->
>
<!--Bottom navigation view for the Selection of the Tabs and Items in Menu-->
<com.google.android.material.bottomnavigation.BottomNavigationView
app:layout_scrollFlags="scroll|enterAlways|snap"<!--Removed this line-->
android:id="#+id/browser_bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#ffff"
app:layout_behavior="com.example.android.browserui.BottomNavigationBehavior"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_nav_menu"
/>
Here in activity_main.xml, I removed the layout_behavior because the appbar itself is invoking the scroll behavior for other layout items, it acts as a parent.
app:layout_behavior="#string/appbar_scrolling_view_behavior"
Also removed scrollflags from the bottom navigation view, AS I was implementing this behavior from the Class BottomNavigationBehavior.kt, You find the implementation below
app:layout_scrollFlags="scroll|enterAlways|snap"
browser_search_tap_tb.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar
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="?actionBarSize"
android:background="#android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways" <!--Removed this line-->
app:popupTheme="#style/ThemeOverlay.AppCompat"
app:contentInsetStart="8dp"
app:contentInsetEnd="8dp"
>
Here in browser_search_tb, I removed the following line because it was overriding the Scrollflags in co-ordinator layout so removed it and it worked flawlessly
app:layout_scrollFlags="scroll|enterAlways"
BottomNavigationBehavior.kt
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) {
if (!isSnappingEnabled)
return // removed this line
{
// add snap behaviour
// Logic here borrowed from AppBarLayout onStopNestedScroll code
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// find nearest seam
val currTranslation = child.translationY
val childHalfHeight = child.height * 0.5f
// translate down
if (currTranslation >= childHalfHeight) {
animateBarVisibility(child, isVisible = false)
}
// translate up
else {
animateBarVisibility(child, isVisible = true)
}
}
}
}
Here I removed the
return
and added braces{} to the if statement and snap feature did work properly
Hope this answer will help you and will cut down your debugging time.
The above reference is one of the best article you can find for scrolling behavior on the internet it simple and smooth to grasp
I have bottom nav bar define in the Main activity. I have three fragments linked with BottomNavigation bar in fragments I have recycler view so I want to hide BottomNavigation bar when RecyclerView scrolls down and shows when RecyclerView scrolls up.
My problem is how can I access BottomNavigation bar in fragments because it is defined in MainActivity.
This is my code:
activity_main.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/AppTheme.AppBarOverlay"
app:elevation="0dp"
android:background="#color/colorPrimary"
android:paddingBottom="7dp"
android:fitsSystemWindows="true">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways|snap">
<Spinner
android:layout_width="110dp"
android:layout_height="50dp"
android:id="#+id/chooseLocation"
app:backgroundTint="#android:color/white"/>
</android.support.v7.widget.Toolbar>
<EditText
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:id="#+id/search"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingRight="6dp"
android:paddingLeft="12dp"
android:hint="Search here"
android:textColorHint="#9e9e9e"
android:textColor="#000"
tools:ignore="HardcodedText"
android:background="#drawable/search_edit_text"
android:paddingEnd="6dp"
android:paddingStart="12dp"/>
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_main" />
<android.support.design.widget.BottomNavigationView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/bottomBar"
android:layout_gravity="bottom"
app:menu="#menu/bottom_menu"
android:background="#fff"
app:itemIconTint="#drawable/nav_check"
app:itemTextColor="#drawable/nav_check"/>
</android.support.design.widget.CoordinatorLayout>
fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Tab1Fragment"
android:background="#fff">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/purchasedBook"/>
</RelativeLayout>
This is how my fragments are defined as there is no bottom nav bar in any fragments so how can I access bottom nav bar in fragments.
Someone, please let me know any help would be appreciated.
THANKS
navController.addOnDestinationChangedListener { _, destination, _ ->
if(destination.id == R.id.full_screen_destination) {
bottomNavigationView.visibility = View.GONE
} else {
bottomNavigationView.visibility = View.VISIBLE
}
}
Do this in the main activity.
Here R.id.full_screen_destination is id of the fragment in navigation fragment.
To access your BottomNavigationView from within the fragments use the following code:
BottomNavigationView navBar = getActivity().findViewById(R.id.bottomBar);
As the fragment is always inside an activity and you can call getActivity() in fragment to access objects that already exist in the activity. So you can do this:
Activity
public class MainActivity extends Activity {
//...
Toolbar toolbar;
//...
public Toolbar getNav() {
return toolbar;
}
//...
}
Fragment
//...
if(getActivity() != null && getActivity instanceOf MainActivity)
((MainActivity)getActivity()).getNav().setVisiblity(View.GONE);
//...
Try this,
Add this line in the BottomNavigationView in Xml
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"
And Implement this BottomNavigation behavior using CoOrdinator Layout and you can hide or show the view using the scroll listeners.
public class BottomNavigationViewBehavior extends CoordinatorLayout.Behavior<BottomNavigationView> {
private int height;
#Override
public boolean onLayoutChild(CoordinatorLayout parent, BottomNavigationView child, int layoutDirection) {
height = child.getHeight();
return super.onLayoutChild(parent, child, layoutDirection);
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout coordinatorLayout,
BottomNavigationView child, #NonNull
View directTargetChild, #NonNull View target,
int axes, int type)
{
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
#Override
public void onNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull BottomNavigationView child,
#NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed,
#ViewCompat.NestedScrollType int type)
{
if (dyConsumed > 0) {
slideDown(child);
} else if (dyConsumed < 0) {
slideUp(child);
}
}
private void slideUp(BottomNavigationView child) {
child.clearAnimation();
child.animate().translationY(0).setDuration(200);
}
private void slideDown(BottomNavigationView child) {
child.clearAnimation();
child.animate().translationY(height).setDuration(200);
}
}
Add this line code to your Activity where it contains bottom navigation
bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottom_nav);
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams)
bottomNavigationView .getLayoutParams();
layoutParams.setBehavior(new BottomNavigationViewBehavior());
Try this and let me know Digvijay.Happy Coding.
Fragment has onAttach() method which give you context. So you have to create instance of activity using,
MainActivity mainActivity;
#Override
public void onAttach(Context context) {
super.onAttach(context);
mainActivity = (MainActivity)context;
}
Now make method with boolean param which hide and show bottom bar.
public void visibilityOfBottom(boolean isScroll){
if(isScroll){
// hide bottom bar
} else{
// show bottom bar
}
}
Now access above method in fragment using MainActivity context by,
mainActivity.visibilityOfBottom(false);
Kotlin
Access the navigation view from a fragment
val navBar: BottomNavigationView = activity!!.findViewById(R.id.bottomBar)
The only way I found for hiding bottom navigator in V6
was making structure like this -
Stack Nav {
Bottom Nav
Screen 1
Screen 2
Screen 3
}
Add this code to your fragment. will hide the Bottom nav.
AppCompatActivity activity = (AppCompatActivity) view.getContext();
chipNavigationBar = activity.findViewById(R.id.chipNavigation);
chipNavigationBar.animate().translationY(chipNavigationBar.getHeight()).setDuration(1000);
Use this code :
when Scrolling down the Recyclerview to your fragment will hide the bottom navigation.
then when Scrolled Up it will show the Bottom nav.
private View view;
private AppCompatActivity activity;
private ChipNavigationBar chipNavigationBar;
//...............................................
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (view == null) {
view = inflater.inflate(R.layout.list_fragment, container, false);
hide_NavigationBar_adwhen_Scrolling();
}
return view;
}
//...........................................................
private void hide_NavigationBar_adwhen_Scrolling() {
activity = (AppCompatActivity) view.getContext();
chipNavigationBar = activity.findViewById(R.id.chipNavigation);
RecyclerView recyclerView = view.findViewById(R.id.recylerView);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {//on_Scrolled_down
// chipNavigationBar.animate().translationY(200).setDuration(500);
chipNavigationBar.animate().translationY(banner_ad_card_1.getHeight()).setDuration(1000);
} else {//on_Scrolled_up
chipNavigationBar.setVisibility(View.VISIBLE);
chipNavigationBar.animate().translationY(0).setDuration(1000);
// chipNavigationBar.setItemSelected(R.id.home, true);
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
}
Add
app:layout_behavior="#string/hide_bottom_view_on_scroll_behavior"
to
BottomNavigationView
app:layout_behavior="#string/appbar_scrolling_view_behavior"
to
RecyclerView
This question was asked before in a too broad and unclear way here, so I've made it much more specific, with full explanation and code of what I've tried.
Background
I'm required to mimic the way the Google Calendar has a view at the top, that can animate and push down the view at the bottom, yet it has extra, different behavior. I've summarized what I'm trying to do on 3 characteristics:
Pressing on the toolbar will always work, to toggle expanding/collapsing of the top view, while having an arrow icon that changes its rotation. This is like on Google Calendar app.
The top view will always snap, just like on Google Calendar app.
When the top view is collapsed, only pressing on the toolbar will allow to expand it. This is like on Google Calendar app
When the top view is expanded, scrolling at the bottom view will only allow to collapse. If you try to scroll the other direction, nothing occurs, not even to the bottom view. This is like on Google Calendar app
Once collapsed, the top view will be replaced with a smaller view. This means it will always take some space, above the bottom view. This is not like on Google Calendar app, because on the Calendar app, the top view completely disappears once you collapse it.
Here's how Google Calendar app look like:
Scrolling on the bottom view also slowly hides the view at the top:
The problem
Using various solutions I've found in the past, I've succeeded to implement only a part of the needed behavior:
Having some UI in the toolbar is done by having some views in it, including the arrow view. For manual expanding/collapsing I use setExpanded on the AppBarLayout view. For the rotation of the arrow, I use a listener of how much the AppBarLayout has resized, using addOnOffsetChangedListener on it.
Snapping is easily done by adding snap value into layout_scrollFlags attribute of the CollapsingToolbarLayout. However, to make it really work well, without weird issues (reported here), I used this solution.
Blocking of affecting the top view when scrolling can be done by using the same code I've used on #2 (here), by calling setExpandEnabled there.
This works fine for when the top view is collapsed.
Similar to #3, but sadly, since it uses setNestedScrollingEnabled, which is in both directions, this works well only when the top view is collapsed. When it's expanded, it still allows the bottom view to scroll up, as opposed to Calendar app. When expanded, I need it to only allow to collapse, without allowing to really scroll.
Here's a demonstration of the good, and the bad:
This I've failed completely to do. I've tried a lot of solutions I've thought about, putting views in various places with various flags.
In short, I've succeeded doing 1-3, but not 4-5.
The code
Here's the current code (also available as whole project here) :
ScrollingActivity.kt
class ScrollingActivity : AppCompatActivity(), AppBarTracking {
private var mNestedView: MyRecyclerView? = null
private var mAppBarOffset: Int = 0
private var mAppBarIdle = false
private var mAppBarMaxOffset: Int = 0
private var isExpanded: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
mNestedView = findViewById(R.id.nestedView)
app_bar.addOnOffsetChangedListener({ appBarLayout, verticalOffset ->
mAppBarOffset = verticalOffset
val totalScrollRange = appBarLayout.totalScrollRange
val progress = (-verticalOffset).toFloat() / totalScrollRange
arrowImageView.rotation = 180 + progress * 180
isExpanded = verticalOffset == 0;
mAppBarIdle = mAppBarOffset >= 0 || mAppBarOffset <= mAppBarMaxOffset
if (mAppBarIdle)
setExpandAndCollapseEnabled(isExpanded)
})
app_bar.post(Runnable { mAppBarMaxOffset = -app_bar.totalScrollRange })
mNestedView!!.setAppBarTracking(this)
mNestedView!!.layoutManager = LinearLayoutManager(this)
mNestedView!!.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemCount(): Int = 100
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return object : ViewHolder(LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)) {}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(holder.itemView.findViewById<View>(android.R.id.text1) as TextView).text = "item $position"
}
}
expandCollapseButton.setOnClickListener({ v ->
isExpanded = !isExpanded
app_bar.setExpanded(isExpanded, true)
})
}
private fun setExpandAndCollapseEnabled(enabled: Boolean) {
mNestedView!!.isNestedScrollingEnabled = enabled
}
override fun isAppBarExpanded(): Boolean = mAppBarOffset == 0
override fun isAppBarIdle(): Boolean = mAppBarIdle
}
MyRecyclerView.kt
/**A RecyclerView that allows temporary pausing of casuing its scroll to affect appBarLayout, based on https://stackoverflow.com/a/45338791/878126 */
class MyRecyclerView #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 (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
}
}
}
}
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
}
}
ScrollingCalendarBehavior.kt
class ScrollingCalendarBehavior(context: Context, attrs: AttributeSet) : AppBarLayout.Behavior(context, attrs) {
override fun onInterceptTouchEvent(parent: CoordinatorLayout?, child: AppBarLayout?, ev: MotionEvent): Boolean = false
}
activity_scrolling.xml
<android.support.design.widget.CoordinatorLayout
android:id="#+id/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" tools:context=".ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:stateListAnimator="#null" android:theme="#style/AppTheme.AppBarOverlay"
app:expanded="false" app:layout_behavior="com.example.user.expandingtopviewtest.ScrollingCalendarBehavior"
tools:targetApi="lollipop">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsingToolbarLayout" android:layout_width="match_parent"
android:layout_height="match_parent" android:fitsSystemWindows="true"
android:minHeight="?attr/actionBarSize" app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:statusBarScrim="?attr/colorPrimaryDark">
<LinearLayout
android:layout_width="match_parent" android:layout_height="250dp"
android:layout_marginTop="?attr/actionBarSize" app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1.0">
<TextView
android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="10dp"
android:paddingRight="10dp" android:text="some large, expanded view"/>
</LinearLayout>
<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.constraint.ConstraintLayout
android:id="#+id/expandCollapseButton" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:background="?android:selectableItemBackground"
android:clickable="true" android:focusable="true" android:orientation="vertical">
<TextView
android:id="#+id/titleTextView" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:ellipsize="end"
android:gravity="center" android:maxLines="1" android:text="title"
android:textAppearance="#style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textColor="#android:color/white" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="#+id/arrowImageView" android:layout_width="wrap_content" android:layout_height="0dp"
android:layout_marginLeft="8dp" android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="#+id/titleTextView"
app:layout_constraintStart_toEndOf="#+id/titleTextView"
app:layout_constraintTop_toTopOf="#+id/titleTextView"
app:srcCompat="#android:drawable/arrow_down_float"
tools:ignore="ContentDescription,RtlHardcoded"/>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<com.example.user.expandingtopviewtest.MyRecyclerView
android:id="#+id/nestedView" android:layout_width="match_parent" android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" tools:context=".ScrollingActivity"/>
</android.support.design.widget.CoordinatorLayout>
The questions
How can I make the scrolling being blocked when the top view is expanded, yet allow to collapse while scrolling ?
How can I make the top view be replaced with a smaller one when collapsed (and back to large one when expanded), instead of completely disappear ?
Update
Even though I've got the basic of what I asked about, there are still 2 issues with the current code (available on Github, here) :
The small view (the one you see on collapsed state) has inner views that need to have a clicking effect on them. When using the android:background="?attr/selectableItemBackgroundBorderless" on them, and clicking on this area while being expanded, the clicking is done on the small view. I've handled it by putting the small view on a different toolbar, but then the clicking effect doesn't get shown at all. I've written about this here, including sample project.
Here's the fix:
<android.support.design.widget.CoordinatorLayout
android:id="#+id/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" tools:context=".MainActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:stateListAnimator="#null" android:theme="#style/AppTheme.AppBarOverlay"
app:expanded="false" app:layout_behavior="com.example.expandedtopviewtestupdate.ScrollingCalendarBehavior"
tools:targetApi="lollipop">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsingToolbarLayout" android:layout_width="match_parent"
android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false"
android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
app:statusBarScrim="?attr/colorPrimaryDark">
<!--large view -->
<LinearLayout
android:id="#+id/largeView" android:layout_width="match_parent" android:layout_height="280dp"
android:layout_marginTop="?attr/actionBarSize" android:orientation="vertical"
app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="1.0">
<TextView
android:id="#+id/largeTextView" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true"
android:focusable="true" android:focusableInTouchMode="false" android:gravity="center"
android:text="largeView" android:textSize="14dp" tools:background="?attr/colorPrimary"
tools:layout_gravity="top|center_horizontal" tools:layout_height="40dp" tools:layout_width="40dp"
tools:text="1"/>
</LinearLayout>
<!--top toolbar-->
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/small_view_height" app:contentInsetStart="0dp"
app:layout_collapseMode="pin" app:popupTheme="#style/AppTheme.PopupOverlay">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true"
android:focusable="true">
<LinearLayout
android:id="#+id/expandCollapseButton" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?android:selectableItemBackground" android:gravity="center_vertical"
android:orientation="horizontal" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="#+id/titleTextView" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:ellipsize="end" android:gravity="center"
android:maxLines="1" android:text="title"
android:textAppearance="#style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textColor="#android:color/white"/>
<ImageView
android:id="#+id/arrowImageView" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp" app:srcCompat="#android:drawable/arrow_up_float"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar>
<android.support.v7.widget.Toolbar
android:id="#+id/smallLayoutContainer" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_marginTop="?attr/actionBarSize"
android:clipChildren="false" android:clipToPadding="false" app:contentInsetStart="0dp"
app:layout_collapseMode="pin">
<!--small view-->
<LinearLayout
android:id="#+id/smallLayout" android:layout_width="match_parent"
android:layout_height="#dimen/small_view_height" android:clipChildren="false"
android:clipToPadding="false" android:orientation="horizontal" tools:background="#ff330000"
tools:layout_height="#dimen/small_view_height">
<TextView
android:id="#+id/smallTextView" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true"
android:focusable="true" android:focusableInTouchMode="false" android:gravity="center"
android:text="smallView" android:textSize="14dp" tools:background="?attr/colorPrimary"
tools:layout_gravity="top|center_horizontal" tools:layout_height="40dp"
tools:layout_width="40dp" tools:text="1"/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<com.example.expandedtopviewtestupdate.MyRecyclerView
android:id="#+id/nestedView" android:layout_width="match_parent" android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior" tools:context=".ScrollingActivity"/>
</android.support.design.widget.CoordinatorLayout>
The Google Calendar allows to perform a scroll-down gesture on the toolbar itself, to trigger showing the month view. I've succeeded only adding a clicking event there, but not scrolling. Here's how it looks like:
Note: Full updated project is available here.
How can I make the scrolling being blocked when the top view is expanded, yet allow to collapse while scrolling ?
Issue #1: The RecyclerView should not be able to scroll at all when the app bar is not collapsed. To fix this, add enterAlways to the scroll flags for the CollapsingToolbarLayout as follows:
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
app:statusBarScrim="?attr/colorPrimaryDark">
enterAlways will not cause the app bar to open when closed since you are suppressing that functionality but works as desired otherwise.
Issue #2: When the app bar is fully expanded, the RecyclerView should not be allowed to scroll up. This happens to be a distinct issue from issue #1.
[Updated] To correct this, modify the behavior for the RecyclerView to consume scroll when the RecyclerView tries to scroll up and the app bar is fully expanded or will be fully expanded after the scroll(dy) is consumed . The RecyclerView can scroll up, but it never sees that action since its behavior, SlidingPanelBehavior, consumes the scroll. If the app bar is not fully expanded but will be expanded after the current scroll is consumed, the behavior forces the app bar to fully expand by calling modifying dy and calling the super before fully consuming the scroll. (See SlidingPanelBehavior#onNestedPreScroll()). (In the previous answer, the appBar behavior was modified. Putting the behavior change on RecyclerView is a better choice.)
Issue #3: Setting nested scrolling for the RecyclerView to enable/disabled when nested scrolling is already in the required state causes problems. To avoid these issues, only change the state of nested scrolling when a change is really being made with the following code change in ScrollingActivity:
private void setExpandAndCollapseEnabled(boolean enabled) {
if (mNestedView.isNestedScrollingEnabled() != enabled) {
mNestedView.setNestedScrollingEnabled(enabled);
}
}
This is how the test app behaves with the changes from above:
The changed modules with the above-mentioned changes are at the end of the post.
How can I make the top view be replaced with a smaller one when collapsed (and back to large one when expanded), instead of completely disappear ?
[Update] Make the smaller view a direct child of CollapsingToolbarLayout so it is a sibling of Toolbar. The following is a demonstration of this approach. The collapseMode of the smaller view is set to pin. The smaller view's margins as well as the margins of the toolbar are adjusted so the smaller view falls immediately below the toolbar. Since CollapsingToolbarLayout is a FrameLayout, views stack and the height of the FrameLayout just becomes the height of the tallest child view. This structure will avoid the issue where the insets needed adjustment and the problem with the missing click effect.
One final issue remains and that dragging the appbar down should open it with the assumption that dragging the smaller view down should not open the appbar. Permitting the appbar to open upon dragging is accomplished with setDragCallback of AppBarLayout.Behavior. Since the smaller view is incorporated into the appBar, dragging it down will open the appbar. To prevent this, a new behavior called MyAppBarBehavior is attached to the appbar. This behavior, in conjunction with code in the MainActivity prevents dragging of the smaller view to open the appbar but will permit the toolbar to be dragged.
activity_main.xml
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:stateListAnimator="#null"
android:theme="#style/AppTheme.AppBarOverlay"
app:expanded="false"
app:layout_behavior=".MyAppBarBehavior"
tools:targetApi="lollipop">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
app:statusBarScrim="?attr/colorPrimaryDark">
<!--large view -->
<LinearLayout
android:id="#+id/largeView"
android:layout_width="match_parent"
android:layout_height="280dp"
android:layout_marginTop="?attr/actionBarSize"
android:orientation="vertical"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1.0">
<TextView
android:id="#+id/largeTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="false"
android:gravity="center"
android:text="largeView"
android:textSize="14dp"
tools:background="?attr/colorPrimary"
tools:layout_gravity="top|center_horizontal"
tools:layout_height="40dp"
tools:layout_width="40dp"
tools:text="1" />
</LinearLayout>
<!--top toolbar-->
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/small_view_height"
app:contentInsetStart="0dp"
app:layout_collapseMode="pin"
app:popupTheme="#style/AppTheme.PopupOverlay">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:id="#+id/expandCollapseButton"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?android:selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="#+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:text="title"
android:textAppearance="#style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textColor="#android:color/white" />
<ImageView
android:id="#+id/arrowImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:srcCompat="#android:drawable/arrow_up_float"
tools:ignore="ContentDescription,RtlHardcoded" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar>
<!--small view-->
<LinearLayout
android:id="#+id/smallLayout"
android:layout_width="match_parent"
android:layout_height="#dimen/small_view_height"
android:layout_marginTop="?attr/actionBarSize"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal"
app:layout_collapseMode="pin"
tools:background="#ff330000"
tools:layout_height="#dimen/small_view_height">
<TextView
android:id="#+id/smallTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="false"
android:gravity="center"
android:text="smallView"
android:textSize="14dp"
tools:background="?attr/colorPrimary"
tools:layout_gravity="top|center_horizontal"
tools:layout_height="40dp"
tools:layout_width="40dp"
tools:text="1" />
</LinearLayout>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<com.example.expandedtopviewtestupdate.MyRecyclerView
android:id="#+id/nestedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".SlidingPanelBehavior" />
</android.support.design.widget.CoordinatorLayout>
Finally, in the addOnOffsetChangedListener add the following code to fade out/fade in the smaller view as the app bar expands and contracts. Once the view's alpha is zero (invisible), set its visibility to View.INVISIBLE so it can't be clicked. Once the view's alpha increases above zero, make it visible and clickable by setting its visibility to View.VISIBLE.
mSmallLayout.setAlpha((float) -verticalOffset / totalScrollRange);
// If the small layout is not visible, make it officially invisible so
// it can't receive clicks.
if (alpha == 0) {
mSmallLayout.setVisibility(View.INVISIBLE);
} else if (mSmallLayout.getVisibility() == View.INVISIBLE) {
mSmallLayout.setVisibility(View.VISIBLE);
}
Here are the results:
Here are the new modules with all of the above changes incorporated.
MainActivity.java
public class MainActivity extends AppCompatActivity
implements MyRecyclerView.AppBarTracking {
private MyRecyclerView mNestedView;
private int mAppBarOffset = 0;
private boolean mAppBarIdle = true;
private int mAppBarMaxOffset = 0;
private AppBarLayout mAppBar;
private boolean mIsExpanded = false;
private ImageView mArrowImageView;
private LinearLayout mSmallLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
LinearLayout expandCollapse;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
expandCollapse = findViewById(R.id.expandCollapseButton);
mArrowImageView = findViewById(R.id.arrowImageView);
mNestedView = findViewById(R.id.nestedView);
mAppBar = findViewById(R.id.app_bar);
mSmallLayout = findViewById(R.id.smallLayout);
// Log when the small text view is clicked
findViewById(R.id.smallTextView).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "<<<<click small layout");
}
});
// Log when the big text view is clicked.
findViewById(R.id.largeTextView).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "<<<<click big view");
}
});
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
}
mAppBar.post(new Runnable() {
#Override
public void run() {
mAppBarMaxOffset = -mAppBar.getTotalScrollRange();
CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams();
MyAppBarBehavior behavior = (MyAppBarBehavior) lp.getBehavior();
// Only allow drag-to-open if the drag touch is on the toolbar.
// Once open, all drags are allowed.
if (behavior != null) {
behavior.setCanOpenBottom(findViewById(R.id.toolbar).getHeight());
}
}
});
mNestedView.setAppBarTracking(this);
mNestedView.setLayoutManager(new LinearLayoutManager(this));
mNestedView.setAdapter(new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, 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(RecyclerView.ViewHolder holder, int position) {
((TextView) holder.itemView.findViewById(android.R.id.text1))
.setText("Item " + position);
}
#Override
public int getItemCount() {
return 200;
}
class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View view) {
super(view);
}
}
});
mAppBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarOffset = verticalOffset;
int totalScrollRange = appBarLayout.getTotalScrollRange();
float progress = (float) (-verticalOffset) / (float) totalScrollRange;
mArrowImageView.setRotation(-progress * 180);
mIsExpanded = verticalOffset == 0;
mAppBarIdle = mAppBarOffset >= 0 || mAppBarOffset <= mAppBarMaxOffset;
float alpha = (float) -verticalOffset / totalScrollRange;
mSmallLayout.setAlpha(alpha);
// If the small layout is not visible, make it officially invisible so
// it can't receive clicks.
if (alpha == 0) {
mSmallLayout.setVisibility(View.INVISIBLE);
} else if (mSmallLayout.getVisibility() == View.INVISIBLE) {
mSmallLayout.setVisibility(View.VISIBLE);
}
}
});
expandCollapse.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
setExpandAndCollapseEnabled(true);
if (mIsExpanded) {
setExpandAndCollapseEnabled(false);
}
mIsExpanded = !mIsExpanded;
mNestedView.stopScroll();
mAppBar.setExpanded(mIsExpanded, true);
}
});
}
private void setExpandAndCollapseEnabled(boolean enabled) {
if (mNestedView.isNestedScrollingEnabled() != enabled) {
mNestedView.setNestedScrollingEnabled(enabled);
}
}
#Override
public boolean isAppBarExpanded() {
return mAppBarOffset == 0;
}
#Override
public boolean isAppBarIdle() {
return mAppBarIdle;
}
private static final String TAG = "MainActivity";
}
SlidingPanelBehavior.java
public class SlidingPanelBehavior extends AppBarLayout.ScrollingViewBehavior {
private AppBarLayout mAppBar;
public SlidingPanelBehavior() {
super();
}
public SlidingPanelBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(final CoordinatorLayout parent, View child, View dependency) {
if (mAppBar == null && dependency instanceof AppBarLayout) {
// Capture our appbar for later use.
mAppBar = (AppBarLayout) dependency;
}
return dependency instanceof AppBarLayout;
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) {
int action = event.getAction();
if (event.getAction() != MotionEvent.ACTION_DOWN) { // Only want "down" events
return false;
}
if (getAppBarLayoutOffset(mAppBar) == -mAppBar.getTotalScrollRange()) {
// When appbar is collapsed, don't let it open through nested scrolling.
setNestedScrollingEnabledWithTest((NestedScrollingChild2) child, false);
} else {
// Appbar is partially to fully expanded. Set nested scrolling enabled to activate
// the methods within this behavior.
setNestedScrollingEnabledWithTest((NestedScrollingChild2) child, true);
}
return false;
}
#Override
public boolean onStartNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull View child,
#NonNull View directTargetChild, #NonNull View target,
int axes, int type) {
//noinspection RedundantCast
return ((NestedScrollingChild2) child).isNestedScrollingEnabled();
}
#Override
public void onNestedPreScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull View child,
#NonNull View target, int dx, int dy, #NonNull int[] consumed,
int type) {
// How many pixels we must scroll to fully expand the appbar. This value is <= 0.
final int appBarOffset = getAppBarLayoutOffset(mAppBar);
// Check to see if this scroll will expand the appbar 100% or collapse it fully.
if (dy <= appBarOffset) {
// Scroll by the amount that will fully expand the appbar and dispose of the rest (dy).
super.onNestedPreScroll(coordinatorLayout, mAppBar, target, dx,
appBarOffset, consumed, type);
consumed[1] += dy;
} else if (dy >= (mAppBar.getTotalScrollRange() + appBarOffset)) {
// This scroll will collapse the appbar. Collapse it and dispose of the rest.
super.onNestedPreScroll(coordinatorLayout, mAppBar, target, dx,
mAppBar.getTotalScrollRange() + appBarOffset,
consumed, type);
consumed[1] += dy;
} else {
// This scroll will leave the appbar partially open. Just do normal stuff.
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}
/**
* {#code onNestedPreFling()} is overriden to address a nested scrolling defect that was
* introduced in API 26. This method prevent the appbar from misbehaving when scrolled/flung.
* <p>
* Refer to "Bug in design support library"
*/
#Override
public boolean onNestedPreFling(#NonNull CoordinatorLayout coordinatorLayout,
#NonNull View child, #NonNull View target,
float velocityX, float velocityY) {
//noinspection RedundantCast
if (((NestedScrollingChild2) child).isNestedScrollingEnabled()) {
// Just stop the nested fling and let the appbar settle into place.
((NestedScrollingChild2) child).stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
return true;
}
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
private static int getAppBarLayoutOffset(AppBarLayout appBar) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) appBar.getLayoutParams()).getBehavior();
if (behavior instanceof AppBarLayout.Behavior) {
return ((AppBarLayout.Behavior) behavior).getTopAndBottomOffset();
}
return 0;
}
// Something goes amiss when the flag it set to its current value, so only call
// setNestedScrollingEnabled() if it will result in a change.
private void setNestedScrollingEnabledWithTest(NestedScrollingChild2 child, boolean enabled) {
if (child.isNestedScrollingEnabled() != enabled) {
child.setNestedScrollingEnabled(enabled);
}
}
#SuppressWarnings("unused")
private static final String TAG = "SlidingPanelBehavior";
}
MyRecyclerView.kt
/**A RecyclerView that allows temporary pausing of casuing its scroll to affect appBarLayout, based on https://stackoverflow.com/a/45338791/878126 */
class MyRecyclerView #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 (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
}
override fun fling(velocityX: Int, velocityY: Int): Boolean {
var velocityY = velocityY
if (!mAppBarTracking!!.isAppBarIdle()) {
val vc = ViewConfiguration.get(context)
velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
else vc.scaledMinimumFlingVelocity
}
return super.fling(velocityX, velocityY)
}
}
MyAppBarBehavior.java
/**
* Attach this behavior to AppBarLayout to disable the bottom portion of a closed appBar
* so it cannot be touched to open the appBar. This behavior is helpful if there is some
* portion of the appBar that displays when the appBar is closed, but should not open the appBar
* when the appBar is closed.
*/
public class MyAppBarBehavior extends AppBarLayout.Behavior {
// Touch above this y-axis value can open the appBar.
private int mCanOpenBottom;
// Determines if the appBar can be dragged open or not via direct touch on the appBar.
private boolean mCanDrag = true;
#SuppressWarnings("unused")
public MyAppBarBehavior() {
init();
}
#SuppressWarnings("unused")
public MyAppBarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setDragCallback(new AppBarLayout.Behavior.DragCallback() {
#Override
public boolean canDrag(#NonNull AppBarLayout appBarLayout) {
return mCanDrag;
}
});
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent,
AppBarLayout child,
MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// If appBar is closed. Only allow scrolling in defined area.
if (child.getTop() <= -child.getTotalScrollRange()) {
mCanDrag = event.getY() < mCanOpenBottom;
}
}
return super.onInterceptTouchEvent(parent, child, event);
}
public void setCanOpenBottom(int bottom) {
mCanOpenBottom = bottom;
}
}
I have a BottomSheetDialogFragment with a RecyclerView. The problem is, I want to disable the drag close function of the BottomSheetDialogFragment as long as the RecyclerView is not scrolled up (currently I can't scroll my RecyclerView as the attempt will always close the BottomSheetDialogFragment). Any ideas how to achieve this?
RecyclerView scrolling problem in BottomSheetDialog can be solved by this way.
from: https://android.jlelse.eu/recyclerview-within-nestedscrollview-scrolling-issue-3180b5ad2542
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.widget.NestedScrollView
android:id="#+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
Try this in your Fragment that extends RecyclerView in onCreateView
recyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
v.getParent().requestDisallowInterceptTouchEvent(true);
v.onTouchEvent(event);
return true;
}
});
Just add one (!) attribute to your layout:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/receiversList"
android:nestedScrollingEnabled="false"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:nestedScrollingEnabled="false" doing all the job.
Also, if you wrap around your RecyclerView with SwipeRefreshLayout - refresh function will continue to work. And even you put your layout with more complex Views, your BottomSheetDialog will continue to support drag-down-to-dismiss behaviour (If touch with swipe down gesture occurs outside of RecyclerView & SwipeRefreshLayout).
Just treat it as a BottomSheetDialog , and simply disable its dragging or sliding when touch .
When create a BottomSheetDialog , it will automatically wrap your layout in a
CoordinatorLayout , so if you want to get a behaviour from your view , call
final BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent());
Then through this behaviour, you can do what you need.
final BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent());
behavior.setHideable(false);
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
This worked for me.Hope it helps.
When recycle view has scope to scroll, bottom sheet will remain expanded. But if recycle view has no scroll to up direction, than drag from up, will revert normal behaviour.
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
if(!recyclerView.canScrollVertically(1)) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
try this solution :
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/layout_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<!-- elements-->
<!-- elements-->
</LinearLayout>
</androidx.core.widget.NestedScrollView>
Change the Behaviour in the BottomSheetDialogFragment in setupDialog method:
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams();
final CoordinatorLayout.Behavior behavior = layoutParams.getBehavior();
if (behavior != null && behavior instanceof BottomSheetBehavior) {
((BottomSheetBehavior) behavior).setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
((BottomSheetBehavior) behavior).setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
}
I had the same situation and what worked for me is just a single line of code inside onCreate function.
What you have to do is just enable recyclerViews nested scrolling.
recyclerView.setNestedScrollingEnabled(true);
I had the same problem recently on my project and I have tried all the answer suggested in this and in other threads on SO but without luck so I decided to solve it my way.
Just a note, my situation it's that I have a BottomSheetDialog with a complex layout that result in something like this
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="_mainLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id=" />
<LinearLayout
android:id="">
<ImageView
android:id=""/>
<LinearLayout
android:id="">
<TextView
android:id="" />
<TextView
android:id=""/>
</LinearLayout>
<LinearLayout
android:id="">
<TextView
android:id=""/>
</LinearLayout>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:layout_marginTop="10dp"
android:id=""
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="">
<Button
android:id="">
</LinearLayout>
Basically what it's needed it's to add a BottomSheetBehavior to the main layout of the dialog
_bottomSheetBehavior = BottomSheetBehavior.From((View)_mainLayout.Parent);
once we have this we create a custom BottomSheetCallback implementation and we add it to the behavior
_bottomSheetCallback = new BottomSheetFullScreenCallback(_bottomSheetBehavior);
_bottomSheetBehavior.AddBottomSheetCallback(_bottomSheetCallback);
last step it's to create a custo OntouchListener that we will add to the RecyclerView
_recyclerView.SetOnTouchListener(new MyTouchListener(_bottomSheetBehavior));
Now we have everything in place and we just need to manage the touch on the recyclerView. So in our custom OnTouchListener we implement the OnTouch method in this way
public bool OnTouch(View view, MotionEvent e)
{
if (e.Action == MotionEventActions.Down || e.Action == MotionEventActions.Move)
{
_bottomSheetBehavior.Draggable = false;
}
if (e.Action == MotionEventActions.Up)
{
_bottomSheetBehavior.Draggable = true;
}
return true;
}
Once we have done this we need to be sure that the OnSlide event of the custom BottomSheetCallback will never be called once the bottomSheetBehavior it's not draggable, and we can do this in the following way:
public override void OnSlide(View bottomSheet, float slideOffset)
{
if (_bottomSheetBehavior.Draggable)
{
OnSlideEvent?.Invoke(this, slideOffset);
}
}
And that's it ;)
To scroll RecyclerView inside BottomSheet can use this way:
Add android:focusableInTouchMode="true" to RecyclerView in file XML.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
android:clipToPadding="false"
android:focusableInTouchMode="true"
android:paddingBottom="50dp" />
- In func onCreateDialog for BottomSheet:
+ where: maxDesiredHeight is set fixed size of BottomSheet.
+ this.isDraggable = false -> disabling dragging on BottomSheet.
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setOnShowListener {
dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)!!
.apply {
val maxDesiredHeight = (resources.displayMetrics.heightPixels * 0.95).toInt()
val bottomSheetLayoutParams = this.layoutParams
bottomSheetLayoutParams.height = maxDesiredHeight
this.layoutParams = bottomSheetLayoutParams
BottomSheetBehavior.from(this).apply {
this.state = BottomSheetBehavior.STATE_EXPANDED
this.isDraggable = false
}
}
}
return dialog
}
- Call the setOnTouchListener function for RecyclerView:
Recycler.setOnTouchListener { v, event ->
v.parent.requestDisallowInterceptTouchEvent(true)
v.onTouchEvent(event)
true
}
With Constraint layout
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_team_mates"
android:layout_width="#dimen/dp_0"
android:layout_height="#dimen/dp_0"
android:layout_marginVertical="#dimen/dp_20"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/txt_title_custom"
app:layout_constraintBottom_toBottomOf="parent"/>
Put your RecyclerView under NestedScrollView where NestedScroll is parent of recycler