Using backdrop on Android - 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)

Related

How to Allow outside touch for BottomSheetDialog?

I am working on BottomSheetDialogFragment my requirement is to create Bottom menu,
Where if I click outside fragment area it should not cancel the Dialog and should persist.
ISSUE:
And Event outside the Fragment should propagate to the lower fragment view/fragment.
I have already tried below(doesn't work for BottomDialogFragment):
Allow outside touch for DialogFragment
To stop the dialog cancel i tried Below(i call setCancelable(boolean) in onStart() of BottomDialogFragment):
#Override
public void setCancelable(boolean cancelable) {
super.setCancelable(cancelable);
BottomSheetDialog dialog = (BottomSheetDialog) getDialog();
dialog.setCanceledOnTouchOutside(cancelable);
View bottomSheetView = dialog.getWindow().getDecorView().findViewById(R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheetView).setHideable(cancelable);
}
reference
EDIT: Found it the hard way there is no other go then using Coordinate layout.The best solution for BottomSheetDialog is here
This Solution solve's the issue but bring's in one more issue. i.e all the actionMode event's are not navigated while all other app event's are.
And this is my best solution to the problem
Try code below in your BottomSheetDialog:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return (super.onCreateDialog(savedInstanceState) as BottomSheetDialog).apply {
setCanceledOnTouchOutside(false)
}
}
or wrap by <CoordinatorLayout> for instance your <ConstraintLayout> and implement <layout /> and attach to BottomSheetBehavior.
As been said by Pankaj Kumar, this is not possible by default. However, I found a workaround that works and allows touches to views outside of the bottom sheet while keeping the bottom sheet open
You can override the layout of the BottomSheetDialog like so:
values/refs.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<item name="design_bottom_sheet_dialog" type="layout" tools:override="true">#layout/custom_design_bottom_sheet_dialog</item>
</resources>
layout/custom_design_bottom_sheet_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This is an override of the design_bottom_sheet_dialog from material library
-->
<FrameLayout
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="wrap_content">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/coordinator"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="#+id/touch_outside"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"
tools:ignore="UnusedAttribute"/>
<FrameLayout
android:id="#+id/design_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
app:layout_behavior="#string/bottom_sheet_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>
YourCustomBottomSheetDialogFragment
override fun onStart() {
super.onStart()
// Set layout for custom bottom sheet by allowing background touches
dialog?.window?.apply {
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
attributes = attributes.apply {
gravity = Gravity.BOTTOM
}
setDimAmount(0.0f)
}
}
By doing this, the dialog has a wrap_content height and the flags allow touches to be handled by views outside of this dialog
you should use android.support.design.widget.BottomSheetBehavior.
but if you want to have a bottomSheet in other class I suggest you use a Fragment and in this fragment open your bottomSheet
open your fragment in this way.
And in your fragment, open your bottomSheet in the following code:
in onInitViews
var mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheetCoordinatorLayout)
mBottomSheetBehavior!!.state = BottomSheetBehavior.STATE_HIDDEN
mBottomSheetBehavior!!.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
when (newState) {
BottomSheetBehavior.STATE_HIDDEN -> {
fragmentManager?.popBackStack()
}
//BottomSheetBehavior.STATE_COLLAPSED -> "Collapsed"
//BottomSheetBehavior.STATE_DRAGGING -> "Dragging..."
//BottomSheetBehavior.STATE_EXPANDED -> "Expanded"
//BottomSheetBehavior.STATE_SETTLING -> "Settling..."
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
//text_view_state!!.text = "Sliding..."
}
})
And your layout should be like this:
<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:layoutDirection="ltr">
<android.support.design.widget.CoordinatorLayout
android:id="#+id/bottomSheetCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:behavior_peekHeight="55dp"
app:layout_behavior="#string/bottom_sheet_behavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/top_radius_primary_color"
android:paddingStart="#dimen/material_size_32"
android:paddingEnd="#dimen/material_size_32">
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
</android.support.design.widget.CoordinatorLayout>
I hope it helps you
This is not possible till you are using BottomSheetDialogFragment. BottomSheetDialogFragment is a dialog and as behaviour of every dialog, it does not allow user interception on any view behind the dialog, although that is visible to user.
So to achieve this you need to use Fragment instead of BottomSheetDialogFragment. And yes it will require lot of code changes :) and you have to live without BottomSheetDialogFragment if you want to intercept views behind.

Collapsing CardView Animation not working correctly

What I'm trying to do
I have a RecyclerView with many items that are basically some CardView.
Those cards have a supporting text in the middle of their bodies, which has the visibility set to GONE by default, and it's made VISIBLE when I click the arrow on the right of the card.
I'm trying to animate the card while the text is revealed and while it's collapsed.
The picture below shows the expanded card and the collapsed one:
The CardView layout (I've removed some parts for readability):
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="3dp"
card_view:cardElevation="4dp"
card_view:cardUseCompatPadding="true"
android:id="#+id/root">
<LinearLayout
android:id="#+id/item_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="#dimen/activity_vertical_margin">
<!-- The header with the title and the item -->
<TextView
android:id="#+id/body_content"
style="#style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_marginBottom="8dp"
android:text="#string/about_page_description"
android:textColor="#color/secondaryText"
android:visibility="gone"/>
<!-- The divider, and the footer with the timestamp -->
</LinearLayout>
</android.support.v7.widget.CardView>
The problem
The animations is working when the card is expanding and revealing the body TextView, however, when I try to collapse it back, the cards below the animated one overlaps the first one.
Example:
What I've tried so far
I've already asked a similar question about this behavior here before, but that solution is not working for a TextView in the middle of the card.
The code that's responsible for the animation part is inside the RecyclerView adapter. The arrow has a click listener that calls the method below:
private fun toggleVisibility() {
if (bodyContent.visibility == View.GONE || bodyContent.visibility == View.INVISIBLE) {
btSeeMore.animate().rotation(180f).start()
TransitionManager.beginDelayedTransition(root, AutoTransition())
bodyContent.visibility = View.VISIBLE
}
else {
btSeeMore.animate().rotation(0f).start()
TransitionManager.beginDelayedTransition(root, AutoTransition())
bodyContent.visibility = View.GONE
}
}
Where root is my CardView.
I've also tried to use the LinearLayout instead of the card itself for the delayed transition, but that didn't work either.
How can I achieve that behavior for my layout?
You will have to perform the transition on the RecyclerView, not on individual items. Otherwise, the RecyclerView layout changes aren't taken into account by the auto transition, because it will only look at what changes in that very child view, even though in fact, other ViewHolders are indirectly affected (layout parameters are changing).
So, instead of passing "root" (the item view) to TransitionManager#beginDelayedTransition, pass a reference to your RecyclerView
You have to apply TransitionManager.beginDelayedTransition on the root view where the cardview is contained
You have to remove android:animateLayoutChanges="true" from all over the layout
TransitionManager.beginDelayedTransition(the_root_view_where_card_view_exist, new AutoTransition());
RecyclerView does behave oddly if his items are resizing outside RecyclerViews callbacks. Try using adapter.notifyItemChanged(position, payload) and updating the item then:
Replace adapter's onclick with this:
adapter.notifyItemChanged(adapterPosition, true) // needs adapter reference, can use more meaningful payload
Then inside of your adapter:
override fun onBindViewHolder(holder: Holder, position: Int, payloads: List<Any>) {
if (payloads.isEmpty())
onBindViewHolder(holder, position)
else
holder.toggleVisibility()
}
You can also see what happens when running delayedTransition on LinearLayout instead of Card itself.
This won't be perfect, but it will trigger animation of following items instead of them jumping and clipping.
I would recomment you to use Animator framework and apply height animation to your TextView.
Here is a nice library you can use: https://github.com/cachapa/ExpandableLayout
I also suggest you to check it's source code, it uses the Animators
Maybe this is too late.
Inside onBindViewHolder() include this
holder.view.btSeeMore.setOnClickListener { view ->
val seeMore = (bodyContent.visibility != View.VISIBLE)
view.animate().rotation(if (seeMore) 180f else 0f).start()
bodyContent.visibility = if (seeMore) View.VISIBLE else View.GONE
}

How to set Bottom Sheet peek height to show only certain parts initially?

Let's say that my Bottom Sheet has lines of widgets like the following. If I want to show only the first two lines (i.e., the first two LinearLayouts) initially, but not the rest of the widgets below. I do not want those to be seen initially. How can I set the correct peek height? Hard-coding app:behavior_peekHeight probably would not work, so I would need to set it programatically, but how to calculate the height?
Or is there a more recommended way to get the same result? I mean, if I test Google Maps, long pressing a location first shows only the title part as the bottom sheet, but when I try to scroll up the bottom sheet, it feels as if the title part (which might not have been a real bottom sheet) is replaced by a real bottom sheet that contains all the elements. If my explanation is not enough, please try Google Maps yourself.
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottom_sheet"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<android.support.v7.widget.AppCompatSpinner/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<TextView/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView/>
<TextView/>
</LinearLayout>
<android.support.v7.widget.RecyclerView/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
I would solve this by using a ViewTreeObserver.OnGlobalLayoutListener to wait for your bottom sheet to be laid out, and then calling BottomSheetBehavior.setPeekHeight() with the y-coordinate of the first view you don't want to see.
private BottomSheetBehavior<View> behavior;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View bottomSheet = findViewById(R.id.bottomSheet);
behavior = BottomSheetBehavior.from(bottomSheet);
final LinearLayout inner = findViewById(R.id.inner);
inner.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
inner.getViewTreeObserver().removeOnGlobalLayoutListener(this);
View hidden = inner.getChildAt(2);
behavior.setPeekHeight(hidden.getTop());
}
});
}
In this case, my bottom sheet is a NestedScrollView holding a LinearLayout that holds many TextViews. By setting the peek height to be the top of the third TextView (obtained by getChildAt(2)), my bottom sheet winds up showing exactly two TextViews while collapsed.
Customized #Ben P.'s answer to target a view id as a reference of the peekHeight and made a function:
/**
* Gets the bottom part of the target view and sets it as the peek height of the specified #{BottomSheetBehavior}
*
* #param layout - layout of the bottom sheet.
* #param targetViewId - id of the target view. Must be a view inside the 'layout' param.
* #param behavior - bottom sheet behavior recipient.
*/
private fun <T : ViewGroup> getViewBottomHeight(layout: ViewGroup,
targetViewId: Int,
behavior: BottomSheetBehavior<T>) {
layout.apply {
viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
behavior.peekHeight = findViewById<View>(targetViewId).bottom
}
})
}
}
In our use case, we needed to target the bottom part of the view, so we set it that way. It can be adjusted depending on the use-case.
That's smart!
My problem was trying to getTop() or getHeight() at wrong timing, it returns 0 if the view is not ready.
And yes, use viewTreeObserver to avoid that.
This is actually no different with #Ben P.'s previous answer, just a kotlin version:
class MyBottomSheetDialog() : BottomSheetDialogFragment(){
private val binding by viewBinding(SomeLayoutViewBinding::bind)
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(this.dialog as BottomSheetDialog).behavior.let { behavior ->
/* Set "pivotView" as interested target and make it the pivot of peek */
binding.pivotView.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.pivotView.viewTreeObserver.removeOnGlobalLayoutListener(this)
behavior.peekHeight = binding.pivotView.top
}
})
}
}
}

Add onInterceptTouchEvent() to inflated layout

I have a linear layout existing of an EditText and an ImageView.
When I click anywhere inside the linear layout I need a touch event to be dispatched to this layer, but the EditText is blocking/consuming the touch event.
I want to use onInterceptTouchEvent(ev MotionEvent) but I can't figure out how to set it on the linear layout.
<LinearLayout
android:id="#+id/consultCv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:layout_width="#dimen/grid_3x"
android:layout_height="#dimen/grid_3x"
android:layout_gravity="center"
android:src="#drawable/img_document" />
<EditText
style="#style/TextStyle.EditDisabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="#dimen/grid_1x"
android:text="#string/document" />
</LinearLayout>
class ArchiveActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
}
}
I tried to override onInterceptorTouchEvent() in this activity but it can only be overridden in a view.
Is there a way to treat my linear layout as a view and then implement this as a lambda expression?
Do I need to tear apart my XML file and then rebuild every separate view group in code?
My linear layout is inside a layout with several layers, so I can't just make a view out of it alone.
Help me Obi Wan Kenobi, you'r my only hope!

fitsSystemWindows effect gone for fragments added via FragmentTransaction

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.

Categories

Resources