I want to draw the LudoDeck on top of the LudoBoard. I have create a custom view group and disable the willNotDraw and setup the child view position and size, but it somewhat does not rendered to the screen. I saw the log for the LudoDeck onDraw in logcat, but I'm not sure why it does not drawn, is it because I have not set the view size correctly?
Can someone help me figuring where is my mistakes? Thanks.
LudoBoard.kt
package io.github.andraantariksa.ludo
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
const val RATIO = 1.0f
class LudoBoard(context: Context, attributeSet: AttributeSet):
ViewGroup(context, attributeSet) {
private val ludoPawnsInLane = arrayOf<LudoPawn>()
private val totalGridToTarget = 6
private var gridSideSize = 0F
private val colors = arrayOf(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN)
private val deck = arrayOf<LudoDeck>(
LudoDeck(context))
init {
setWillNotDraw(false)
deck.forEach {
addView(it)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
var width = measuredWidth
var height = measuredHeight
val widthWithoutPadding = width - paddingLeft - paddingRight
val heightWithoutPadding = height - paddingTop - paddingBottom
val maxWidth = (heightWithoutPadding * RATIO).toInt()
val maxHeight = (widthWithoutPadding / RATIO).toInt()
if (widthWithoutPadding > maxWidth) {
width = maxWidth + paddingLeft + paddingRight
} else {
height = maxHeight + paddingTop + paddingBottom
}
gridSideSize = width / (totalGridToTarget * 2 + 3).toFloat()
val deckSideSize = gridSideSize * 6F
deck.forEach {
it.measure(deckSideSize.toInt(), deckSideSize.toInt())
it.x = 0F
it.y = 0F
}
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// some code to draw the board
}
}
LudoDeck.kt
package io.github.andraantariksa.ludo
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
class LudoDeck(context: Context): View(context) {
private var totalPawn = 4
init {
setWillNotDraw(false)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas) {
Log.d("zzzzz", "Draw")
val p = Paint()
p.color = Color.BLACK
val rect = Rect(0, 0, measuredWidth, measuredHeight)
canvas.drawRect(rect, p)
}
}
So basically I tweaked your code a bit and in the process got to learn about ViewGroups also. Thanks for that!
I have commented the explanations of the changes made in code you can refer to that and if any doubt please feel free to ask..
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.ViewGroup
const val RATIO = 1.0f
class LudoBoard1(context: Context, attributeSet: AttributeSet):
ViewGroup(context, attributeSet) {
// private val ludoPawnsInLane = arrayOf<LudoPawn>()
private val totalGridToTarget = 6
private var gridSideSize = 0F
private val colors = arrayOf(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN)
private val deck = arrayOf<LudoDeck>(
LudoDeck(context, attributeSet), LudoDeck(context, attributeSet))
init {
//setting this will call the onDraw method of the viewGroup. If we just want to treat this as a container
//then set this as 'true'. This way it will not draw anything.
setWillNotDraw(false)
//Add all your custom views here in the beginning.
deck.forEach {
addView(it)
}
}
/**
* This method is used to measure the size of the view itself and also the size of the children.
* Here we calculate the size and allocate it to the children also by calling their "measure()"
* method.
* It's extremely important to call the "setMeasuredDimension()" at the end as this method will
* allocated the measured width and the height.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
var width = measuredWidth
var height = measuredHeight
val widthWithoutPadding = width - paddingLeft - paddingRight
val heightWithoutPadding = height - paddingTop - paddingBottom
val maxWidth = (heightWithoutPadding * RATIO).toInt()
val maxHeight = (widthWithoutPadding / RATIO).toInt()
if (widthWithoutPadding > maxWidth) {
width = maxWidth + paddingLeft + paddingRight
} else {
height = maxHeight + paddingTop + paddingBottom
}
gridSideSize = width / (totalGridToTarget * 2 + 3).toFloat()
val deckSideSize = gridSideSize * 6F
deck.forEach {
it.measure(deckSideSize.toInt(), deckSideSize.toInt())
}
setMeasuredDimension(width, height)
}
/**
* For a viewGroup, its better if we don't draw anything, but still if we have to, then we can.
* The view group is designed as a container where it determines it's own size, it's children's size
* and their positions.
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// some code to draw the board
}
/**
* This is the method where we calculate the positions for every child. Here we determine the
* starting and ending point for every child element of this viewGroup.
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
/*
Here you will determine the position for every child. Calculate the left top right bottom for every child
and allocate it to the layout.
For now, i am just positioning the ludo deck besides each other.
*/
var previousXStartPoint = 0
deck.forEachIndexed { index, it ->
it.layout(previousXStartPoint , 0, previousXStartPoint.plus(it.measuredWidth), (it.measuredHeight))
previousXStartPoint = it.right + 20
}
}
}
And this is the LudoDeck class:
class LudoDeck(context: Context, attrs: AttributeSet?): View(context, attrs) {
private var totalPawn = 4
private val rect = Rect(0, 0, 50, 50)
private val p = Paint()
/*
As we are drawing something in this view, it's appropriate to set call this method in the beginning.
*/
init {
setWillNotDraw(false)
}
/**
* Its advised not to initialize anything in the onMeasure() method. So, we have already initialized
* the rect in the beginning and now we will just update its params.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
rect.right = widthMeasureSpec
rect.bottom = heightMeasureSpec
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
/**
* Its also advised not to initialize anything in onDraw() method, so we have already created the paint
* object. Here we simply draw!
*/
override fun onDraw(canvas: Canvas) {
p.color = Color.BLACK
canvas.drawRect(rect, p)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
//Uncomment to check whether the dims set in onLayout of Ludo Board are properly allocated. :)
//Log.e("ludoDeck", "left: $left, top: $top, right: $right, bottom: $bottom")
}
Lastly, the source where i was able to understand a few things and get this working is : https://academy.realm.io/posts/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/
Let me know how it works out!
Related
I have tried using ProgressBar or SeekBar but how to add left cut on one side and right side also
I've created a sample custom ConstraintLayout for you to achive what you want.it will behave as a regular constraintLayout but just with a custom background shape.
you have to change numbers and logic according to ui ofcourse, but it will cut your imageView inside it and you can also put progressbar and textViews to create your own ui
package com.customView
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
/**
* Created by Payam Monsef
* At: 2022/Jun/27
*/
class CustomConstraintLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
private var strokeWidth = 4F
private var strokeColor = Color.CYAN
private val cutPosition = CutPosition.TOP_LEFT
//like 0.2 of width will be reduced from cut position
private val reduceWidthMultiplier = 0.2
private var mPath: Path? = null
private var mPathBorder: Path? = null
private var mPaint: Paint? = null
init {
mPath = Path()
mPathBorder = Path()
mPaint = Paint()
mPaint?.style = Paint.Style.STROKE
mPaint?.color = strokeColor
mPaint?.strokeWidth = strokeWidth
setBackgroundColor(Color.TRANSPARENT)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val mW = width.toFloat()
val mH = height.toFloat()
mPathBorder?.apply {
reset()
if (cutPosition == CutPosition.BOTTOM_RIGHT) {
moveTo(0F, 0F)
lineTo(mW, 0F)
lineTo(mW - (reduceWidthMultiplier * mW).toFloat(), mH)
lineTo(0F, mH)
close()
} else {
moveTo((reduceWidthMultiplier * mW).toFloat(), 0F)
lineTo(mW, 0F)
lineTo(mW, mH)
lineTo(0F, mH)
close()
}
}
val offset = strokeWidth / 2
mPath?.apply {
reset()
if (cutPosition == CutPosition.BOTTOM_RIGHT) {
moveTo(offset, offset)
lineTo(mW - offset, offset)
lineTo(mW - (reduceWidthMultiplier * mW).toFloat(), mH - offset)
lineTo(offset, mH - offset)
close()
} else {
moveTo((reduceWidthMultiplier * mW).toFloat() , offset)
lineTo(mW - offset, offset)
lineTo(mW - offset, mH - offset)
lineTo(offset, mH - offset)
close()
}
}
}
override fun onDraw(canvas: Canvas) {
mPathBorder?.let { p ->
mPaint?.let { paint ->
canvas.drawPath(p, paint)
}
}
mPath?.let {
canvas.clipPath(it)
}
}
private enum class CutPosition {
TOP_LEFT, BOTTOM_RIGHT
}
}
Its better than you define above variables as custom attributes.you can read the Google documents to do that
I am a beginner. I have started learning custom views these days, and there are almost no problems in the process.
When I went to Google to solve this problem, some people proposed solutions, but none of them was successful. I use a custom view written by Kotlin.
This is my custom view class,and the name is MyView.kt
package com.example.demos
import android.R
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
class MyView : View {
// init
private lateinit var arcPaint: Paint
private lateinit var progressTextPaint: Paint
// private lateinit var arcPaintColor: Color
private var arcPaintColor = Color.BLACK
// private lateinit var progressTextPaintColor: Color
private var progressTextPaintColor = Color.BLACK
private var angle = 0f
private var progress: Float = angle / 3.6f
// get/set
fun setArcPaintColor(color: Int) {
arcPaintColor = color
}
fun getArcPaintColor(): Int {
return arcPaintColor
}
fun setProgressTextPaintColor(color: Int) {
progressTextPaintColor = color
}
fun getProgressTextPaintColor(): Int {
return progressTextPaintColor
}
fun setAngle(float: Float) {
angle = float
progress = angle / 3.6f
invalidate()
}
fun getAngle(): Float {
return angle
}
fun setProgress(float: Float) {
progress = float
angle = progress * 3.6f
invalidate()
}
fun getProgress(): Float {
return progress
}
/*call method initPaint()*/
constructor(context: Context) : super(context) {
initPaint()
}
constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) {
initPaint()
}
constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
) {
arcPaintColor = typedArray.getColor(R.styleable.arcPaintColor,)
initPaint()
}
/*override onDraw(),draw view*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawView(canvas)
}
//init paints
private fun initPaint() {
arcPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.color = arcPaintColor
it.strokeWidth = 5f
it.strokeWidth = 40f
it.style = Paint.Style.STROKE
it.strokeCap = Paint.Cap.ROUND
}
progressTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.color = progressTextPaintColor
// it.color = Color.GREEN
// it.setStrokeWidth(5f)
it.style = Paint.Style.FILL
it.textSize = 50f
}
}
/*draw view*/
private fun drawView(canvas: Canvas?) {
val displayWidth = width
val displayHeight = height
/*get center of circle*/
val centerX = (displayWidth / 2).toFloat()
val centerY = (displayHeight / 2).toFloat()
/*get radius*/
val radius = min(displayWidth, displayHeight) / 4
val rectF = RectF(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
canvas?.drawArc(
rectF,
0f,
angle,
false,
arcPaint
)
canvas?.drawText(
"${String.format("%.1f", progress)}%",
centerX - progressTextPaint.measureText("${String.format("%.1f", progress)}%") / 2,
centerY,
progressTextPaint
)
}
}
This is my xml file of custom attributes
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="arcPaintColor" format="color"/>
<attr name="progressTextPaintColor" format="color"/>
</declare-styleable>
</resources>
the xml file of my custom attribute
The issue is that you are importing android.R
You need to import the [you package name].R version instead.
You are not picking up the custom attributes correctly. This is how it should be done:
val typedArray =
context.theme.obtainStyledAttributes(
attributeSet, R.styleable.MyView, 0, 0
)
arcPaintColor = typedArray.getColor(R.styleable.MyView_arcPaintColor, 0)
typedArray.recycle() // Important!
initPaint()
You will have to make sure that this code executes in each of your constructors. The first zero will be replace by defStyleAttr when that is available. I suggest that you integrate the above code into initPaint().
See the documentation for obtainStyledAttributes().
I'm making half circle custom view on Android. However, I'm struggling to remove the un-needed bottom white space on wrap content. I think because it is drawing based on 'a full circle' calculation.
I'm sharing my Custom View implementation, as well as how I call it from my application.
See also this image:
Click here for the screenshot
Note: If I change the onMeasure size, then it will cut the upper circle:
Click here for the screenshot
class CircularProgressView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var circle = RectF()
private val paint = Paint()
private var size = 0
init {
paint.isAntiAlias = true
paint.style = Paint.Style.STROKE
paint.strokeWidth = strokeWidth.toFloat()
paint.strokeCap = Paint.Cap.BUTT
setupAttributes(attrs)
}
private fun setupAttributes(attrs: AttributeSet?) {
// TypedArray objects are shared and must be recycled.
typedArray.recycle()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawBackground(canvas)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
size = Math.min(measuredWidth, measuredHeight)
setMeasuredDimension(size, size)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val centerX = w / 2
val centerY = h / 2
// Pick the minimum value so that it can fit the container. Radius is half size
val radius = size / 2f
// Create the background and progress circle, adding dialStrokeWidth in equation so that make sure the dial can fit container
circle.top = (centerY - radius)
circle.bottom = (centerY + radius)
circle.left = (centerX - radius)
circle.right = (centerX + radius)
}
private fun drawBackground(canvas: Canvas) {
paint.shader = null
paint.color = backGroundColor
canvas.drawArc(circle, startAngle, sweepAngle, false, paint)
}
}
This is how I use it:
<com.enova.circular_progress.CircularProgressView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#color/white"
app:backgroundColor="#color/colorHint"
app:dialColor="#color/colorPrimary"
app:foregroundColorEnd="#color/colorPrimary"
app:foregroundColorStart="#color/colorPrimaryDark"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:percent="80">
</com.enova.circular_progress.CircularProgressView>
Change your onMeasure. YOu're setting your measured width and height to the same value. If you only want to display a top half circle, then you want to set the height to half the width. Otherwise, it will think the view is the full circle in height.
At this class i draw simple ground for Tic-Tae-Toe. It consists of intercepted lines and "X" in the center of the cell.
So when User touches the cell, then textColor in it should be changed.
I use invalidate(rect) to redraw concrete cell, but in this case every cell changes it's textColor.
According to Romain Guy words, the canvas with whole view Rect comes
for drawing. The DisplayList will find interceptions between drawing commands and your dirty Rect, and only those commands will be drawn. But seems, that it doesn't work so way.
Partial invalidation in custom Android view with hardware acceleration
And also i found strange code change between 4.4 - 5.0 Android. So you can see, that mCurrentDirty disappeared from code at all.
Android View.invalidate(Rect) different behavior between two devices
P.S for SA this logic works correctly, and only dirty Rect is changed.
package com.eugeneshapovalov.fizmigclient
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import timber.log.Timber
class TicTacToeView : View, View.OnTouchListener {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
companion object {
const val CELL_SIZE_RATIO = 1 / 3f
const val LINE_SIZE = 2f
const val CELL_COUNT = 3
}
val linePaint = Paint()
val textPaint = Paint()
val dirtyCell = Rect()
var colorNumber: Int
init {
setOnTouchListener(this)
colorNumber = 0
linePaint.strokeWidth = resources.displayMetrics.density * LINE_SIZE
textPaint.textSize = 60f
}
private lateinit var cells: Array<Array<Rect>>
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
initCells()
}
private fun initCells() {
cells = Array(CELL_COUNT, { Array(CELL_COUNT, { Rect() }) })
val xCell = (width * CELL_SIZE_RATIO).toInt()
val yCell = (height * CELL_SIZE_RATIO).toInt()
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
cells[i][j].left = (x + j * xCell).toInt()
cells[i][j].top = (y + i * yCell).toInt()
cells[i][j].right = (x + (j + 1) * xCell).toInt()
cells[i][j].bottom = (y + (i + 1) * yCell).toInt()
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawLines(canvas)
drawText(canvas)
}
private fun drawLines(canvas: Canvas) {
// Vertical lines
canvas.drawLine(x + width * CELL_SIZE_RATIO, y, x + width * CELL_SIZE_RATIO, y + height, linePaint)
canvas.drawLine(x + width * 2 * CELL_SIZE_RATIO, y, x + width * 2 * CELL_SIZE_RATIO, y + height, linePaint)
// Horizontal lines
canvas.drawLine(x, y + height * CELL_SIZE_RATIO, x + width, y + height * CELL_SIZE_RATIO, linePaint)
canvas.drawLine(x, y + height * 2 * CELL_SIZE_RATIO, x + width, y + height * 2 * CELL_SIZE_RATIO, linePaint)
}
private fun drawText(canvas: Canvas) {
textPaint.color = when (colorNumber % 5) {
0 -> Color.BLACK
1 -> Color.BLUE
2 -> Color.RED
3 -> Color.GRAY
4 -> Color.YELLOW
else -> Color.GREEN
}
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
val rect = cells[i][j]
canvas.drawText("X", rect.exactCenterX(), rect.exactCenterY(), textPaint)
}
}
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_UP -> {
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
val rect = cells[i][j]
if (rect.contains(event.x.toInt(), event.y.toInt())) {
colorNumber += (j + 7)
Timber.d("Rect: ${rect.flattenToString()}")
invalidate(rect)
}
}
}
}
}
return true
}
}
I am new to android and I am trying to draw a sine wave on the screen on the trigger of accelerometer values change. I just need a plain sine wave dynamically drawn on the screen(confined to screen). I can draw the coordinates on Canvas and validate screen dimensions but I am not able to think of how to convert sine values(ranging from 0 to 1) to the screen coordinates.
I was trying something like this in onSensorChanged():
tvarY = sin(tvarX)*2.0; // tvarX and tvarY are double values
tvarX= (tvarX+ 2); // 2.0 is for magnifying
xPosition = (float)tvarX;
yPosition = (float)tvarY;
But the values of tvarx using this approach are always switching between back and forth from infinity to 0. Can anybody please suggest me any approach to change the values and convert them into screen coordinates for drawing a proper sine wave?
Thanks :-)
I think you can use function:
path.rQuadTo(float dx1, float dy1, float dx2, float dy2)
Same as quadTo, but the coordinates are considered relative to the last point on this contour.
I wrote a sentence for your reference:
Path mpath = new Path();
mpath.moveTo(0, 100);
mpath.rQuadTo(20, 5, 40, 0);
mpath.rQuadTo(20, -5, 40, 0);
You can try once, then you will get a sine wave like this picture:
I think this method will be easier comparing with converting value of sin to coordinate.
Create custom view:
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class WaveView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var amplitude = 30f.toDp() // scale
private var speed = 0f
private val path = Path()
private var paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var animator: ValueAnimator? = null
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
animator?.cancel()
animator = createAnimator().apply { start() }
}
override fun onDraw(c: Canvas) = c.drawPath(path, paint)
private fun createAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, Float.MAX_VALUE).apply {
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
speed -= WAVE_SPEED
createPath()
invalidate()
}
}
}
private fun createPath() {
path.reset()
paint.color = Color.parseColor("#1da6f9")
path.moveTo(0f, height.toFloat())
path.lineTo(0f, amplitude)
var i = 0
while (i < width + 10) {
val wx = i.toFloat()
val wy = amplitude * 2 + amplitude * Math.sin((i + 10) * Math.PI / WAVE_AMOUNT_ON_SCREEN + speed).toFloat()
path.lineTo(wx, wy)
i += 10
}
path.lineTo(width.toFloat(), height.toFloat())
path.close()
}
override fun onDetachedFromWindow() {
animator?.cancel()
super.onDetachedFromWindow()
}
companion object {
const val WAVE_SPEED = 0.3f
const val WAVE_AMOUNT_ON_SCREEN = 350
}
private fun Float.toDp() = this * context.resources.displayMetrics.density
}
In activity layout(ConstraintLayout):
<com.uvn.test.WaveView
android:layout_width="0dp"
android:layout_height="350dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>