Negative Margins For RecyclerView Item Decoration - android

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.

Related

RecyclerView obscurs items below it in a BottomSheetFragment

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.

Align two views in opposites sides (one at the top and one at the bottom) and be able to push the bottom view when the top view grows?

I basically have an EditText aligned at the top of the view and there's a RecyclerView on the bottom of the view that can also grow (with the newest item on the bottom)
That's easily doable with a constraint layout but my problem is that when the EditText grows it should start pushing down the list. But the list was initially aligned at the bottom of the parent. (and everything should be scrollable)
I hope this image makes things more clear
The trick here is to avoid forming a vertical chain (so that the EditText at the top always stays in place) and also to leverage the app:layout_constrainedHeight attribute on the RecyclerView in order to make it shrink when the EditText grows.
<?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="match_parent">
<EditText
android:id="#+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/text"
app:layout_constraintVertical_bias="1"/>
</androidx.constraintlayout.widget.ConstraintLayout>
The combination of android:layout_height="wrap_content" and app:layout_constrainedHeight="true", along with both top and bottom constraints, means that the RecyclerView will always be only as tall as its items or the available space below the EditText, whichever is smaller.
The app:layout_constraintVertical_bias="1" attribute ensures that, when the list doesn't fill the screen, it sits at the bottom.
I would add to the previous answer and if you want to be able to scroll your layout you can use sth like this:
<androidx.core.widget.NestedScrollView 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:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="#+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#id/edit_text"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
This will make everything align as you want to and you are able to scroll down to see your full recyclerView. NestedScrollView will make sure that everything runs smoothly.

Constraintlayout and View Rotation not resizing Views

Im using a Constraintlayout with bias to fill the screen with multiple views. When I rotate the views they dont get resized to to fill the screen. My layout is more complicated but I created an example to show my problem.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/one"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_orange_dark"
app:layout_constraintBottom_toTopOf="#+id/two"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:rotation="90"
android:background="#android:color/holo_green_light"/>
</FrameLayout>
<FrameLayout
android:id="#+id/two"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/one">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_green_light" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
It doesn't really matter if I rotate the outer or the inner FrameLayout. I don't think I had this problem with LinearLayouts maybe the constraints are getting messed up by the rotation?
EDIT: Hmm looks like the same is happening when using a Linearlayout with weight as parent, so Im probably just doing something wrong here.
The view properties of rotation, translationX and translationY all take effect post-layout. I think that this is true for all view groups. In other words, the views are laid out as if rotation was not specified. Then, after layout, the rotation is applied. This is what you are seeing.
I don't have a reference for this but this problem comes up a lot on Stack Overflow.
Here is an example of this using translationY. Look at the "clarification" section at the top. See how the bottom view does not move even though it is constrained top-to-bottom with the view above? That is because it is positioned to the top view before the top view moves. translationY happens post-layout as does rotation.
This problem can be solved with (probably) a little coding. The exact solution depends on what you are trying to do.

Align items in a RecyclerView at the bottom

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"

Position view outside of ConstraintLayout

I want to position views outside of a ConstraintLayout to animate them with a sliding animation. I've tried setting contraints like constraintBottom_toTopOf="parent" but the View stays inside the container.
Note that I want to achieve this with constraints to use built-in animations, not with in-code animations.
Any idea how I could do this ?
I'm using compile 'com.android.support.constraint:constraint-layout:1.1.0-beta1'
with Android Studio 3.0 Beta 7
This is a simple xml file that should place the view outside of the container :
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#color/colorAccent">
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#color/colorPrimary"
app:layout_constraintBottom_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
But this is the result
This appears to be an issue with ConstraintLayout 1.1.0-beta1; It works as expected in ConstraintLayout 1.1.0-beta3.
Update to ConstraintLayout 1.1.0-beta3. I will also note that you need to constrain your view horizontally by doing something like the following.
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#color/colorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="parent" />
On a side note, negative margins are not accepted in ConstraintLayout. See this Stack Overflow question regarding negative margins and ConstraintLayout.
In every view you can use negative margin, which will put the view outside of the parent view, and then set the clipping parameters.
android:clipChildren="false"
android:clipToPadding="false"
this will make the view not to clip.
I got another way to solve the problem:
1.Add a anchor(anchor_left) layout_constraintStart_toStartOf="parent".
2.Add YourView layout_constraintEnd_toStartOf="#+id/anchor_left"
That's it!
code:
<android.support.constraint.ConstraintLayout>
<View
android:id="#+id/anchor_left"
app:layout_constraintStart_toStartOf="parent"/>
<YourView
android:id="#+id/ll_left"
app:layout_constraintEnd_toStartOf="#+id/anchor_left"/>
</android.support.constraint.ConstraintLayout>
What I did is:
created a view of 0dp height inside the ConstraintLayout, e.g. "fakeView"
placed the new fakeView anchored at Top of the ConstraintLayout
when I need to hide a View, translate it outside the constraint..
change the constraint of the view you want to hide, in order to have BOTTOM connected to the Top of the FakeView.
I think you can use same technique to move object on the left of the fakeview or on the right.
One trick would be to set negative margin for the side you want, in the ConstraintLayout itself. This requires that other views that have constraint to that side be offset:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...
android:layout_marginBottom="-48dp">
<ImageButton
android:id="#+id/leftButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="72dp"
android:background="#drawable/shape_next_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageButton
android:id="#+id/rightButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:background="#drawable/shape_previous_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Categories

Resources