Android MaterialCardView's drop shadow/elevation disappears when rotating it 180 degrees - android

Summarize the problem:
I am working on a paint app, and in this app, the user has the option to rotate the canvas, this feature can be useful in some scenarios.
The problem I am having, is that when I rotate the CardView, which hosts the canvas/bitmap, 180 degrees -- the drop shadow gets lost. I do not get this problem when I rotate the CardView 90 degrees, only 180 degrees.
Describe what you've tried
I've tried many things, such as giving the CardView more space so that the drop shadow will show, or manually applying drop shadow from the code after rotating it 180 degrees, but it hasn't worked.
Show some code
Here is the code responsible for rotating the CardView (it's really simple):
fun CanvasActivity.rotate(rotationValue: RotationValue, animate: Boolean = false) {
rotate(rotationValue.degrees, rotationValue.clockwise, animate)
}
fun CanvasActivity.rotate(degrees: Int, clockwise: Boolean = true, animate: Boolean = false) {
val rotationAmount = if (clockwise) {
(binding.activityCanvasCardView.rotation + degrees)
} else {
(binding.activityCanvasCardView.rotation - degrees)
}
if (animate) {
binding.activityCanvasCardView
.animate()
.rotation(rotationAmount)
} else {
binding.activityCanvasCardView.rotation = rotationAmount
}
}
activity_canvas:
<?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:background="#color/fragment_background_color_daynight"
tools:context=".activities.canvas.CanvasActivity">
<!-- This view is here to ensure that when the user zooms in, there is no overlap -->
<View
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_topView"
android:layout_width="0dp"
android:layout_height="90dp"
android:background="#color/fragment_background_color_daynight"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- The ColorSwitcherView is a view I created which helps
simplify the code for controlling the user's primary/secondary color -->
<com.therealbluepandabear.pixapencil.customviews.colorswitcherview.ColorSwitcherView
android:id="#+id/activityCanvas_colorSwitcherView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:elevation="20dp"
android:outlineProvider="none"
app:isPrimarySelected="true"
app:layout_constraintEnd_toEndOf="#+id/activityCanvas_topView"
app:layout_constraintTop_toTopOf="#+id/activityCanvas_colorPickerRecyclerView" />
<!-- The user's color palette data will be displayed in this RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_colorPickerRecyclerView"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_topView"
app:layout_constraintEnd_toStartOf="#+id/activityCanvas_colorSwitcherView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/activityCanvas_primaryFragmentHost"
tools:listitem="#layout/color_picker_layout" />
<!-- This FrameLayout is crucial when it comes to the calculation of the TransparentBackgroundView and PixelGridView -->
<FrameLayout
android:id="#+id/activityCanvas_distanceContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_tabLayout"
app:layout_constraintEnd_toEndOf="#+id/activityCanvas_primaryFragmentHost"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView" />
<!-- This gives both views (the PixelGridView and TransparentBackgroundView) a nice drop shadow -->
<com.google.android.material.card.MaterialCardView
android:id="#+id/activityCanvas_cardView"
style="#style/activityCanvas_canvasFragmentHostCardViewParent_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_tabLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView">
<!-- At runtime, the width and height of the TransparentBackgroundView and PixelGridView will be calculated -->
<com.therealbluepandabear.pixapencil.customviews.transparentbackgroundview.TransparentBackgroundView
android:id="#+id/activityCanvas_transparentBackgroundView"
android:layout_width="0dp"
android:layout_height="0dp" />
<com.therealbluepandabear.pixapencil.customviews.pixelgridview.PixelGridView
android:id="#+id/activityCanvas_pixelGridView"
android:layout_width="0dp"
android:layout_height="0dp" />
</com.google.android.material.card.MaterialCardView>
<!-- The primary tab layout -->
<com.google.android.material.tabs.TabLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_tabLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:tabStripEnabled="false"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_viewPager2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_tools_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_filters_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_color_palettes_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_brushes_str" />
</com.google.android.material.tabs.TabLayout>
<!-- This view allows move functionality -->
<View
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_moveView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_distanceContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView" />
<!-- The tools, palettes, brushes, and filters fragment will be displayed inside this ViewPager -->
<androidx.viewpager2.widget.ViewPager2
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_viewPager2"
android:layout_width="0dp"
android:layout_height="110dp"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_primaryFragmentHost"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- This CoordinatorLayout is responsible for ensuring that the app's snackbars can be swiped -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_coordinatorLayout"
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" />
<!-- All of the full page fragments will be displayed in this fragment host -->
<FrameLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_primaryFragmentHost"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
0 degrees:
90 degrees:
180 degrees:
As you can see, no drop shadow when it's rotated 180 degrees (when the origin is 0 degrees).
This problem is not major but it makes the UI of my app inconsistent.
Note: for some reason, I only get the issue on phones but not tablet devices.

It's a curious thing, actually. I've tried to rotate a simple shadowed View and it seems that it cuts off the shadow when it has 180/-180 rotation angle without any logical reason. I also tried to play with outline providers, buut no. So it's not about CardView, it rather comes from the implementation of drawing a shadow in the View class.
I've searched a bit, and it was already reported directly to Google and fixed, but probably some compat libraries still have this issue that's why you're facing it.
https://issuetracker.google.com/issues/173730323
https://issuetracker.google.com/issues/137454913
I can suggest you creating a crutch (yeap, I know, that it's not a solution) with leaving a comment. If your rotation is about -180/180, you can "normalize" it with "-179.9/179.9". In terms of the user experience it won't be visible, in terms of beautiful code - meh, but I don't see any other ways here.

You're setting android:outlineProvider="none" on most items ...
while the drop-shadow being used might have the wrong offset coordinates - you'd need to substract instead of adding the offset. There's too little code provided to answer this more accurately - but with high probability, the shadow is just hidden underneath the canvas, due to the stated cause. I'd suggest to provide your own implementation of ViewOutlineProvider and then pass this instead of none.

You need to set total (left, right, bottom and top) of 16dp on each side android:layout_margin="16dp" and set app:cardElevation="4dp", it will work.

Related

how to set items when soft keyboard is shown with using guideLine in constraint layout?

I am using ConstraintLayout in my application.
When soft keyboard is shown, I want to only button move above that And other items do not move up. I did this with using a scrollView.
Now I used guideLine to support all screen sizes, but after soft keyboard is shown, all items move to up again.
How should I use guideLine for this?
Manifest :
<activity
android:name=".RegisterActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
Xml :
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ScrollView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="#id/button_register_sendVerification"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_topParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_bottomParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.96" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_leftParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.08" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_rightParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.92" />
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_topMobileNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.4"/>
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_register_bottomMobileNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.49"/>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/textInputLayout_register_mobileNumber"
style="#style/TextInputLayout.All"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toRightOf="#id/guideline_register_leftParent"
app:layout_constraintRight_toLeftOf="#id/guideline_register_rightParent"
app:layout_constraintTop_toTopOf="#+id/guideline_register_topMobileNumber"
app:layout_constraintBottom_toBottomOf="#id/guideline_register_bottomMobileNumber">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/textInputEditText_register_mobileNumber"
style="#style/TextInputEditText.All"
android:hint="#string/text_all_enterPhoneNumber"
android:inputType="phone"
android:maxLength="11"
android:textAlignment="center" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<Button
android:id="#+id/button_register_sendVerification"
style="#style/Button.all"
android:layout_width="0dp"
android:layout_height="48dp"
android:text="#string/text_button_registerActivity_sendVerification"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Unfortunately, all windowSoftInputModes will either shift the text view or hide the button.
If you want to use a windowSoftInputMode of adjustResize, you will have to accommodate a ConstraintLayout that will change it's height when the soft keyboard is shown and hidden. Since a percentage Guideline specifies its position as a percentage of the ConstraintLayout's height (not the screen height), the Guideline's position will shift as the layout's height changes.
In your case, the easiest way around this problem is to specify Guideline positions using an offset from the top of the ConstraintLayout. Since this offset will remain a constant distance from the top of the ConstraintLayout even as the ConstraintLayout's height changes, it will behave as you want.
You can also leave the percent Guidelines in and convert them programmatically to fixed Guidelines. That way you can get the 40% offset regardless of the device.
The following code will replace a percentage Guideline with a fixed offset Guideline.
private fun replaceGuideline(layout: ConstraintLayout, oldGuideline: Guideline) {
require(layout.height > 0) { "ConstraintLayout must have gone through a layout pass." }
Guideline(layout.context).apply {
val lp = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
lp.orientation = ConstraintLayout.LayoutParams.HORIZONTAL
layoutParams = lp
id = oldGuideline.id
setGuidelineBegin(oldGuideline.top)
layout.removeView(oldGuideline)
layout.addView(this)
}
}
You could also specify a windowSoftInputMode of adjustNothing (which is strangely missing from the docs). You can then detect when the soft keyboard is opened and closed to adjust the position of the button, although detecting soft keyboard status is oddly tricky.

How to make sure recycler view adapter item takes full height of the screen

I have a recyclerview which contains many items.
But the first item is like an intro screen, it has a button on which user can tap to view more, or user can scroll to view more data.
Something like following.
I want this button to be at the bottom of screen, in small devices it is not visible unless user scrolls the view. So basically layout is taking more height, so user has to scroll to view the complete layout.
I am using constraint layout, and connect it to the bottom of the view.
There are only 2 items in the view, 1 is imageView, which does not have any image, I have only added background color to it, and other is a TextView.
<androidx.constraintlayout.widget.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">
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#color/Black_Color"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center"
android:layout_marginBottom="50dp"
android:textSize="17dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</
in small devices it is not visible unless user scrolls the view - sounds like your image is taking a lot of screen height in small devices so the user must scroll trow the screen
If you want to "disable" the scrolling behavior of your item and put everything inside a screen (without views gong out of the screen and thus making the user scroll) you can change your layout so your item will only occupy the given screen height and not more, and by doing so you will not have this problem on small devices.
something like this:
<?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"
tools:context=".MainActivity">
<ImageView
android:id="#+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.9"
android:scaleType="fitXY"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="#tools:sample/backgrounds/scenic" />
<TextView
android:id="#+id/textView"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="TextView"
android:textSize="17sp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
The trick was to simply give the image height in percentage like this
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.9"
And now your image will take 90% height on every screen size.

ConstraintLayout: center view on full screen but limit width to not overlap with side views

I have a toolbar like component implementation, that I'm having trouble with the layout in all situations. It has a left icon, a title and a right menu/button. I need the title to be centered on the full screen (or at least the full width of the layout) but also to not overlap with the other components. So the width of the title would have to be constrained by the left icon and the right button.
I have two intermediary solutions but can't seem to find a way to combine them.
One is to center the title on the screen. The problem here is that the title overlaps with the right button (and would overlap with the left icon too, if large enough ...). Here is the layout XML:
<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">
<ImageView
android:id="#+id/bottomSheetHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/bottomSheetHeaderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="My also very very very very very long title" />
<TextView
android:id="#+id/right_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Really long string longer"/>
</androidx.constraintlayout.widget.ConstraintLayout>
The other is to center the title between the left icon and the right button. Now there is no overlap, but the title is not centered correctly. Since the two side elements have very different sizes, the title is only centered between them, which is no good. Here is the same layout with the constraints for this case:
<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">
<ImageView
android:id="#+id/bottomSheetHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/bottomSheetHeaderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textAlignment="center"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/right_action"
app:layout_constraintStart_toEndOf="#+id/bottomSheetHeaderIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="My also very very very very very long title" />
<TextView
android:id="#+id/right_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Really long string longer"/>
</androidx.constraintlayout.widget.ConstraintLayout>
I'm trying to get away with just a XML layout solution, without having to do programmatically detect overlaps and change the layout.
Solutions with other layouts would also work, if they exist.
The problem is the same as the questions here and here, but I'm hoping that by giving more detail, this one gets an accepted solution.
Here is a programatic answer to this question, that is what I'm using for now, but I'd really like a layout solution ...
I use the first layout, centered on the screen, and, as this is part of a custom view for me, add the following code to the constructor (title is a reference to bottomSheetHeaderTitle and rightAction is right_action, the left icon is assumed to always be smaller than the right action text).
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (title.width > 0) {
if (title.right > rightAction.x) {
title.maxWidth = (2.0 * (width / 2.0 - (width - rightAction.x))).toInt()
}
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
This code detects if the title overlaps the right action, and, if it does, adds a max width to it. The max width is the double of the distance between the center of the screen and the beginning of the right action.

Inexplicable white/blank space in Android app, not showing in layout preview

The preview looks fine, but the app runs differently. In the layout inspector, there is simply nothing where this weird white space is. It seems to be a problem with ConstraintLayout + ViewPager. I had the same issue in three places, all with same ViewPager, 0dp vertical size setup. I have solved by converting ConstraintLayouts into LinearLayouts. But the question is why do I need to regress to a LinearLayout? I originally wrote the UI with ConstraintLayout, and ViewPager, it was fine. Somewhere along the way, without touching the code, things turned weird and this blank space showed up. As if something in Android UI rendering changed under the hood. Anyone have any idea what it is, or how to solve while keeping a ConstraintLayout?
Here is the layout inspector image of the app running on a device. I have clearly marked the problematic white space with my mouse calligraphy skills.
Here is the layout preview for the following xml code, note UI elements fill the screen, no white space after the bottom toolbar in this view, unlike live app. Don't mind the colour difference, the app changes colours at run time.
fragment_layout.xml (Between green and marked blank space in live app image, the rest of UI is from activity, but I know that's not the problem, so won't add the xml for that)
<?xml version="1.0" encoding="utf-8"?>
<layout
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.order.OrderBuildFragment"
>
<com.google.android.material.tabs.TabLayout
android:id="#+id/order_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed"
app:layout_constraintBottom_toTopOf="#+id/order_viewpager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.viewpager.widget.ViewPager
android:id="#+id/order_viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#android:color/white"
app:layout_constraintTop_toBottomOf="#id/order_tabs"
app:layout_constraintBottom_toTopOf="#id/build_order_bottom_toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
</androidx.viewpager.widget.ViewPager>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/build_order_bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/order_viewpager"
app:layout_constraintBottom_toBottomOf="parent"
android:background="?attr/colorPrimary"
>
<TextView
android:id="#+id/order_service_point_spinner_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#string/service_point"
android:textColor="#color/white"
app:layout_constraintEnd_toStartOf="#id/order_toolbar_service_point_spinner"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/build_order_guideline_service_point_and_action_buttons"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="#+id/order_toolbar_service_point_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/build_order_guideline_service_point_and_action_buttons"
app:layout_constraintStart_toEndOf="#id/order_service_point_spinner_label"
android:layout_marginEnd="8dp"/>
<androidx.constraintlayout.widget.Guideline
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/build_order_guideline_service_point_and_action_buttons"
app:layout_constraintGuide_percent="0.5"
/>
<TextView
android:id="#+id/order_textView_price"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="#{#string/currency_symbol + viewModel.price}"
android:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/order_toolbar_button_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/build_order_guideline_service_point_and_action_buttons"
tools:text="$999.99"
/>
<Button
android:id="#+id/order_toolbar_button_more"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="moreButtonAction"
android:text="#string/more"
android:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/order_toolbar_button_send"
app:layout_constraintStart_toEndOf="#id/order_textView_price"
app:layout_constraintTop_toBottomOf="#id/build_order_guideline_service_point_and_action_buttons"/>
<Button
android:id="#+id/order_toolbar_button_send"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="sendButtonAction"
android:text="#{viewModel.sendButtonText}"
android:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/order_toolbar_button_more"
app:layout_constraintTop_toBottomOf="#+id/build_order_guideline_service_point_and_action_buttons"
tools:text="#string/send"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.ContentLoadingProgressBar
android:id="#+id/build_order_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="#{safeUnbox(viewModel.loading)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My Linear layout solution simply included replacing ConstraintLayout with a LinearLayout, removing all constraints from child elements and adding orientation: "vertical" (of course) plus putting layout_weight: 1 in the ViewPager element. If that helps anyone.
My guess is that you are using androidx.constraintlayout:constraintlayout:2.0.0-beta2.
This specific version is known to cause issues like this.
Update to the latest version ('androidx.constraintlayout:constraintlayout:2.0.0-beta6' at the time of writing).

ConstraintLayout relative to ImageView dimensions

I'm currently developing a small Android application and use the new ConstraintLayout.
I have an ImageView which contains a vector graphic image which is supposed to take the maximum space available with respect to its aspect ratio. I got this to work with the following code:
<ImageView
android:id="#+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="#dimen/margin"
android:src="#drawable/image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
Now, I want to place multiple custom views (buttons) at exact positions. Using Constraint Guidelines, I came up with the following:
<android.support.constraint.Guideline
android:id="#+id/guideline_x"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.20"
tools:layout_editor_absoluteX="..."
tools:layout_editor_absoluteY="..."
/>
<android.support.constraint.Guideline
android:id="#+id/guideline_y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.20"
tools:layout_editor_absoluteX="..."
tools:layout_editor_absoluteY="..."
/>
<com.example.android.myCustomView
android:id="#+id/myCustomView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doSomething"
app:layout_constraintLeft_toLeftOf="#+id/guideline_x"
app:layout_constraintTop_toTopOf="#+id/guideline_y"
/>
This works great for the specific device that I've been testing with initially. But: As soon as the device dimensions vary, the custom views are placed at wrong positions.
I am looking for a way to place a custom view n% relative to the x coordinate of the image view. I've tried adding app:layout_constraintLeft_toLeftOf="#+id/imageView" to the guidelines but this changes nothing. Do you have any ideas how I can resolve this issue? Thanks a lot!
Edit:
Here are 2 images which illustrate the issue.
Samsung Galaxy S8:
Google Pixel XL:
The little red square should always be at the exact same position relative to the Android icon.
Here is a easier way to place a widget precisely on an ImageView or any other view that will retain it placement regardless of any change in size of the ImageView as long as the aspect ratio remains constant. In the first proposed solution of my other answer, I suggested using bias to place the widget within the ImageView. Unfortunately, a change in the relative size of the widget to to the size of the ImageView causes the widget to shift on the image which is not desireable.
To get around this, I suggest placing a 1x1 pixel Space view on the ImageView using bias. Because the Space view is just 1x1 pixel in size, it can be placed anywhere within the ImageView by using bias. It's relative position will remain constant as the ImageView changes size due to rotation on a single device or because the app is being hosted on devices with different screen sizes. It is just important that the aspect ratio of the image remain constant regardless of the size of the screen.
Once the 1x1 pixel view is placed, the widget can be attached through constraints. In the example below, I have chosen to center the widgets for the eyes on the placement pixels by constraining all sides of each eye widget to the corresponding sides of its placement pixel.
Here are some images showing the effect. Screen with varying sizes that I have tested show the same result.
Nexus 6 Portrait
Nexus 6 Landscape
Because the ImageViews for the eyes are not scaled, they appear smaller on the Nexus 10 screen. The eyes are placed in the same location as on the Nexus 6. Scaling the eyes is a different matter.
Nexus 10 Landscape
Here is the layout showing the constraints:
position_view.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/androidGreen"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="#drawable/android_image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<Space
android:id="#+id/pinpoint1"
android:layout_width="1px"
android:layout_height="1px"
app:layout_constraintBottom_toBottomOf="#id/androidGreen"
app:layout_constraintEnd_toEndOf="#id/androidGreen"
app:layout_constraintHorizontal_bias="0.373"
app:layout_constraintStart_toStartOf="#id/androidGreen"
app:layout_constraintTop_toTopOf="#id/androidGreen"
app:layout_constraintVertical_bias="0.19" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/circle"
app:layout_constraintBottom_toBottomOf="#id/pinpoint1"
app:layout_constraintEnd_toEndOf="#id/pinpoint1"
app:layout_constraintStart_toStartOf="#id/pinpoint1"
app:layout_constraintTop_toTopOf="#id/pinpoint1"
app:srcCompat="#drawable/circle"
tools:ignore="ContentDescription" />
<Space
android:id="#+id/pinpoint2"
android:layout_width="1px"
android:layout_height="1px"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/androidGreen"
app:layout_constraintEnd_toEndOf="#id/androidGreen"
app:layout_constraintHorizontal_bias="0.625"
app:layout_constraintStart_toStartOf="#id/androidGreen"
app:layout_constraintTop_toTopOf="#id/androidGreen"
app:layout_constraintVertical_bias="0.19" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/circle"
app:layout_constraintBottom_toBottomOf="#id/pinpoint2"
app:layout_constraintEnd_toEndOf="#id/pinpoint2"
app:layout_constraintStart_toStartOf="#id/pinpoint2"
app:layout_constraintTop_toTopOf="#id/pinpoint2"
tools:ignore="ContentDescription" />
</android.support.constraint.ConstraintLayout>
You can also achieve the same goal by wrapping the ImageView in a ConstraintLayout and setting the width and height of the ImageView to match_parent. You can then use percent guidelines to position a widget on the ImageView. This method introduces another level in the layout hierarchy but may be easier to understand.
Here is XML that will demonstrate this method.
XML using guidelines within nested ConstraintLayout
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/androidGreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#drawable/android_image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.19" />
<android.support.constraint.Guideline
android:id="#+id/guidelineVerticalLeft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.375" />
<android.support.constraint.Guideline
android:id="#+id/guidelineVerticalRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.625" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/circle"
app:layout_constraintBottom_toBottomOf="#id/guidelineHorizontal"
app:layout_constraintEnd_toEndOf="#id/guidelineVerticalLeft"
app:layout_constraintStart_toStartOf="#id/guidelineVerticalLeft"
app:layout_constraintTop_toTopOf="#id/guidelineHorizontal"
app:srcCompat="#drawable/circle"
tools:ignore="ContentDescription" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/circle"
app:layout_constraintBottom_toBottomOf="#id/guidelineHorizontal"
app:layout_constraintEnd_toEndOf="#id/guidelineVerticalRight"
app:layout_constraintStart_toStartOf="#id/guidelineVerticalRight"
app:layout_constraintTop_toTopOf="#id/guidelineHorizontal"
tools:ignore="ContentDescription" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
If you want to position a view within another view, take a look at Centering positioning and bias in ConstraintLayout.
Bias
The default when encountering such opposite constraints is to center the widget; but you can tweak the positioning to favor one side over another using the bias attributes:
Here is a TextView positioned 20% from the left side of the image and 70% from the top. The positioning will remain constant with different screen sizes, but you will have to make sure the aspect ratio stays constant; otherwise, the TextView will drift.
Here is the XML for the image above:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
android:src="#drawable/ic_android_green_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:text="TextView here"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="#id/imageView"
app:layout_constraintEnd_toEndOf="#id/imageView"
app:layout_constraintHorizontal_bias="0.2"
app:layout_constraintStart_toStartOf="#id/imageView"
app:layout_constraintTop_toTopOf="#id/imageView"
app:layout_constraintVertical_bias="0.7" />
</android.support.constraint.ConstraintLayout>
Another way to handle positioning within a view that provides more precise targeting is to define a horizontal and vertical chain of Space views that use weights to divide up the container view. Here is the same layout as above but with a vertical boundary between two Space views at 20% of the width of the ImageView. An additional two Space views have a horizontal boundary at 70% of the container view. See Chains in the documentation for ConstraintLayout.
Here is the XML:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
android:src="#drawable/ic_android_green_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Space
android:id="#+id/space1"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_blue_light"
app:layout_constraintBottom_toBottomOf="#id/imageView"
app:layout_constraintEnd_toStartOf="#id/space2"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="20"
app:layout_constraintStart_toStartOf="#id/imageView"
app:layout_constraintTop_toTopOf="#id/imageView" />
<Space
android:id="#+id/space2"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/imageView"
app:layout_constraintEnd_toEndOf="#id/imageView"
app:layout_constraintHorizontal_weight="80"
app:layout_constraintStart_toEndOf="#id/space1"
app:layout_constraintTop_toTopOf="#id/imageView" />
<Space
android:id="#+id/space3"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_blue_light"
app:layout_constraintBottom_toTopOf="#id/space4"
app:layout_constraintEnd_toEndOf="#id/imageView"
app:layout_constraintStart_toStartOf="#id/imageView"
app:layout_constraintTop_toTopOf="#id/imageView"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_weight="70" />
<Space
android:id="#+id/space4"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/imageView"
app:layout_constraintEnd_toEndOf="#id/imageView"
app:layout_constraintStart_toStartOf="#id/imageView"
app:layout_constraintTop_toBottomOf="#id/space3"
app:layout_constraintVertical_weight="30" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView here"
android:textSize="24sp"
app:layout_constraintStart_toEndOf="#id/space1"
app:layout_constraintTop_toBottomOf="#id/space3" />
</android.support.constraint.ConstraintLayout>

Categories

Resources