I've been trying for several days to make a list with the same effect that some Wear OS apps do, such as Google Play Store, where the list is shrinking the items that are disappearing and expanding the new items that are appearing:
I followed the Google Developers documentation, the problem is that when I apply the custom Layout Manager to it, the items don't stay centered for me, they look like this:
This is the code I used:
<?xml version="1.0" encoding="utf-8"?>
<androidx.wear.widget.BoxInsetLayout 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:padding="#dimen/box_inset_layout_padding"
tools:context=".ui.activity.HomeActivity"
tools:deviceIds="wear">
<androidx.wear.widget.WearableRecyclerView
android:id="#+id/wearable_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.wear.widget.BoxInsetLayout>
private fun setupAdapter() {
val homeOptionsAdapter = HomeOptionsAdapter(petList)
val customScrollingLayoutCallback = CustomScrollingLayoutCallback()
recyclerWearable.layoutManager = WearableLinearLayoutManager(
this,
customScrollingLayoutCallback
)
recyclerWearable.isEdgeItemsCenteringEnabled = true
recyclerWearable.adapter = homeOptionsAdapter
}
private const val MAX_ICON_PROGRESS = 2f
class CustomScrollingLayoutCallback : WearableLinearLayoutManager.LayoutCallback() {
private var progressToCenter: Float = 0f
override fun onLayoutFinished(child: View, parent: RecyclerView) {
child.apply {
// Figure out % progress from top to bottom
val centerOffset = height.toFloat() / 2.0f / parent.height.toFloat()
val yRelativeToCenterOffset = y / parent.height + centerOffset
// Normalize for center
progressToCenter = abs(0.5f - yRelativeToCenterOffset)
// Adjust to the maximum scale
progressToCenter = progressToCenter.coerceAtMost(MAX_ICON_PROGRESS)
scaleX = 1 - progressToCenter.pow(2f)
scaleY = 1 - progressToCenter.pow(2f)
}
}
}
I read that with the option setEdgeItemsCenteringEnabled the list was centered, but I don't see it centered, what can I do?
I have/had the same issue, this is the best work around I could get:
Vid demostration: video link
main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- OPTIONAL -->
<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="fill_parent"
android:layout_height="fill_parent"
android:background="#000000"
tools:context=".Main"
android:id="#+id/main">
<!-- REQUIRED-->
<androidx.wear.widget.WearableRecyclerView
android:id="#+id/recycler_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<!-- OPTIONAL -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/top_shadow"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#drawable/gradient_shadow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- OPTIONAL -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/bottom_shadow"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#drawable/gradient_shadow"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
gradient_shadow.xml //OPTIONAL
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:type="linear"
android:centerX="100%"
android:startColor="#DD000000"
android:endColor="#00000000"
android:angle="-90"/>
</shape>
list_item.xml
<?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="fill_parent"
android:layout_height="56sp"
android:layout_marginBottom="3sp"
android:background="#drawable/list_design">
<ImageView
android:id="#+id/imageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="18sp"
android:background="#FF0000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="#tools:sample/avatars" />
<TextView
android:id="#+id/list_main_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10sp"
android:text="List item"
android:textColor="#color/white"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/imageView"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
list_design.item
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#color/gray"></solid>
<corners android:radius="50dp"/>
</shape>
customScrollingLayoutCallback
// I have this "math" from another app that I was design in javascript for smartphone a long time ago... I dont remember why each part, so, sorry, cant explain hahaha
//I just added the if part and alpha
public void onLayoutFinished(View child, RecyclerView parent) {
int height_parent = parent.getHeight();
float half_height_parent = (float) parent.getHeight() / 2;
int child_max_width = parent.getWidth();
float child_new_width = ((1 - Math.abs((child.getY() - half_height_parent + 45) / height_parent)) * height_parent);
float equivalent_scale = child_new_width / child_max_width;
child.setAlpha(equivalent_scale);
if(child_new_width > (child_max_width * 0.63)) {
child.setScaleX(0.96f);
child.setScaleY(1);
} else {
child.setScaleX(equivalent_scale + 0.2f);
child.setScaleY(0.98f);
}
}
I am using MPAndroid chart library to show pie chart in my app
The legends / chart description labels are not wrapping below one another
I used pieChart.legend.isWordWrapEnabled=true but it doesn't work out
This is my 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="match_parent"
tools:context=".equity.EquityFragment">
<include
android:id="#+id/pageTitleLayout"
layout="#layout/page_title" />
<com.github.mikephil.charting.charts.PieChart
android:id="#+id/pieChart"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/pageTitleLayout" />
<include
android:id="#+id/loader"
layout="#layout/view_progress_loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
And this is code
private fun createChart(chartData:List<EquityPieChart>){
val pieEntry= chartData.map { PieEntry(it.equity,it.name) }
val rnd = Random()
val colors = mutableListOf<Int>()
for (i in chartData.indices){
colors.add(Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)))
}
val dataSet=PieDataSet(pieEntry,getString(R.string.equity_title))
dataSet.colors=colors
binding.pieChart.data = PieData(dataSet)
binding.pieChart.isDrawHoleEnabled=false
binding.pieChart.legend.isWordWrapEnabled=true
binding.pieChart.invalidate()
}
this is the UI i get in device
The text of legends are too big to be fit inside the graph. One way is to keep them outside the graph.
The following attribute can be added to achieve this job:
setDrawInside()
Please use this code:
Legend l = pieChart.getLegend();
l.setVerticalAlignment(Legend.LegendVerticalAlignment.BOTTOM);
l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.LEFT);
l.setOrientation(Legend.LegendOrientation.HORIZONTAL);
l.setDrawInside(false);
l.setXEntrySpace(4f);
l.setYEntrySpace(0f);
l.setWordWrapEnabled(true);
This will set the legend outside the graph.
I ended up doing like this with the help of flexboxlayout
XML
<?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=".equity.EquityFragment">
<include
android:id="#+id/pageTitleLayout"
layout="#layout/page_title" />
<com.github.mikephil.charting.charts.PieChart
android:id="#+id/pieChart"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/chartLabels"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/pageTitleLayout" />
<com.google.android.flexbox.FlexboxLayout
android:id="#+id/chartLabels"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include
android:id="#+id/loader"
layout="#layout/view_progress_loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
Legend layout
<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="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="25dp"
android:padding="2dp">
<View
android:id="#+id/legendBox"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="#color/primary_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="#+id/legendTitle"
app:layout_constraintTop_toTopOf="#+id/legendTitle"/>
<com.google.android.material.textview.MaterialTextView
android:id="#+id/legendTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="xxxxjdsfjsdfj"
android:layout_marginStart="3dp"
app:layout_constraintStart_toEndOf="#+id/legendBox"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Kotlin code
private fun createChart(chartData: List<EquityPieChart>) {
val pieEntry = chartData.map { PieEntry(it.equity) }
val rnd = Random()
val colors = mutableListOf<Int>()
for (i in chartData.indices) {
colors.add(Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)))
}
val dataSet = PieDataSet(pieEntry, getString(R.string.equity_title))
dataSet.colors = colors
dataSet.valueTextSize = 14f
dataSet.setDrawValues(false)
dataSet.valueTextColor = ContextCompat.getColor(requireContext(), android.R.color.white)
binding.pieChart.data = PieData(dataSet)
binding.pieChart.isDrawHoleEnabled = false
binding.pieChart.description = Description().apply { text="" }
binding.pieChart.invalidate()
binding.pieChart.animateY(1400, Easing.EaseInOutQuad);
binding.pieChart.legend.isEnabled = false
creatChartLabels(colors,chartData)
}
private fun creatChartLabels(colors:List<Int>,chartData: List<EquityPieChart>){
for (i in chartData.indices) {
val view=ItemLegendBinding.inflate(layoutInflater)
view.legendBox.setBackgroundColor(colors[i])
view.legendTitle.text=chartData[i].name
binding.chartLabels.addView(view.root)
}
}
output
I would like to add content to an app that starts at about 70% down vertically and can be scrolled upwards to cover the top 70% views.
I thought of using two children ConstraintLayout's inside a parent ConstraintLayout - the two children would be on top of each other. One would contain the views that would populate the first 70% of the screen while the other would contain a NestedScrollView which has an invisible <View> that takes up 70% of the height and then the additional content that can be scrolled up.
I'm facing a problem with marking the 70% spot - using a Guideline inside the NestedScrollView isn't working because the %s are fluid (it matches to 70% of the content inside the NestedScrollView instead of 70% of the viewable screen). Using a Guideline outside the NestedScrollView doesn't work because well... constraints have to be siblings to compile.
How can I accomplish this?
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red5F"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
// A bunch of content that should fill up the first 70% of the screen and be covered by the overlay if user scrolls
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
android:id="#+id/scrollView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/overlayInnerLayout">
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/verticalGuidelineOverlay"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7"/>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="#+id/spacerView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/verticalGuidelineOverlay"
app:layout_constraintLeft_toLeftOf="parent"/>
// More content here that the user could scroll upwards that would start at the 70% point and eventually cover the entire screen.
</ConstraintLayout>
</NestedScrollView>
</ConstraintLayout>
</ConstraintLayout>
Video w/example here: https://imgur.com/a/BTolYUu
Try out this method,
<?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:id="#+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:weightSum="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="horizontal">
<RelativeLayout
android:id="#+id/transparentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="0.3" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/bg">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20sp"
android:text="#string/lorem_ipsum"
tools:ignore="MissingConstraints"
android:textSize="18sp"/>
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
set 70% height programatically using layoutParams
val transparentView = findViewById<RelativeLayout>(R.id.transparentView)
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
val height = Math.min(metrics.widthPixels, metrics.heightPixels) //height
val params = transparentView.layoutParams
params.height = (height * 70) / 70
transparentView.layoutParams = params
you will get the required result : enter link description here
Remove guidelines and use a view like this as a spacer view. It's height constrained to be 1.15 of it's width. You can change it around a littile to get what you want
<View
android:id="#+id/spacerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
Also just as an advice
you're not supposed to use match_parent in ConstraintLayout, use 0dp and constraint it to both sides.
Top layout can be replaced with FrameLayout, cause you don't really use any constraints
You can use a customized BottomSheetDialogFragment that has a theme of Theme_Translucent_NoTitleBar, and change the y value of the root layout of the dialog whenever the user drags it up or down.
class MyDialogFragment(height: Int) : BottomSheetDialogFragment(), View.OnTouchListener {
private val outsideWindowHeight = height
private val rootLayout by lazy {
requireView().findViewById<LinearLayout>(R.id.dialog_root)
}
private var oldY = 0
private var baseLayoutPosition = 0
private var defaultViewHeight = 0
private var isClosing = false
private var isScrollingUp = false
private var isScrollingDown = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return BottomSheetDialog(
requireContext(),
android.R.style.Theme_Translucent_NoTitleBar
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view: View = inflater.inflate(
R.layout.fragment_dialog, container,
false
)
view.setBackgroundResource(R.drawable.rounded_background)
(dialog as BottomSheetDialog).apply {
setCancelable(false)
behavior.peekHeight =
(outsideWindowHeight * 0.3).toInt() // Minimum height of the BottomSheet is 30% of the root layout (to leave the 70% to the main layout)
}
return view
}
#SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rootLayout.setOnTouchListener(this)
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
// Get finger position on screen
val y = event!!.rawY.toInt()
// Switch on motion event type
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// save default base layout height
defaultViewHeight = rootLayout.height
oldY = y
baseLayoutPosition = rootLayout.y.toInt()
}
MotionEvent.ACTION_UP -> {
// If user was doing a scroll up
if (isScrollingUp) {
// Reset baselayout position
rootLayout.y = 0f
// We are not in scrolling up anymore
isScrollingUp = false
}
// If user was doing a scroll down
if (isScrollingDown) {
// Reset baselayout position
rootLayout.y = 0f
// Reset base layout size
rootLayout.layoutParams.height = defaultViewHeight
rootLayout.requestLayout()
// We are not in scrolling down anymore
isScrollingDown = false
}
}
MotionEvent.ACTION_MOVE -> {
if (rootLayout.y <= -100) {
return true
}
if (!isClosing) {
val currentYPosition = rootLayout.y.toInt()
// If we scroll up
if (oldY > y) {
// First time android rise an event for "up" move
if (!isScrollingUp) {
isScrollingUp = true
}
rootLayout.y = rootLayout.y + (y - oldY)
} else {
// First time android rise an event for "down" move
if (!isScrollingDown) {
isScrollingDown = true
}
// change position because view anchor is top left corner
rootLayout.y = rootLayout.y + (y - oldY)
rootLayout.requestLayout()
}
// Update position
oldY = y
}
}
}
return true
}
}
fragment_dialog.xml (Nothing fancy):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/tv_bottom_sheet_heading"
android:layout_width="wrap_content"
android:layout_height="#dimen/dp_56"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:gravity="center"
android:text="#string/bottom_sheet_option_heading"
android:textColor="#android:color/black"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_camera"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_camera_alt_black_24dp"
android:drawableLeft="#drawable/ic_camera_alt_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_camera"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_gallery"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_insert_photo_black_24dp"
android:drawableLeft="#drawable/ic_insert_photo_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_gallery"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="#dimen/md_bottom_sheet_separator_top_margin"
android:layout_marginBottom="#dimen/dp_8"
android:background="#color/grayTextColor" />
<TextView
android:id="#+id/tv_btn_remove_photo"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_delete_black_24dp"
android:drawableLeft="#drawable/ic_delete_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_remove_photo"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Material button"
android:textAppearance="#style/TextAppearance.AppCompat.Medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/longText1"
android:textColor="#color/white"
android:textSize="22sp" />
</LinearLayout>
And send the height of the root ViewGroup of the main layout to the dialog in the main activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val root = findViewById<ConstraintLayout>(R.id.root)
root.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
root.viewTreeObserver
.removeOnGlobalLayoutListener(this)
val dialogFragment = MyDialogFragment(root.height)
dialogFragment.show(supportFragmentManager, "dialog_tag")
}
})
}
}
Preview:
I have this layout of my login activity. I want to overlay progressBar as like it can be done using FrameLayout. How to do this using ConstraintLayout?
<layout>
<data>
<variable
name="vm"
type="com.app.android.login.vm" />
</data>
<ScrollView 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:fillViewport="true"
tools:context="com.app.android.login.LoginActivity"
tools:ignore="missingPrefix">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="#dimen/default_view_margin_bottom_8dp">
<android.support.design.widget.TextInputLayout
android:id="#+id/til_login_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:textColorHint="#color/colorSecondaryText"
app:hintTextAppearance="#style/AppTheme.InputLayoutStyle"
app:layout_constraintBottom_toTopOf="#+id/til_login_password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/login_email"
android:imeOptions="actionNext"
android:singleLine="true"
android:text="#={vm.emailField}"
android:textColor="#color/colorPrimaryText" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="#+id/til_login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:textColorHint="#color/colorSecondaryText"
app:hintTextAppearance="#style/AppTheme.InputLayoutStyle"
app:layout_constraintBottom_toTopOf="#+id/btn_login_login"
app:layout_constraintTop_toBottomOf="#+id/til_login_email"
app:layout_constraintVertical_chainStyle="packed">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/login_password"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:singleLine="true"
android:text="#={vm.passwordField}"
android:textColor="#color/colorPrimaryText" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="#+id/btn_login_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:layout_marginTop="48dp"
android:onClick="#{vm::login}"
android:text="#string/login_btn_text"
android:textColor="#color/colorWhite"
app:layout_constraintBottom_toTopOf="#+id/textview_login_forgot_password"
app:layout_constraintTop_toBottomOf="#+id/til_login_password"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/textview_login_forgot_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:layout_marginTop="36dp"
android:gravity="center"
android:text="#string/login_forgot_password"
app:layout_constraintBottom_toTopOf="#+id/btn_login_register"
app:layout_constraintTop_toBottomOf="#+id/btn_login_login"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="#+id/btn_login_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/default_view_margin_right_8dp"
android:layout_marginStart="#dimen/default_view_margin_left_8dp"
android:text="#string/login_sign_up"
android:textColor="#color/colorWhite"
app:layout_constraintBottom_toBottomOf="parent" />
<ProgressBar
android:id="#+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{vm.progressVisibility}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
</layout>
It looks like this:
I am looking for solution which should work for API level 19+. I don't want to add more hierarchy in my layout by wrapping Button or ProgressBar inside ViewGroup or so.
There are two options, in each case you add one attribute to your <ProgressBar/> tag. It is either
android:translationZ="2dp"
or
android:elevation="2dp"
API level must be >= 21.
If you want to 100% overlay the target view, constrain all the sides of the overlaying view to the corresponding sides of the target view and set the height and width of the overlaying view to 0dp like the following:
<View
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#id/animation_view"
app:layout_constraintLeft_toLeftOf="#id/animation_view"
app:layout_constraintRight_toRightOf="#id/animation_view"
app:layout_constraintTop_toTopOf="#id/animation_view"/>
Here is a working example. In the following image, a red scrim is placed over an image. The XML follows the image.
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/animation_view"
android:layout_width="250dp"
android:layout_height="250dp"
android:src="#mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#AAFF0000"
app:layout_constraintBottom_toBottomOf="#id/animation_view"
app:layout_constraintLeft_toLeftOf="#id/animation_view"
app:layout_constraintRight_toRightOf="#id/animation_view"
app:layout_constraintTop_toTopOf="#id/animation_view" />
</android.support.constraint.ConstraintLayout>
See the documentation for ConstraintLayout for more information.
Set an elevation on the ProgressBar 2dp seems to work.
android:elevation="2dp"
You could also try setting translationZ as suggested in the answer to a similar question. For me this works on an emulator running API 17 and the progress bar appeared on top as expected. If you get any warning than check your build version
In my observations Android "stacks" the views along the z-axis in the order they appear in the xml file in that the view at the end of the file will be at the top of the z-axis and the view at the start of the file will be at the bottom. Of course once you start setting elevation and zTranslation etc the z-axis stack order will be affected...
so moving the progressBar declaration to the end of the ConstraintLayout should make it appear above the other views, this has worked for me.
Here is your API 19 solution. It puts a CircularProgressDrawable in an overlay on top of your ConstraintLayout.
This is what it looks like:
What you have to do is:
Get rid of the XML ProgressBar.
Give your XML ConstraintLayout an id, for example:
android:id="#+id/cl"
Add this code to your MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
boolean toggle;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ConstraintLayout cl = findViewById(R.id.cl);
cl.setOnClickListener(this);
}
#Override
public void onClick(final View view) {
toggle ^= true;
if (toggle) {
startProgress();
} else {
stopProgress();
}
}
void startProgress() {
final ConstraintLayout cl = findViewById(R.id.cl);
final CircularProgressDrawable progressDrawable = new CircularProgressDrawable(this);
progressDrawable.setColorSchemeColors(Color.MAGENTA);
progressDrawable.setCenterRadius(50f);
progressDrawable.setStrokeWidth(12f);
progressDrawable.setStrokeCap(Paint.Cap.BUTT);
cl.post(new Runnable() {
#Override
public void run() {
progressDrawable.setBounds(0, 0, cl.getWidth(), cl.getHeight());
cl.getOverlay().add(progressDrawable);
}
});
progressDrawable.start();
}
void stopProgress() {
final ConstraintLayout cl = findViewById(R.id.cl);
final CircularProgressDrawable progressDrawable = new CircularProgressDrawable(this);
progressDrawable.setColorSchemeColors(Color.MAGENTA);
progressDrawable.setCenterRadius(50f);
progressDrawable.setStrokeWidth(12f);
progressDrawable.setStrokeCap(Paint.Cap.BUTT);
cl.post(new Runnable() {
#Override
public void run() {
cl.getOverlay().clear();
}
});
progressDrawable.stop();
}
}
I want to provide you with an alternative to the XML solution.
You can also add a view programmatically to your root view. (ConstraintLayout)
ViewGroupOverlay is an extra layer that sits on top of a ViewGroup (the "host view") which is drawn after all other content in that view (including the view group's children). Interaction with the overlay layer is done by adding and removing views and drawables.
<android.support.constraint.ConstraintLayout
android:id="#+id/root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="#dimen/default_view_margin_bottom_8dp">
If you reference the root in your code you can then add your ProgressBar.
Something like this:
rootLayout.overlay.add(ProgressBar(context).apply {
measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY))
layout(0, 0, 100, 100)
})
You can also check out this link for extra info.
And this SO question can also help.
You could try one of these options:
1. Add a relative layout outside your layout as here
<RelativeLayout
android:id="#+id/relativelayout_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="#aa000022" >
<ProgressBar
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateOnly="true" />
</RelativeLayout>
Add a view overlay in your activity's onCreate as described here Android 4.3 User Interface View overlays
Another easy way to solve this is to change Button to View, change background and size. Overlay it with TextView for replacing old Button text and another View for clickable later
You can achieve your goal by setting the Z translation for the view.
Put this method in a helper class (for example: UIUtils) and use it for your view:
/**
* Set the 'Z' translation for a view
*
* #param view {#link View} to set 'Z' translation for
* #param translationZ 'Z' translation as float
*/
public static void setTranslationZ(View view, float translationZ) {
ViewCompat.setTranslationZ(view, translationZ);
}
USAGE:
UIUTils.setTranslationZ(YOUR_VIEW, 5);
Updated Solution for 2020
Kotlin language
I have 2 scenarios, the first is just a normal button and the second one is
progress on the center of the button with preventing the click action
here is just shortcode if you want to know how I handle button and progress bar
Use jsut these two method if you want get an idea
private fun disableButton() {
button.run {
preservedButtonText = text.toString()
text = ""
isClickable = true
isEnabled = false
}
progressBar.visibility = View.VISIBLE
}
private fun enableButton() {
button.run {
text = preservedButtonText
isClickable = false
}
progressBar.visibility = View.INVISIBLE
}
and surprise is had for you a class that does all the stuff you just use it
I handle create parent constraint layout and it's child's (button, progress bar)
programmatically
/**
* Created by Amirahmad Adibi.
* XProject | Copyrights 2019-12-16.
*/
class ZProgressButton(context: Context, var attributeSet: AttributeSet) :
ConstraintLayout(context, attributeSet) {
var isLoading: Boolean = false
set(value) {
handelShowing(value)
field = value
}
var preservedButtonText: String = ""
var button: Button
var progressBar: ProgressBar
init {
resetPadding()
button = getButton(context, attributeSet)
progressBar = getProgressBar(context, attributeSet)
preservedButtonText = button.text.toString()
addView(button)
addView(progressBar)
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
progressBar.elevation = 2f
ViewCompat.setTranslationZ(progressBar, 5f)
ViewCompat.setTranslationZ(button, 1f)
handleConstraint(this, button, progressBar)
}
private fun handleConstraint(
view: ConstraintLayout,
button: Button,
progressBar: ProgressBar
) {
with(ConstraintSet()) {
clone(view)
connect(button.id, ConstraintSet.RIGHT, view.id, ConstraintSet.RIGHT)
connect(button.id, ConstraintSet.LEFT, view.id, ConstraintSet.LEFT)
connect(button.id, ConstraintSet.TOP, view.id, ConstraintSet.TOP)
connect(button.id, ConstraintSet.BOTTOM, view.id, ConstraintSet.BOTTOM)
connect(progressBar.id, ConstraintSet.RIGHT, view.id, ConstraintSet.RIGHT)
connect(progressBar.id, ConstraintSet.LEFT, view.id, ConstraintSet.LEFT)
connect(progressBenter code herear.id, ConstraintSet.TOP, view.id, ConstraintSet.TOP)
connect(progressBar.id, ConstraintSet.BOTTOM, view.id, ConstraintSet.BOTTOM)
applyTo(view)
}
}
private fun getButton(context: Context, attributeSet: AttributeSet): Button {
return ZButton(context, attributeSet).run {
id = View.generateViewId()
text = "text"
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
isClickable = true
return#run this
}
}
private fun getProgressBar(context: Context, attributeSet: AttributeSet): ProgressBar {
return ProgressBar(context, attributeSet).run {
id = View.generateViewId()
visibility = View.VISIBLE
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setPadding(4, 4, 4, 4)
return#run this
}
}
fun resetPadding() {
setPadding(0, 0, 0, 0)
}
fun handelShowing(value: Boolean) {
removeView(button)
removeView(progressBar)
if (value) {
disableButton()
} else {
enableButton()
}
addView(button)
addView(progressBar)
}
private fun disableButton() {
button.run {
preservedButtonText = text.toString()
text = ""
isClickable = true
isEnabled = false
}
progressBar.visibility = View.VISIBLE
}
private fun enableButton() {
button.run {
text = preservedButtonText
isClickable = false
}
progressBar.visibility = View.INVISIBLE
}
}
Usage :
buttonLoading.setOnClickListener {
progressButton.isLoading = true
}
buttonDone.setOnClickListener {
progressButton.isLoading = false
}
android:elevation is not working in API level 19. A simple trick will be used to use a card view and set the elevation of this card
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:padding="#dimen/padding_10"
app:contentPadding="16dp"/>
With the above case, I fixed it this way
<FrameLayout
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="#+id/progress_reset_password"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateTint="#color/colorAccent"
android:layout_gravity="center_horizontal"
android:translationZ="2dp"
android:elevation="2dp"/>
<Button
android:id="#+id/btnGetOTP"
style="#style/styleButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:layout_marginRight="80dp"
android:text="#string/check_email"
android:textColor="#color/colorWhite"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
Open the image above here
I added overlays to the image view layout using an extra image. Also, put the extra overlay image view just immediately below the main image view.
<?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.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="#dimen/spacing_small"
android:layout_marginTop="#dimen/spacing_normal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/imageViewBlog"
android:layout_width="match_parent"
android:layout_height="#dimen/blog_post_height"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_placeholder" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/black_overlay" />
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/tvBlogTopTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/spacing_small"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:maxLines="1"
android:textColor="#color/pale_grey"
app:layout_constraintBottom_toTopOf="#+id/tvBlogBigTitle"
app:layout_constraintEnd_toEndOf="#+id/tvBlogBigTitle"
app:layout_constraintStart_toStartOf="#id/tvBlogBigTitle"
tools:text="Travel Inspiration" />
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/tvBlogBigTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/spacing_normal"
android:layout_marginLeft="#dimen/spacing_normal"
android:layout_marginEnd="#dimen/spacing_normal"
android:layout_marginRight="#dimen/spacing_normal"
android:layout_marginBottom="#dimen/spacing_normal"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#color/white"
android:textSize="#dimen/font_tiny"
app:layout_constraintBottom_toBottomOf="#+id/imageViewBlog"
app:layout_constraintEnd_toEndOf="#id/imageViewBlog"
app:layout_constraintStart_toStartOf="#id/imageViewBlog"
tools:text="This Photographer is Selling Prints of Her Wildlife Photos to \n in the Masai Mara…" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Looks like the below picture.
Overlay image:
There are lots of examples here on how to set z order in xml, which are great. However, if you if you end up needing to programmatically adjust view overlays, and those views happen to be buttons, make sure to set stateListAnimator to null in your xml layout file. stateListAnimator is android's under-the-hood process to adjust translationZ of buttons when they are clicked, so the button that is clicked ends up visible on top. This is not always what you want... for full Z order control, do this: