I'm using a BottomSheetDialogFragment with a custom layout. I'm trying to have the following setup:
<TextView> -> pinned to the top of the bottom sheet
<RecyclerView> -> wrap_content
<Button> -> pinned to the bottom of the bottom sheet
Both TextView and Button must be visible at all time (sticky), while the RecyclerView should stay in the middle and scroll without obscuring other views.
This is my layout so far:
<androidx.constraintlayout.widget.ConstraintLayout 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">
<TextView
android:id="#+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Title"
app:layout_constraintBottom_toTopOf="#id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="#id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/title" />
<Button
android:id="#+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
This is what it looks like with a small list of items, where the RecyclerView has no need to scroll.
This is what it looks like with a large list of items.
The title stays pinned to the top, but the button doesn't.
The button is actually not even visible, even if I scroll down all the way.
What's strange to me is that this same layout works with a regular full screen activity, but it somehow fails with a BottomSheetFragment.
I've already looked at other posts, but none of them helped e.g.
RecyclerView (wrap_content) inside of a BottomSheetDialogFragment
The height of recycler view shouldn't be wrap_content
If you want recyler in-between your title and footer, the better approach is set height = 0 and pin its top to the bottom title and its bottom to the top of footer (like you already did), it will auto stretch for you
What ended up being the solution is setting the state of bottom sheet to expanded e.g.
val bottomSheetDialog = requireDialog() as BottomSheetDialog
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
I suppose that the bottom sheet never expanded fully, so the layout was never fully visible. I thought that the bottom sheet would expand automatically based on the height of the content, but I was wrong. The layout itself is fine, I didn't have to make any changes to it.
Related
I have a layout activity in my project in which I am using a constraint layout and basically only 3 view, first is custom toolbar second a Recycler View and last one is a fragment view. I constrained recycler view to top toolbar and top of fragment view but I goes below fragment and even under screen.
This is my layout code.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:theme="#style/Theme.Music.Font">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="#+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="#font/bold"
android:text="#string/app_name"
android:textSize="26sp" />
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:searchIcon="#drawable/search_icon" />
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbarThumbVertical="#drawable/scroll_bar"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar" />
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nowPlaying"
android:name="com.example.music.NowPlaying"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:layout="#layout/fragment_now_playing" />
</androidx.constraintlayout.widget.ConstraintLayout>
And when I constrained with fragment also it goes on top overriding toolbar.
As shown in picture.
When you use wrap_content for the height or width, you're saying the view should expand to be as large as necessary to "display" the content. "Display" in air quotes because if there's a lot of content, parts of the view might end up off the screen.
Your constraints don't limit the size here, just the positioning. If you constrain one edge to something, you'll pin it, and it will expand off the screen at the other end. If you constrain opposite edges, then you're basically centering it around those constraints.
If you want to match those constraints, you need to set the height to 0dp. That way your constrained edges will actually be pinned where you want them, and that makes the view a certain height. It fits the space between its constrained edges.
Looking at your layout, I'm guessing the "now playing" part at the bottom is your FragmentContainerView. That's set to wrap_content but its contents seem to be a fixed size, so that's fine - exactly what you want! Same goes for the toolbar - it's wrap_content, but its contents are a fixed size.
So your RecyclerView also needs its bottom to be constrained to the top of nowPlaying, and it needs a layout_height of 0dp. That way, it's constrained to the space between the toolbar and the now playing bit, and its size fills that area. The other two take up as much space as they need, and the RecyclerView fills what's left - which is what you want, right?
Generally this is how it works for anything that scrolls, like a RecyclerView or ScrollView - the view in the layout is a window into the scrolling content, so the actual size of the view (layout_height and layout_width) controls the size of that window. The content just scrolls up and down behind it. If that window is the same size as the content, wrap_content, then the whole thing is visible (but possibly off the screen) and there's nothing to scroll! So for scrolling stuff, you always want to limit the size of the view.
I need to implement the below layout for my RecyclerView Items (The picture represents two rows):
This is my XML file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:background="#color/theme_primary_color">
<View
android:layout_width="60dp"
android:layout_height="60dp"
android:background="#drawable/half_circle"
android:translationZ="3dp"
app:layout_constraintBottom_toTopOf="#id/cvTop"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="#id/cvTop" />
<androidx.cardview.widget.CardView
android:id="#+id/cvTop"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
app:cardCornerRadius="#dimen/card_view_corner"
app:layout_constraintTop_toTopOf="parent">
</androidx.cardview.widget.CardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fabCall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_call"
app:backgroundTint="#color/fab_green"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="#+id/cvTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/cvTop" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried to set negative margins to root ConstraintLayout but the second item went to top of the first one, but I need the first item to be on top of the second one.
So, this is not exactly what you want but at least it has the same layout as you want with a simpler approach.
So, the main challenges are:
Taking a curve cutout on the CardView probably need to be built programmatically with canvas for a better result. But for simplicity, this is replaced by a BottomAppBar wrapped in a CoordinatorLayout in order to have the curve effect with the top circle/gap.
Replacing the top View with Fab in order to have an Inset FAB by setting the layout_anchor to the BottomAppBar. Check material design for this.
And having the cutout behavior requires to make the FAB like it doesn't exist by setting a transparent backgroundTint & removing the outlineProvider
Making the top cutout (gap) of a particular row get overlapped to the top row like if it is a part of it. This works with the negative margin on the root view.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:layout_marginBottom="-20dp"
android:background="#android:color/transparent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/cvTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="60dp"
android:layout_height="60dp"
android:outlineProvider="none"
app:backgroundTint="#android:color/transparent"
app:layout_anchor="#id/navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_gravity="bottom"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
app:backgroundTint="#android:color/holo_blue_dark">
</com.google.android.material.bottomappbar.BottomAppBar>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fabCall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="#color/fab_green"
app:layout_constraintBottom_toBottomOf="#+id/cvTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/cvTop" />
<TextView
android:id="#+id/tv_item_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Item No. 0" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note: you can remove the TextView, I just added it to check the item doesn't change its position like you had in your case.
Preview on the RecyclerView
UPDATE:
A new challenge:
Only the top half of the FAB will intercept touch events, as any particular row will be laid on top of its direct top row; and hence the FAB of the top row won't intercept events in the intersection area.
Well, this can be manipulated well using canvas & custom View.
But also this can be solved in the current approach by laying out the rows of the RecyclerViews from bottom to top.
One way to setReverseLayout(true) and then either:
=> Reverse the RecyclerView list positions before submitting to the adapter
=> Or use mList.size - 1 - position instead of position within the adapter. Assuming the list of items is mList
I assume that the general RecyclerView layout looks and operates OK except that each item view overlays part of the item view above such that the bottom part of the top view's FAB cannot be seen.
I believe that what you are seeing is a result of the top-down default drawing order of RecyclerView. To change the drawing order to bottom-up so the bottom-most view overlays the one above, take a look at RecyclerView.ChildDrawingOrderCallback to see if you can get a reverse drawing order that will work.
Try the following:
mRecycler.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
#Override
public int onGetChildDrawingOrder(int childCount, int i) {
return childCount - i - 1;
}
});
I am not sure how you are offsetting your items, but consider using an RecyclerView.ItemDecoration. Something like this:
class OverlapItemDecoration(context: Context, overlapDp: Int) : RecyclerView.ItemDecoration() {
private val overlapPx: Int
init {
overlapPx = (context.resources.displayMetrics.density * overlapDp.toFloat()).toInt()
}
override fun getItemOffsets(
outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
) {
if (parent.getChildAdapterPosition(view) == 0) return
outRect.set(0, overlapPx, 0, 0)
}
}
Both of these techniques, together, will create a RecyclerView with overlapping item views laid out bottom to top so the FAB is 100% clickable.
Here is an example of the application of these techniques. I also replaced the CardView with a MaterialCardView with a ShapeableImageView as a background.
I'm migrating some projects to AndroidX with Kotlin. I'm having some issues with ConstraintLayout, I already know that, according to the documentation:
Negative margins will not be supported in ConstraintLayout.
[...]
But I have the following situation:
I need to move up in 5dp the LinearLayout, however I need the height to continue to match the lower limit of the screen. That is, I move up 5dp and increment the height by 5dp.
As it's in the image, I've already tried translateY, but it just moves the entire view (not what I need). Also I can not create a view inside the id#top with height of 5dp and align with constraintTop_toTopOf, since they are part of different groups.
Is there any solution for this case?
It's tricky to get views to overlap in ConstraintLayout, but you can do it by adding an invisible view and constraining the overlapping view to the invisible view.
In this case the invisible view's bottom could be constrained to the bottom of the green LinearLayout, with a bottom margin of 5dp. The red LinearLayout can then have its top constrained to the bottom of the invisible view. This should give you 5dp of overlap.
Try copy-pasting the following into your constraint layout
<LinearLayout
android:id="#+id/green"
android:layout_width="0dp"
android:layout_height="100dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="#android:color/holo_green_light" />
<View
android:id="#+id/dummyView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="5dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="#id/green"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:id="#+id/red"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="#id/dummyView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="#android:color/holo_red_light" />
Note that a dimension of "0dp" means "match constraints" when set on a child view of a ConstraintLayout. This is not obvious, but is in fact documented here https://developer.android.com/reference/android/support/constraint/ConstraintLayout
The whole purpose of ConstraintLayout is to have a flat view hierarchy. Therefore, having LinearLayouts nested in defeats the purpose.
I suggest you get rid of the nested LinearLayouts and do everything using constraints.
I have a RecyclerView inside of a ConstraintLayout whose top starts at the bottom of a textView, and whose bottom stretches down to the parent. I want the items in this RecyclerView to default to the bottom of the RecyclerView. I tried adding gravity="bottom" and also tried the same thing with foregroundGravity and layout_gravity. I also tried adding layout_gravity="bottom" to my recyclerView item's layout. Still, all the items in the recyclerView continues to default to the top. Is there anything i can do to make sure the items are at the bottom? Here is the xml:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/my_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="needed to unlock next coupon"
android:textColor="#color/gray"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/irrelevant_tv"
android:layout_marginTop="#dimen/spacing_tiny"
android:gravity="center"
/>
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="#dimen/spacing_xl"
android:paddingTop="#dimen/spacing_large"
android:overScrollMode="always"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="#id/my_tv"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
I don't want to do wrap_content on recyclerView either, because otherwise it is not scrollable (I always have 3 items in this recyclerView, and based on the user's display settings, the recyclerView can get pushed off the screen, which requires me to be able to scroll)
You can use the method LinearLayoutManager#setStackFromEnd(boolean).
Then your items will be rendered starting from bottom of view.
See the docs.
You can also add this to XML layout:
app:stackFromEnd="true"
I'm trying to use the support library's BottomSheetDialogFragment to replicate the standard sheet that shows when you tap a Share button (see below). How would I achieve a similar layout, where there's a title at the top, independently scrollable content in the center, but an bottom anchored view with buttons that always stay on top.
You need to build a custom layout for the bottomSheet, for example share_bottom.xml. Within that layout you could
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_hideable="true"
app:behavior_peekHeight="200dp">
<TextView
android:layout_width="match_parent"
android:text="header"
android:layout_height="wrap_content"/>
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"/>
<LinearLayout
android:layout_weight="0"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
And then include it at he bottom of your fragment's layout:
After that you can control the visibility of this sheet
//retrieve the bottomsheet
bottomSheet = (LinearLayout) findViewById(R.id.bottomSheet);
//get the behaviour controller
bsb = BottomSheetBehavior.from(bottomSheet);
//hide the sheet
bsb.setState(BottomSheetBehavior.STATE_HIDDEN);
//showthe sheet
bsb.setState(BottomSheetBehavior.STATE_EXPANDED);
I was also having the same problem. I solved using these steps.
Make FrameLayout as your root layout.
Include the view to be anchored as second child and first child should contain all the content of activity.
Animate the second child to be visible when bottom sheet is expanded and make it invisible when bottom sheet is collapsed.
The independently scrolling content inside bottom sheet can be acheived by using a scroll view or nested scroll view inside bottom sheet.
To dim background refer this link
This is just a workaround. Basically what I did is replicate the persistent bottom sheet to behave like modal sheet.