fitsSystemWindows effect gone for fragments added via FragmentTransaction - android

I have an Activity with navigation drawer and full-bleed Fragment (with image in the top that must appear behind translucent system bar on Lollipop). While I had an interim solution where the Fragment was inflated by simply having <fragment> tag in Activity's XML, it looked fine.
Then I had to replace <fragment> with <FrameLayout> and perform fragment transactions, and now the fragment does not appear behind the system bar anymore, despite fitsSystemWindows is set to true across all required hierarchy.
I believe there might be some difference between how <fragment> gets inflated within Activity's layout vs on its own. I googled and found some solutions for KitKat, but neither of those worked for me (Lollipop).
activity.xml
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true">
<FrameLayout
android:id="#+id/fragment_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
</FrameLayout>
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:fitsSystemWindows="true"/>
</android.support.v4.widget.DrawerLayout>
fragment.xml
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="224dp"
android:fitsSystemWindows="true"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
...
It worked when activity.xml was this way:
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true">
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/fragment"
android:name="com.actinarium.random.ui.home.HomeCardsFragment"
tools:layout="#layout/fragment_home"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:fitsSystemWindows="true"/>
</android.support.v4.widget.DrawerLayout>

When you use <fragment>, the layout returned in your Fragment's onCreateView is directly attached in place of the <fragment> tag (you'll never actually see a <fragment> tag if you look at your View hierarchy.
Therefore in the <fragment> case, you have
DrawerLayout
CoordinatorLayout
AppBarLayout
...
NavigationView
Similar to how cheesesquare works. This works because, as explained in this blog post, DrawerLayout and CoordinatorLayout both have different rules on how fitsSystemWindows applies to them - they both use it to inset their child Views, but also call dispatchApplyWindowInsets() on each child, allowing them access to the fitsSystemWindows="true" property.
This is a difference from the default behavior with layouts such as FrameLayout where when you use fitsSystemWindows="true" is consumes all insets, blindly applying padding without informing any child views (that's the 'depth first' part of the blog post).
So when you replace the <fragment> tag with a FrameLayout and FragmentTransactions, your view hierarchy becomes:
DrawerLayout
FrameLayout
CoordinatorLayout
AppBarLayout
...
NavigationView
as the Fragment's view is inserted into the FrameLayout. That View doesn't know anything about passing fitsSystemWindows to child views, so your CoordinatorLayout never gets to see that flag or do its custom behavior.
Fixing the problem is actually fairly simple: replace your FrameLayout with another CoordinatorLayout. This ensures the fitsSystemWindows="true" gets passed onto the newly inflated CoordinatorLayout from the Fragment.
Alternate and equally valid solutions would be to make a custom subclass of FrameLayout and override onApplyWindowInsets() to dispatch to each child (in your case just the one) or use the ViewCompat.setOnApplyWindowInsetsListener() method to intercept the call in code and dispatch from there (no subclass required). Less code is usually the easiest to maintain, so I wouldn't necessarily recommend going these routes over the CoordinatorLayout solution unless you feel strongly about it.

My problem was similar to yours: I have a Bottom Bar Navigation which is replacing the content fragments. Now some of the fragments want to draw over the status bar (with CoordinatorLayout, AppBarLayout), others not (with ConstraintLayout, Toolbar).
ConstraintLayout
FrameLayout
[the ViewGroup of your choice]
BottomNavigationView
The suggestion of ianhanniballake to add another CoordinatorLayout layer is not what I want, so I created a custom FrameLayout which handles the insets (like he suggested), and after some time I came upon this solution which really is not much code:
activity_main.xml
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.app.WindowInsetsFrameLayout
android:id="#+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
WindowInsetsFrameLayout.java
/**
* FrameLayout which takes care of applying the window insets to child views.
*/
public class WindowInsetsFrameLayout extends FrameLayout {
public WindowInsetsFrameLayout(Context context) {
this(context, null);
}
public WindowInsetsFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WindowInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Look for replaced fragments and apply the insets again.
setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
#Override
public void onChildViewAdded(View parent, View child) {
requestApplyInsets();
}
#Override
public void onChildViewRemoved(View parent, View child) {
}
});
}
}

OK, after several people pointing out that fitsSystemWindows works differently, and it should not be used on every view down the hierarchy, I went on experimenting and removing the property from different views.
I got the expected state after removing fitsSystemWindows from every node in activity.xml =\

Another approach written in Kotlin,
The problem:
The FrameLayout you are using does not propagate fitsSystemWindows="true" to his childs:
<FrameLayout
android:id="#+id/fragment_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" />
A solution:
Extend FrameLayout class and override the function onApplyWindowInsets() to propagate the window insets to attached fragments:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
class BetterFrameLayout : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
childCount.let {
// propagates window insets to children's
for (index in 0 until it) {
getChildAt(index).dispatchApplyWindowInsets(windowInsets)
}
}
return windowInsets
}
}
Use this layout as a fragment container instead of the standard FrameLayout:
<com.foo.bar.BetterFrameLayout
android:id="#+id/fragment_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" />
Extra:
If you want to know more about this checkout Chris Banes blog post Becoming a master window fitter.

The other horrendous problem with dispatching of Window insets is that the first View to consume window insets in a depth-first search prevents all other views in the heirarchy from seeing window insets.
The following code fragment allows more than one child to handle window insets. Extremely useful if you're trying to apply windows insets to decorations outside a NavigationView (or CoordinatorLayout). Override in the ViewGroup of your choice.
#Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
if (!insets.isConsumed()) {
// each child gets a fresh set of window insets
// to consume.
final int count = getChildCount();
for (int i = 0; i < count; i++) {
WindowInsets freshInsets = new WindowInsets(insets);
getChildAt(i).dispatchApplyWindowInsets(freshInsets);
}
}
return insets; // and we don't.
}
Also useful:
#Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
return insets.consume(); // consume without adding padding!
}
which allows plain ordinary Views that are children of this view to be laid out without window insets.

I created this last year to solve this problem: https://gist.github.com/cbeyls/ab6903e103475bd4d51b
Edit: be sure you understand what fitsSystemWindows does first. When you set it on a View it basically means: "put this View and all its children below the status bar and above the navigation bar". It makes no sense to set this attribute on the top container.

Related

Android - Problem with fitsSystemWindows property

I need your help to understand a strange behavior. When I set the fitsSystemWindows property to 'true', the navigation bar hides some part of my layout, see the image below :
When I set to false, I have this behavior (it's OK) :
When I read the Android documentation and many posts on Stackoverflow, I understand it should be the exact opposite of this behaviour : https://developer.android.com/reference/android/view/View#attr_android:fitsSystemWindows.
The first case with fitsSystemWindows='true' should be OK and the second case should be hidden by the navigation bar, am I wrong ?
Could someone explain me what's happened ? My targetVersionSdk is 29 and I tested it on many versions (Android 6,7 10 and 11). Maybe it's specific to CoordinatorLayout ? Thanks for your explanations :)
Here is my xml layout :
<?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:id="#+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="[true or false]">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
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="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>
[...]
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginBottom="#dimen/activity_vertical_margin"
android:layout_marginEnd="#dimen/activity_vertical_margin"
android:src="#drawable/ic_arrow_forward_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I think the view is working as expected. First you need to understand the insets and how it's passed around. The default behavior of fitsSystemWindow is to consume all the insets and apply them as padding. But ViewGroups like CoordinatorLayout, DrawerLayout override this behavior.
Here is the snippet of the code in CoordinatorLayout that overrides the behavior.
private void setupForInsets() {
if (Build.VERSION.SDK_INT < 21) {
return;
}
if (ViewCompat.getFitsSystemWindows(this)) {
if (mApplyWindowInsetsListener == null) {
mApplyWindowInsetsListener =
new androidx.core.view.OnApplyWindowInsetsListener() {
#Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
return setWindowInsets(insets);
}
};
}
ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener);
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
} else {
ViewCompat.setOnApplyWindowInsetsListener(this, null);
}
}
As you can see applying fitsSystemWindow in CoordinatorLayout causes it to render the contents under the system UI. What you need to do is to add the insets provided by the system and apply it as margin or padding to the top and bottom views.
You can use setOnApplyWindowInsetsListener() to listen for insets and apply it. Let's say you havebottomNav as bottom view then you can do something like this to account for the bottom inset.
ViewCompat.setOnApplyWindowInsetsListener(bottomNav) { view, insets ->
bottomNav.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
You can learn more about insets in this blog post.

Using backdrop on Android

https://material.io/design/components/backdrop.html
I found this on Material Design, but couldn't find any resources.
Thinking about its layout, I think it's made up of any layout with material card view, and I am trying to make my activity file using layout + material card view. Is this method correct to make backdrop layout?
Also, I want to know about which layout I should use. Is RelativeLayout can be the way? I don't get it actually.
This component (BackDrop) is still under development for the Android Material Components library as of 16 December 2018.
However, if you are using Material Components already, it's not that hard to implement your own. You will need the following:
CoordinatorLayout as the root layout
a BottomSheetBehaviour applied to an immediate child of the root layout
The provided solution below, looks like the following image...
The example below uses a fragment, I'll ommit the details of the hosting activity because it is irrelevant to the question/answer. However, you can do exactly the same with an activity. Your fragment layout file will look like below...
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">
<!--This the interface sitting behind the backdrop and shown when it is collapsed-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#color/colorPrimary"
android:padding="#dimen/activity_spacing">
<EditText
android:id="#+id/searchTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="#drawable/ic_search_primary_xlight_24dp"
style="#style/EditTextStyle.Inverse.Large.Light"
android:hint="#string/search_hint"/>
<EditText
android:id="#+id/datesFilterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="#drawable/ic_calendar_primary_xlight_24dp"
style="#style/EditTextStyle.Inverse.Large.Light"
android:hint="#string/select_dates_hint"/>
</LinearLayout>
<!--This is the backdrop's content with a BottomSheetBehaviour applied to it-->
<LinearLayout
android:id="#+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_peekHeight="56dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!--This is the backdrop's header with a title and icon-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clickable="true"
android:background="#drawable/ic_list_header_background"
android:padding="#dimen/activity_spacing"
android:elevation="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="#style/TextAppearance.Stems.Body2"
android:text="0 items(s)"/>
<ImageView
android:id="#+id/filterIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_filter_black_24dp"
android:layout_gravity="end"/>
</LinearLayout>
<!--And finally this is the body of the backdrop's content. You can add here whatever you need inside a view group (LinearLayout, RelativeLayout, SwipeRefreshLayout, ConstraintLayout, etc.)-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorBackground">
<!--The content's body goes here-->
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
There's a couple of things you need to be aware of here. First, the LinearLayout that sits behind the backdrop its using the colorPrimary color which exactly the same as the Toolbar's background color...the toolbar has been ommitted for clarity, it is declared in the hosting activity (remember, this solution is for a fragment).
Then the fragment's class will look like this...
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
coordinatorLayout = (CoordinatorLayout)inflater.inflate(R.layout.fragment_hazards, container, false);
Context context = getContext();
if(context != null){
setTitle(context.getString(R.string.title_hazards));
}
filterIcon = coordinatorLayout.findViewById(R.id.filterIcon);
LinearLayout contentLayout = coordinatorLayout.findViewById(R.id.contentLayout);
sheetBehavior = BottomSheetBehavior.from(contentLayout);
sheetBehavior.setFitToContents(false);
sheetBehavior.setHideable(false);//prevents the boottom sheet from completely hiding off the screen
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);//initially state to fully expanded
filterIcon.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
toggleFilters();
}
});
return coordinatorLayout;
}
private void toggleFilters(){
if(sheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED){
sheetBehavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED);
}
else {
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
And that's it, the only thing you need to keep in mind is that root layout has to be a CoordinatorLayout and that the BottomSheetBehaviour has to be applied to an immediate child of the root layout
Round Corners
You will also notice that I'm not using a CardView in the BackDrop's header to get the nice rounded corners the CardView comes with. That's because I only need the top corners to be rounded and the default implementation of CardView doesn't allow you to explicitly set individual corners. Instead, I used a good old LinearLayout and provided my own drawable for its background (ic_list_header_background). Here's the xml declaration of this drawable...
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#color/colorBackground" />
<corners android:topLeftRadius="16dp" android:topRightRadius="16dp" />
</shape>
Nothing really fancy, just a rectangular shape with selective rounded corners (the top ones)
Toolbar's Drop Shadow
You will want to remove the ToolBar's drop shadow, to do so, you can set its elevation to 0dp or programmatically remove the outline provider on the parent AppBarLayout as below...
appBarLayout.setOutlineProvider(null);
obviously, this is assuming that your Toolbar is inside an AppBarLayout as per the guidelines
I hope this really helps someone out there while the Material Component's BackDrop is still under development. It's not perfect because you are still bound to the functionalities exposed by the BottomSheetBehaviour component that it's quite limited. But if you are picky or want to go fancy, I'd recommend implementing your own BottomSheetBehaviour by extending the default one
Disabling user's swipe gesture
Based on Material Design Guidelines, it is recommended not to use swipe gestures on the front layer of the backdrop
Don’t use the swipe gesture on the front layer to reveal the back layer.
However, by default, the BottomSheetBehaviour doesn't expose any properties or APIs to disable swipe gestures. To achieve that, you will need to implement your own by extending the BottomSheetBehaviour overriding all gesture-related methods. Here's an example I'm using in one of my projects (written in Kotlin)
class GestureLockedBottomSheetBehavior<V: View>(context: Context, attributeSet: AttributeSet?) : BottomSheetBehavior<V>(context, attributeSet){
constructor(context: Context):this(context, null)
override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean = false
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) { }
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) { }
override fun onNestedFling(
coordinatorLayout: CoordinatorLayout,
child: V,
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean
): Boolean = false
}
Even if you're not familiar with Kotlin it shouldn't be hard to figure out that all I'm doing is overriding a bunch on methods and return false or doing nothing by not calling the super class's counterpart
Then to use this GestureLockedBottomSheetBehavior class, you will need to replace it in your layout as below...
<LinearLayout
android:id="#+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_peekHeight="56dp"
app:layout_behavior="ui.behaviors.GestureLockedBottomSheetBehavior">
...
</LinearLayout>
Just make sure the fully-qualified name is set according to the package your custom class resides in.
It is under development right now (Backdrop github page).
Code & how to.. would be available once it's developed. So, right now you have to create your own customized backdrop or wait for it.
I'll suggest if you want to do it, then take FrameLayout and add some CardView
in it with some margins to get look like backdrop, add some animations on transitions to it & your custom backdrop would be ready.
I'd like to add a small modification to Leo's answer:
I'm using a RecyclerView inside a LinearLayout. This LinearLayout is my bottomsheet. Using the GestureLockedBottomSheetBehavior as suggested by Leo does not allow the RecyclerView to scroll and the following is what I did to overcome that problem.
This is the custom bottomsheet behavior class that I used finally to make the RecyclerView scroll.
class GestureLockedBottomSheetBehavior<V: View>(context: Context, attributeSet: AttributeSet) : BottomSheetBehavior<V>(context, attributeSet){
override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean = false
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: V,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean = false
}
With this my RecyclerView is scrolling. But it scrolls only when the state of the bottom sheet is STATE_EXPANDED and the RecyclerView does not scroll when the state of the bottom sheet is STATE_HALF_EXPANDED.
If anyone knows how to solve this, it would be very helpful.
Our implementation using Linear Layout and Bottom Sheet Behavior here:
https://github.com/keikenofficial/keiken-android/tree/master/app/src/main/java/com/keiken/view/backdrop
https://user-images.githubusercontent.com/11440565/64996052-26b0be00-d8dd-11e9-95f8-f643ec68e679.gif
https://user-images.githubusercontent.com/11440565/64996027-1567b180-d8dd-11e9-9f50-549479ff0480.gif
https://user-images.githubusercontent.com/11440565/64996019-100a6700-d8dd-11e9-8592-1c9d55b439ce.gif
https://user-images.githubusercontent.com/11440565/64996044-1d275600-d8dd-11e9-8fd1-8780859c74e1.gif)

SwipeRefreshLayout, when visibility == View.GONE, doesn't hide child view

I'm getting inconsistent behavior with SwipeRefreshLayout.setVisibility(View.GONE). Sometimes, my swipe refresh layouts hide the child view, but some times they don't. Since SwipeRefreshLayout subclasses ViewGroup, I'm expecting it to always hide the child view whenever it's visibility is .GONE, but that isn't happening.
Any insights are appreciated.
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/mySwipeRefreshLayout">
<View
android:background="#FF0000"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
Update: After spending the afternoon stripping everything out of the fragment, I confirmed that it's our setup that is causing the problem. Even so, it's still strange that setting the child view to VIEW.Gone works, but doing so to the swipe refresh layout doesn't hide the child.
I feel dumb, but my problem ended up being this: SwipeRefreshLayout doesn't hide while it is animating the refresh logo. You have to setRefreshing(false) in addition to setVisibility(View.GONE). Even after setRefreshing(false), there is an exit animation that happens after you use both of these methods.
Here's the solution I used to fix this. It not only handles calling setRefreshing(false) when you want to hide it, but it also sets the alpha to zero so it hides immediately without having to wait for the refresh animation to wind down.
public class HideableSwipeRefreshLayout extends SwipeRefreshLayout {
public HideableSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public void setVisibility(int visibility) {
if ((visibility == View.INVISIBLE) || (visibility == View.GONE)) {
setAlpha(0);
setRefreshing(false);
} else {
setAlpha(1);
}
super.setVisibility(visibility);
}
}
If you can confirm that this is not caused by your setup, you should report this as a bug, see https://source.android.com/source/report-bugs.html.
As a workaround I recommend to wrap your layout into a FrameLayout and set the visibility on it instead of the SwipeRefreshLayout.
In your xml file provide id to SwipeRefreshLayout and view
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/mySwipeRefreshLayout">
<View
android:background="#FF0000"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
then use id of both in .java file and use their objects.
if(swipeRefreshLayout.isRefreshing()){
view.setVisibility(View.VISIBLE);
}else{
view.setVisibility(View.INVISIBLE);
}

RecyclerView that does not scroll and shows all items

I have a RecyclerView (and some other views) in a ScrollView. Currently the RecyclerView is laid out as very small (it shows 2 items out of 5 that it contains) and it scrolls independently of the ScrollView, which is obviously not great UX. I would like to get the RecyclerView to not scroll and to extend so that all its items are visible.
(I know it's stupid to use a RecyclerView in this case. I'm only doing this because somewhere else in the app I need a normal RecyclerView with scrolling etc. but the same kind of content, and I don't want to duplicate code).
It’s pretty simple, simply set the RecyclerView’s height to wrap_content.
You might also benefit from disabling nested scrolling on the recycler view, like so:
RecyclerView recycler = (RecyclerView) findViewById(R.id.recycler);
recycler.setNestedScrollingEnabled(false);
The solution of setNestedScrollingEnabled(false) isn't as full as it should: you need to use NestedScrollView instead of ScrollViewfocusableInTouchMode="true" to the child of the NestedScrollView .
If you insist on using ScrollView, you should also set minHeight to the RecyclerView, and also set overScrollMode="never" . In this case, it still isn't a good solution because the minHeight might not be enough in some cases
Other alternative solutions that you should consider:
Replace the ScrollView&RecyclerView with a single RecyclerView, which has views with additional view type for what you had in the ScrollView
Use GridLayout or another layout instead.
Maybe it is not completely clear at first sight what to do with all these answers.
I just tried them and the working one is:
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/person_properties"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
...
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
No need to change anything programmatically.
In your activity.xml file
<androidx.core.widget.NestedScrollView
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=".ActivityName">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/RecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false">
</androidx.recyclerview.widget.RecyclerView>
</androidx.core.widget.NestedScrollView>
In RecyclerView use android:nestedSrollingEnabled="false" and use NestedScrollView as a parent Scroll View.
If you are using RecyclerView inside ScrollView then Replace ScrollView with NestedScrollView and enable the nested scrolling
android:nestedScrollingEnabled="false"
This Solved my problem
An easy way is to use in your Custom Adapter, inside the onBindViewHolder method this line: holder.setIsRecyclable(false);
I realised that I use BottomNavigationView which blocked my recycler view from displaying the last item. I fixed it by adding paddingBottom to it:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recipient_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="70dp"
app:layout_constraintTop_toBottomOf="#id/view"
/>
Also try to play with:
android:overScrollMode
You should replace your scrollView for androidx.core.widget.NestedScrollView with matchparent, its simple and work fine.
Following is the code for disabling scroll in the recycler-view, and showing all the items in your layout. It might work:
public class NoScrollRecycler extends RecyclerView {
public NoScrollRecycler(Context context){
super(context);
}
public NoScrollRecycler(Context context, AttributeSet attrs){
super(context, attrs);
}
public NoScrollRecycler(Context context, AttributeSet attrs, int style){
super(context, attrs, style);
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev){
//Ignore scroll events.
if(ev.getAction() == MotionEvent.ACTION_MOVE)
return true;
//Dispatch event for non-scroll actions, namely clicks!
return super.dispatchTouchEvent(ev);
}
}
use like this way:
<com.example.custom.NoScrollRecycler
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/color_white"/>
probably parent of recyclerView is a constraintLayout changed it to RelativeLayout
I also had a recycler view inside a scrollview.
Using NestedScrollView, "height=wrap content" on recycler view, and "nestedScrollingEnabled=false" on recycler view worked.
However I had to go a step further since my recycler view data and height changed after layout:
recylerview.measure(View.MeasureSpec.makeMeasureSpec(recylerview.width,View.MeasureSpec.EXACTLY), View.MeasureSpec.UNSPECIFIED)
val height = recylerview.measuredHeight
recylerview.layoutParams.height = height

How to data bind to a header?

I have a main activity with a side navigation drawer in the action bar, specified as follows (note that a lot of code has been omitted for brevity) in default_screen.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="190dp"
android:background="#drawable/honeycomb"
android:orientation="vertical"
>
<android.support.design.widget.NavigationView
android:id="#+id/navigation_view"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
app:headerLayout="#layout/header"
app:menu="#menu/drawer"
/>
where layout/header is as follows (again, a bunch of lines omitted for brevity):
<data>
<variable name="user" type="oose2017.place2b.ClientUser"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.displayName}"
android:textSize="14sp"
android:textColor="#FFF"
android:textStyle="bold"
android:gravity="left"
android:paddingBottom="4dp"
android:id="#+id/username"
android:layout_above="#+id/email"
android:layout_alignLeft="#+id/profile_image"
android:layout_alignStart="#+id/profile_image" />
</RelativeLayout>
Where I instantiate my default_screen in the main activity as follows:
setContentView(R.layout.default_screen);
How do I data bind to the header? I have tried a few things unsuccessfully, mainly:
DefaultScreenBinding binding = DataBindingUtil.setContentView(R.layout.default_screen);
Which does not work. How can I do this?
Valid solution will be following, add header view in main activity OnCreate:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
NavHeaderMainBinding _bind = DataBindingUtil.inflate(getLayoutInflater(), R.layout.nav_header_main, binding
.navView, false);
binding.navView.addHeaderView(_bind.getRoot());
_bind.setUser(Session.getUserProfile());
}
Note:
Important line is binding.navView.addHeaderView(_bind.getRoot()); remove app:headerLayout="#layout/nav_header_main" from xml add programmatically.
You can bind any view with your parent binding if that view is your child of that parent. Like here: Navigation view header is a child of Navigation view so to bind navigation view header with navigation view need to like this.
default_screen binding for Main screen:
DefaultScreenBinding binding = DataBindingUtil.setContentView(R.layout.default_screen);
for header layout including binding will be:
HeaderBinding headerBinding = HeaderBinding.bind(binding.navigationView.getHeaderView(0));
binding.navigationView.getHeaderView(0) will give header view of navigation view which you want to bind.
Now you can use headerBinding for header layout references. Hope this you understand easily and get helpful.
With viewBinding
// navView is NavigationView
val viewHeader = binding.navView.getHeaderView(0)
// nav_header.xml is headerLayout
val navViewHeaderBinding : NavHeaderBinding = NavHeaderBinding.bind(viewHeader)
// title is Children of nav_header
navViewHeaderBinding.title.text = "Your Text"
Not sure if this is the best way but this is what worked for my scenario.
I created a custom view for NavigationView (extended the class) and then used DataBindingUtil.inflate as part of the custom view constructor(s) with which I set my data binding variable(s) and add then added that view as the header using NavigationView.addHeaderView. Of course this meant in xml I had to replace the NavigationView with my custom view and not specify the app:headerLayout attribute in the custom view. See custom view example below (note that I use Dagger2 to inject my data binding variable).
public class MyNavigationView extends NavigationView {
#Inject
MyViewModel myViewModel;
public MyNavigationView(Context context) {
super(context);
initialize();
}
public MyNavigationView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public MyNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
// NOTE: A private method that "injects" your view model dependency (ex: Using Dagger2)
inject();
NavHeaderHomeBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()),
R.layout.nav_header_home,
null,
false);
binding.setHomeNavDrawerViewModel(myViewModel);
addHeaderView(binding.getRoot());
}
}
My solution is following:
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="#layout/navigation"
app:menu="#menu/activity_drawer_drawer">
<include
layout="#layout/nav_header_drawer"
bind:thisDevice="#{thisDevice}" />
In order to use data binding, we can't use original headLayout so we use an 'included layout'. In this way, our header can show normally by data binding. But, the menu without a header layout will be overlapped. So we add an empty headLayout has the same size with our real layout to make menu show normally.
Detailed explanation can be found in my blog post.
Working example can be found here.
Notify me if I am not clear.
It seems no direct way to make data binding for NavigationView, so I have to implement it in somewhat hacker way:
First, in order to use bind, we can’t use direct headerLayout and replace it with a included layout
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="#layout/nav_header_drawer"
app:menu="#menu/drawer">
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="#menu/drawer">
<include layout="#layout/nav_header_drawer"
bind:thisDevice="#{thisDevice}" />
</android.support.design.widget.NavigationView>
The newly included view is on the top of menu, so it will show normally. But part of menu items will move up because there is no header above it and are overlapped by newly included view( although those items can receive the touch event), so we can add a header.
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="#layout/navigation"
app:menu="#menu/activity_drawer_drawer">
<include
layout="#layout/nav_header_drawer"
bind:thisDevice="#{thisDevice}" />
</android.support.design.widget.NavigationView>
navigation layout is an empty layout having the same height and width with real nav_header_drawer. So both menu our real layout is shown normally.
Of course, the java code is necessary for data binding:
ActivityDrawerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_drawer);
binding.setThisDevice(Device.now);
Layout file is here.
Working example can be found here.
Reference: http://tonyz93.blogspot.com.br/2016/08/learn-data-binding-of-android.html#navigationview-data-binding
in kotlin
<com.google.android.material.navigation.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="#layout/nav_header_main"
app:menu="#menu/activity_main_drawer" />
where layout/header
<de.hdodenhof.circleimageview.CircleImageView
android:id="#+id/nav_user_image"
android:layout_width="#dimen/nav_image_size"
android:layout_height="#dimen/nav_image_size"
android:layout_marginStart="#dimen/nav_content_margin_StartEnd"
android:layout_marginEnd="#dimen/nav_content_margin_StartEnd"
app:civ_border_color="#android:color/white"
app:civ_border_width="#dimen/nav_image_circular_border_width"
android:contentDescription="#string/image_contentDescription"
android:src="#drawable/ic_user_place_holder" />
With viewBinding
val viewHeader = binding?.navView?.getHeaderView(0)
val headerBinding = viewHeader?.let { NavHeaderMainBinding.bind(it) }
headerBinding?.tvUsername?.text = user.name

Categories

Resources