How to animate TextView width from Visible to Gone state? - android

I want to animate TextView width when I change visibility of TextView. I don't wanna achieve generic "fade in/out" effect, but I wanna collapse TextView from sides to 0 width.
Here are my functions:
fun fadeInTextViewSize(){
val parentWidth = (buttonText.parent as View).measuredWidth
val widthAnimator = ValueAnimator.ofInt(buttonText.width, parentWidth)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
fun fadeOutTextViewSize(){
val widthAnimator = ValueAnimator.ofInt(buttonText.width, 0)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
Issue is that with this function, my TextView height is for some reason set to MATCH_PARENT.

I assume that you need the TextView to appear as if erased from both ends equally. Here is a technique that will do that:
private lateinit var buttonText: TextView
private var viewWidth = 0
fun fadeOutTextViewSize() {
with(buttonText) {
// Enable scrolling for the view since we will need to scroll horizontally to center text.
setMovementMethod(ScrollingMovementMethod())
setHorizontallyScrolling(true)
// Lock in the starting width and height.
viewWidth = buttonText.width
layoutParams.width = buttonText.width
layoutParams.height = buttonText.height
visibility = View.VISIBLE
}
with(ValueAnimator.ofInt(viewWidth, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.layoutParams.width = newWidth
// Shift text left so it stays centered in the initial bounds.
val scrollX = (viewWidth - newWidth) / 2
buttonText.scrollTo(scrollX, 0)
buttonText.requestLayout()
}
doOnEnd {
// Make the view invisible to seal its disappeared status. This could also be
// "GONE" depending on the desired effect.
buttonText.visibility = View.INVISIBLE
}
start()
}
}
The technique is to lock in the size and height of the TextView to its initial size. From that initial size, the animation shrinks the view's width from the initial size to zero. Since the layout will position the start of the text to the start of the view, the text is scrolled left to maintain its position on the screen as the width shrinks.
The red line is there just to mark the center of the TextView and is not needed.
Causing the view to reappear is mostly the opposite of the code here.
The test layout:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_blue_dark"
tools:context=".MainActivity">
<TextView
android:id="#+id/buttonText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light"
android:text="Hello World!"
android:textSize="48sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/buttonText"
app:layout_constraintEnd_toEndOf="#id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toTopOf="#id/buttonText" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Click Here"
app:layout_constraintEnd_toEndOf="#+id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toBottomOf="#id/buttonText" />
</androidx.constraintlayout.widget.ConstraintLayout>
There is another way to accomplish this which is to use a custom TextView that will clip its canvas to make the view shrink and expand. This method has the following advantages:
It is probably more efficient in that it does not require additional layout of the TextView.
If the TextView is within a ConstraintLayout and there are other views that are constrained to the start and/or end of the TextView, those views will not shift since the boundaries of the TextView remain unchanged.
private lateinit var buttonText: ClippedTextView
fun fadeOutTextViewSize() {
with(ValueAnimator.ofInt(buttonText.width, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.setClippedWidth(newWidth)
buttonText.invalidate()
}
start()
}
}
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mClipWidth = right - left
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}
We can also bring all the logic into the custom TextView as follows:
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
private val mAnimator: ValueAnimator by lazy {
ValueAnimator().apply {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
setClippedWidth(animation.animatedValue as Int)
invalidate()
}
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
setClippedWidth(right - left)
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun expandView() {
doWidthAnimation(mClipWidth, 0)
}
fun shrinkView() {
doWidthAnimation(mClipWidth, width)
}
private fun doWidthAnimation(startWidth: Int, endWidth: Int) {
animation?.cancel()
with(mAnimator) {
setIntValues(startWidth, endWidth)
start()
}
}
private fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}

Related

How to fit the view to the size of the object? (Kotlin)

I'm building my first game in Android Studio. Right now, dots fall from the top of the screen down to the bottom. For some reason, in Layout Inspector the view of each dot is the entire screen even though the dots are comparatively small. This negatively affects the game since when a user presses anywhere on the screen, it deletes the most recently created dot rather than the one pressed. I want to get the dot's view to match the size of the actual dots without effecting other functionality.
Dot.kt
class Dot(context: Context, attrs: AttributeSet?, private var dotColor: Int, private var xPos: Int, private var yPos: Int) : View(context, attrs) {
private var isMatching: Boolean = false
private var dotIsPressed: Boolean = false
private var isDestroyed: Boolean = false
private lateinit var mHandler: Handler
private lateinit var runnable: Runnable
init {
this.isPressed = false
this.isDestroyed = false
mHandler = Handler()
runnable = object : Runnable {
override fun run() {
moveDown()
invalidate()
mHandler.postDelayed(this, 20)
}
}
val random = Random()
xPos = random.nextInt(context.resources.displayMetrics.widthPixels)
startFalling()
startDrawing()
}
// other methods
fun getDotColor() = dotColor
fun getXPos() = xPos
fun getYPos() = yPos
fun isMatching() = isMatching
fun setMatching(matching: Boolean) {
this.isMatching = matching
}
fun dotIsPressed() = dotIsPressed
override fun setPressed(pressed: Boolean) {
this.dotIsPressed = pressed
}
fun isDestroyed() = isDestroyed
fun setDestroyed(destroyed: Boolean) {
this.isDestroyed = destroyed
}
fun moveDown() {
// code to move the dot down the screen
yPos += 10
}
fun checkCollision(line: Line) {
// check if dot is colliding with line
// if yes, check if dot is matching or not
// update the dot state accordingly
}
fun startFalling() {
mHandler.post(runnable)
}
fun startDrawing() {
mHandler.postDelayed(object : Runnable {
override fun run() {
invalidate()
mHandler.postDelayed(this, 500)
}
}, 500)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (!isDestroyed) {
val paint = Paint().apply {
color = dotColor
}
canvas?.drawCircle(xPos.toFloat(), yPos.toFloat(), 30f, paint)
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var score = 0
private lateinit var scoreCounter: TextView
private val dots = mutableListOf<Dot>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
createLine(Color.RED, 5000)
scoreCounter = TextView(this)
scoreCounter.text = score.toString()
scoreCounter.setTextColor(Color.WHITE)
val layout = findViewById<ConstraintLayout>(R.id.layout)
layout.setBackgroundColor(Color.BLACK)
val params = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
scoreCounter.layoutParams = params
layout.addView(scoreCounter)
val dotColors = intArrayOf(Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW)
val random = Random()
val handler = Handler()
val runnable = object : Runnable {
override fun run() {
val dotColor = dotColors[random.nextInt(dotColors.size)]
createAndAddDot(0, 0, dotColor)
handler.postDelayed(this, 500)
}
}
handler.post(runnable)
}
fun updateScore(increment: Int) {
score += increment
scoreCounter.text = score.toString()
}
fun createAndAddDot(x: Int, y: Int, color: Int) {
Log.d("Dot", "createAndAddDot called")
val dot = Dot(this, null, color, x, y)
val layout = findViewById<ConstraintLayout>(R.id.layout)
layout.addView(dot)
dots.add(dot)
dot.setOnTouchListener { view, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
val dotToRemove = dots.find { it == view }
dotToRemove?.let {
layout.removeView(it)
dots.remove(it)
updateScore(1)
view.performClick()
}
}
true
}
}
fun createLine(color: Int, interval: Int) {
Log.d("Line", "createLine called")
val line = Line(color, interval)
val lineView = Line.LineView(this, null, line)
val layout = findViewById<ConstraintLayout>(R.id.layout)
if (layout == null) {
throw IllegalStateException("Layout not found")
}
layout.addView(lineView)
val params = ConstraintLayout.LayoutParams(2000, 350)
lineView.layoutParams = params
params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
params.bottomMargin = (0.1 * layout.height).toInt()
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Your view here -->
<View
android:id="#+id/view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- Guideline set to 10% from the bottom -->
<androidx.constraintlayout.widget.Guideline
android:id="#+id/bottom_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.1" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changing the view size with
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val diameter = 40 // or any other desired diameter for the dots setMeasuredDimension(diameter, diameter) }
That made the view size a square stuck in the top left corner. As I played around with it, I could only get dots to show in that small window in the top corner rather than moving down the screen from different starting x-positions
Your custom view isn't a dot, it's a large display area that draws a dot somewhere inside it and animates its position. In onDraw you're drawing a circle at xPos (a random point on the screen width via displayMetrics.widthPixels) and yPos (an increasing value which moves the dot down the view).
There are two typical approaches to things like this:
use simple views like ImageViews. Let the containing Activity or Fragment add them to a container and control their position, maybe using the View Animation system. Handle player interaction by giving them click listeners and let the view system work out what's been clicked.
create a custom view that acts as the game area. Let that custom view control the game state (what dots exist, where they currently are) and draw that state in onDraw. Handle touch events on the view, and work out if those touches coincide with a dot (by comparing to the current game state).
What you're doing is sort of a combination of the two with none of the advantages that either approach gives on its own. You have multiple equally-sized "game field" views stacked on top of each other, so any clicks will be consumed by the top one, because you're clicking the entire view itself. And because your custom view fills the whole area, you can't move it around with basic view properties to control where the dot is - you have to write the logic to draw the view and animate its contents.
You could implement some code that handles the clicks and decides whether the view consumes it (because it intersects a dot) or passes it on to the next view in the stack, but that's a lot of work and you still have all your logic split between the Activity/Fragment and the custom view itself.
I think it would be way easier to just pick one approach - either use ImageViews sized to the dot you want and let the view system handle the interaction, or make a view that runs the game internally. Personally I'd go with the latter (you'll find it a lot easier to handle dots going out of bounds, get better performance, more control over the look and interaction etc, no need to cancel Runnables) but it's up to you!

Measuring children in custom view breaks in RTL

I have a custom view that extends LinearLayout and implements onMeasure. I'd like the children to be either as wide as they need to be or filling the available space.
XML files:
Parent:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.AtMostLinearLayout
android:id="#+id/at_most_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
Button example:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:src="#android:drawable/ic_delete" />
</FrameLayout>
Views are added programmatically, for example:
findViewById<AtMostLinearLayout>(R.id.at_most_linear_layout).apply {
repeat(4) {
LayoutInflater.from(context).inflate(R.layout.button, this)
}
}
Finally the Custom view class:
class AtMostLinearLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
init {
orientation = HORIZONTAL
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (childCount < 1) return
val newWidth = min(measuredWidth, maxTotalWidth)
var availableWidth = newWidth
var numberOfLargeChildren = 0
repeat(childCount) {
getChildAt(it).let { child ->
if (child.measuredWidth > availableWidth / childCount) {
availableWidth -= child.measuredWidth
numberOfLargeChildren++
}
}
}
val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
repeat(childCount) {
getChildAt(it).apply {
measure(
MeasureSpec.makeMeasureSpec(max(measuredWidth, minChildWidth), EXACTLY),
UNSPECIFIED
)
}
}
setMeasuredDimension(
makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
}
}
It works fine in LTR:
In RTL however the views are off set for some reason and are drawn outside the ViewGroup:
Where could this offset coming from? It looks like the children's measure calls are being added to the part, or at least half of it...
You could use the Layout Inspector (or "show layout boundaries" on device), in order to determine why it behaves as it does. The calculation of the horizontal offset may have to be flipped; by substracting instead of adding ...in order to account for the change in layout direction, where the absolute offset in pixels may always be understood as LTR.
If the canvas is rtl in the onDraw method, have you tried inverting it?
You could try using View.getLayoutDirection(). Return the layout direction.
onDraw method override and
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
if(isRtrl){
canvas.scale(-1f, 1f, width / 2, height / 2)
}
After reading through the LinearLayout & View measure and layout code some more I figured out why this it's happening.
Whenever LinearLayout#measure is called mTotalLength is calculated, which represents the calculated width of the entire view. As I'm manually remeasuring the children with a different MeasureSpec LinearLayout cannot cache these values. Later in the layout pass the views use the cached mTotalLength to set the child's left i.e. the offset. The left is based on the gravity of the child and thus being affected by the cached value.
See: LinearLayout#onlayout
final int layoutDirection = getLayoutDirection();
switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
case Gravity.RIGHT:
// mTotalLength contains the padding already
childLeft = mPaddingLeft + right - left - mTotalLength;
break;
case Gravity.CENTER_HORIZONTAL:
// mTotalLength contains the padding already
childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
break;
case Gravity.LEFT:
default:
childLeft = mPaddingLeft;
break;
}
I've change the impl to ensure it always sets the gravity to Gravity.LEFT. I should probably manually implement onLayout instead!
class AtMostLinearLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
private val maxTotalWidth = context.resources.getDimensionPixelOffset(R.dimen.max_buttons_width)
init {
orientation = HORIZONTAL
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (childCount < 1) return
val newWidth = min(measuredWidth, maxTotalWidth)
var availableWidth = newWidth
var numberOfLargeChildren = 0
repeat(childCount) {
getChildAt(it).let { child ->
if (child.measuredWidth > availableWidth / childCount) {
availableWidth -= child.measuredWidth
numberOfLargeChildren++
}
}
}
val minChildWidth = availableWidth / max(childCount - numberOfLargeChildren, 1)
repeat(childCount) {
getChildAt(it).let { child ->
child.measure(
makeMeasureSpec(max(child.measuredWidth, minChildWidth), EXACTLY),
UNSPECIFIED
)
}
}
// Effectively always set it to Gravity.LEFT to prevent LinearLayout using its
// internally-cached mTotalLength to set the Child's left.
gravity = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) Gravity.END else Gravity.START
setMeasuredDimension(
makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(measuredHeight, EXACTLY))
}
}
I don't understand why you need a custom ViewGroup for this work. How about set layout_weight when you add child view to LinearLayout.
Just simple by:
val layout = findViewById<LinearLayout>(R.id.linear_layout)
repeat(4) {
val view = LayoutInflater.from(this).inflate(R.layout.button, layout, false)
view.apply {
updateLayoutParams<LinearLayout.LayoutParams> {
width = 0
weight = 1f
}
}
layout.addView(view)
}

How to set constraintTop_toTopOf attribute from MainActivity.kt for dynamically created TextView [duplicate]

I need help with ConstraintSet. My goal is to change view's constraints in code, but I cant figure out how to do this right.
I have 4 TextViews and one ImageView. I need to set ImageView constraints to one of the TextViews.
check_answer4 = (TextView) findViewById(R.id.check_answer4);
check_answer1 = (TextView) findViewById(R.id.check_answer1);
check_answer2 = (TextView) findViewById(R.id.check_answer2);
check_answer3 = (TextView) findViewById(R.id.check_answer3);
correct_answer_icon = (ImageView) findViewById(R.id.correct_answer_icon);
If 1st answer is right, I need to set constraints of ImageView to
app:layout_constraintRight_toRightOf="#+id/check_answer1"
app:layout_constraintTop_toTopOf="#+id/check_answer1"
If 2nd answer is right, I need to set constraints of ImageView to
app:layout_constraintRight_toRightOf="#+id/check_answer2"
app:layout_constraintTop_toTopOf="#+id/check_answer2"
And so on.
To set constraints of image view to:
app:layout_constraintRight_toRightOf="#+id/check_answer1"
app:layout_constraintTop_toTopOf="#+id/check_answer1"
use:
ConstraintLayout constraintLayout = findViewById(R.id.parent_layout);
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout);
constraintSet.connect(R.id.imageView,ConstraintSet.RIGHT,R.id.check_answer1,ConstraintSet.RIGHT,0);
constraintSet.connect(R.id.imageView,ConstraintSet.TOP,R.id.check_answer1,ConstraintSet.TOP,0);
constraintSet.applyTo(constraintLayout);
To set constraints of image view to:
app:layout_constraintRight_toRightOf="#+id/check_answer2"
app:layout_constraintTop_toTopOf="#+id/check_answer2"
use:
ConstraintLayout constraintLayout = findViewById(R.id.parent_layout);
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout);
constraintSet.connect(R.id.imageView,ConstraintSet.RIGHT,R.id.check_answer2,ConstraintSet.RIGHT,0);
constraintSet.connect(R.id.imageView,ConstraintSet.TOP,R.id.check_answer2,ConstraintSet.TOP,0);
constraintSet.applyTo(constraintLayout);
Assume we want to change constraints during runtime, making button1 to be aligned with button2 when clicked:
Then, having this layout:
<android.support.constraint.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:id="#+id/root"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin">
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"
app:layout_constraintTop_toTopOf="#+id/button3"
app:layout_constraintBottom_toBottomOf="#+id/button3"
app:layout_constraintStart_toEndOf="#+id/button3"
android:layout_marginStart="0dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="0dp" />
<Button
android:id="#+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:text="Button 2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintVertical_bias="0.5" />
<Button
android:id="#+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:text="Button 3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintVertical_bias="0.223" />
</android.support.constraint.ConstraintLayout>
We can do following:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button1.setOnClickListener {
val params = button1.layoutParams as ConstraintLayout.LayoutParams
params.leftToRight = button2.id
params.topToTop = button2.id
params.bottomToBottom = button2.id
button1.requestLayout()
}
}
Another approach is to update the layout params of view like this (without requesting Layout):
yourView.updateLayoutParams<ConstraintLayout.LayoutParams> {
startToEnd = targetView.id
topToTop = targetView.id
bottomToBottom = targetView.id
//add other constraints if needed
}
In addition to azizbekian's answer, let me point out two things:
If left/right didn't work, try start/end like this:
params.startToEnd = button2.id
If you want to remove a constraint, use UNSET flag like this:
params.startToEnd = ConstraintLayout.LayoutParams.UNSET
In Kotlin you can simply extend ConstraintSet class and add some methods to take advantage of dsl in Kotlin and produce a more readable code.
Like this
class KotlinConstraintSet : ConstraintSet() {
companion object {
inline fun buildConstraintSet(block:KotlinConstraintSet.()->Unit) =
KotlinConstraintSet().apply(block)
}
//add this if you plan on using the margin param in ConstraintSet.connect
var margin: Int? = null
get() {
val result = field
margin = null //reset it to work with other constraints
return result
}
inline infix fun Unit.and(other: Int) = other // just to join two functions
inline infix fun Int.topToBottomOf(bottom: Int) =
margin?.let {
connect(this, TOP, bottom, BOTTOM, it)
} ?: connect(this, TOP, bottom, BOTTOM)
inline fun margin(margin: Int) {
this.margin = margin
}
inline infix fun Int.bottomToBottomOf(bottom: Int) =
margin?.let {
connect(this, BOTTOM, bottom, BOTTOM, it)
} ?: connect(this, BOTTOM, bottom, BOTTOM)
inline infix fun Int.topToTopOf(top: Int) =
margin?.let {
connect(this, TOP, top, TOP, it)
} ?: connect(this, TOP, top, TOP)
inline infix fun Int.startToEndOf(end: Int) =
margin?.let {
connect(this, START, end, END, it)
} ?: connect(this, START, end, END)
...
//TODO generate other functions depending on your needs
infix fun Int.clear(constraint: Constraints) =
when (constraint) {
Constraints.TOP -> clear(this, TOP)
Constraints.BOTTOM -> clear(this, BOTTOM)
Constraints.END -> clear(this, END)
Constraints.START -> clear(this, START)
}
//inline infix fun clearTopCon
inline infix fun appliesTo(constraintLayout: ConstraintLayout) =
applyTo(constraintLayout)
inline infix fun clones(constraintLayout: ConstraintLayout) =
clone(constraintLayout)
inline fun constraint(view: Int, block: Int.() -> Unit) =
view.apply(block)
}
enum class Constraints {
TOP, BOTTOM, START, END //you could add other values to use with the clear fun like LEFT
}
And use it like this
buildConstraintSet {
this clones yourConstraintLayout
constraint(R.id.view1) {
margin(value:Int) and this topToBottomOf R.id.view2
margin(30) and this bottomToBottomOf ConstraintSet.PARENT_ID
}
constraint(R.id.view2) {
this clear Constraints.BOTTOM
margin(0) and this topToTopOf R.id.topGuide
}
constraint(R.id.view4) {
this topToTopOf R.id.view2
this bottomToBottomOf R.id.view3
this startToEndOf R.id.view2
}
//or you could simply do
R.id.view1 startToEndOf R.view2
R.id.view1 toptToBottomOf R.view3
R.id.view3 bottomtToBottomOf R.view2
R.id.view3 clear Constraints.END
// and finally call applyTo()
this appliesTo yourConstraintLayout
}
I know my answer is very late, yet I'm sure It'd help others that stop by here a lot.
This article is not mine but I made a few changes, that being said, you should endeavor to check out the full article here
Constraint Sets
The key to working with constraint sets in Java code is the ConstraintSet class. This class contains a range of methods that allow tasks such as creating, configuring and applying constraints to a ConstraintLayout instance. In addition, the current constraints for a ConstraintLayout instance may be copied into a ConstraintSet object and used to apply the same constraints to other layouts (with or without modifications).
A ConstraintSet instance is created just like any other Java object:
ConstraintSet set = new ConstraintSet();
Once a constraint set has been created, methods can be called on the instance to perform a wide range of tasks.
The following code configures a constraint set in which the left-hand side of a Button view is connected to the right-hand side of an EditText view with a margin of 70dp:
set.connect(button1.getId(), ConstraintSet.LEFT,
editText1.getId(), ConstraintSet.RIGHT, 70);
Applying Constraints to a Layout
Once the constraint set is configured, it must be applied to a ConstraintLayout instance before it will take effect. A constraint set is applied via a call to the applyTo() method, passing through a reference to the layout object to which the settings are to be applied:
set.applyTo(myLayout);
There are lot more stuffs you can do with the ConstraintSet API, Setting horizontal and vertical bias, center horizontally and vertically, manipulate Chains and a lot more.
Really nice read.
Again, this is just an adaptation.
You can create constraints into ConstraintLayout with ConstraintSet or ConstraintLayout.LayoutParams properties.
I created a ConstraintSet kotlin extension for simple usage.
This is simple and robust.
Usage
applyConstraint {
centerHorizontallyParent(imageView)
centerHorizontallyParent(textView)
topToParent(imageView, px(12))
topToBottom(textView, imageView, px(4))
}
Code
fun ConstraintLayout.applyConstraint(block: ConstraintSet.() -> Unit) {
ConstraintSet().apply {
clone(this#applyConstraint)
block(this)
}.applyTo(this)
}
fun ConstraintSet.centerParent(v1: View) {
centerHorizontallyParent(v1)
centerVerticallyParent(v1)
}
fun ConstraintSet.centerHorizontallyParent(v1: View) {
centerHorizontally(v1.id, PARENT_ID)
}
fun ConstraintSet.centerVerticallyParent(v1: View) {
centerVertically(v1.id, PARENT_ID)
}
fun ConstraintSet.topToTop(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, TOP, v2.id, TOP, margin)
}
fun ConstraintSet.topToParent(v1: View, #Px margin: Int = 0) {
connect(v1.id, TOP, PARENT_ID, TOP, margin)
}
fun ConstraintSet.bottomToBottom(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, BOTTOM, v2.id, BOTTOM, margin)
}
fun ConstraintSet.bottomToParent(v1: View, #Px margin: Int = 0) {
connect(v1.id, BOTTOM, PARENT_ID, BOTTOM, margin)
}
fun ConstraintSet.topToBottom(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, TOP, v2.id, BOTTOM, margin)
}
fun ConstraintSet.bottomToTop(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, BOTTOM, v2.id, TOP, margin)
}
fun ConstraintSet.startToStart(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, START, v2.id, START, margin)
}
fun ConstraintSet.startToParent(v1: View, #Px margin: Int = 0) {
connect(v1.id, START, PARENT_ID, START, margin)
}
fun ConstraintSet.endToEnd(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, END, v2.id, END, margin)
}
fun ConstraintSet.endToParent(v1: View, #Px margin: Int = 0) {
connect(v1.id, END, PARENT_ID, END, margin)
}
fun ConstraintSet.startToEnd(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, START, v2.id, END, margin)
}
fun ConstraintSet.endToStart(v1: View, v2: View, #Px margin: Int = 0) {
connect(v1.id, END, v2.id, START, margin)
}
#vishakha yeolekar 's solution does not work for me.
To change constraints, we need to follow these steps:
clone parent layout
clear previous constraint
connect constraint
apply constraint to parent layout
Solution code (in Kotlin)
val clParent = findViewById<ConstraintLayout>(R.id.parent_layout)
val mConstraintSet = ConstraintSet()
mConstraintSet.clone(clParent)
mConstraintSet.clear(R.id.imageView, ConstraintSet.END)
mConstraintSet.connect(R.id.imageView, ConstraintSet.END, R.id.check_answer, ConstraintSet.END)
mConstraintSet.applyTo(clParent)
Here is the link for more info and methods of ConstraintSet - Click Here.
You can also use TransitionManager to animate the changes:
public static void animateConstraintLayout(ConstraintLayout constraintLayout, ConstraintSet set, long duration) {
AutoTransition trans = new AutoTransition();
trans.setDuration(duration);
trans.setInterpolator(new AccelerateDecelerateInterpolator());
TransitionManager.beginDelayedTransition(constraintLayout, trans);
set.applyTo(constraintLayout);
}
You can call it like this after updating the layout with ConstraintSet:
ConstraintSet set = new ConstraintSet();
set.clone(constraintLayout);
set.connect(R.id.example, ConstraintSet.BOTTOM, R.id.anotherExample, ConstraintSet.BOTTOM);
set.connect(R.id.example, ConstraintSet.TOP, R.id.anotherExample, ConstraintSet.TOP);
animateConstraintLayout(constraintLayout, set, 500);
For example:
app:layout_constraintRight_toRightOf = "parentView"
If you liked to declared it with programe, it will be...
ConstraintLayout.LayoutParams.rightToRight = parentView.getId()
If you used programe to declared the parentview(viewgroup), don't forget to give the parentview a generated id.
parentView.setId(View.generatedId());

How to draw colored internal grid in Recyclerview with GridLayoutManager?

I have to add internal colored grid to my RecyclerView with GridLayoutManager.
Adding space to ViewHolders is not my solution because I have rounded background in recyclerView and background in viewHolders breaks them.
Standart DividerItemDecoration gives me exactly what I need, except I can choose ONLY horizontal or vertical lines and not both.
I've found answers which gave me internal grid by spacing via getItemOffsets(), but I can't color fill outRect.
What is the best way to achieve this idea?
So I've achieved this task by simplifying standart androidx.recyclerview.widget.DividerItemDecoration by deleting all separation by HORIZONTAL and VERTICAL in code. As the result I have internal horizontal+vertical lines.
Applying decoration:
myRecyclerView.addItemDecoration(GridDividerItemDecoration(requireContext()))
The whole decoration class:
class GridDividerItemDecoration(context: Context) : ItemDecoration() {
private val mBounds = Rect()
private var mDivider: Drawable?
fun setDrawable(drawable: Drawable) {
mDivider = drawable
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null || mDivider == null) {
return
}
drawVertical(c, parent)
drawHorizontal(c, parent)
}
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(left, parent.paddingTop, right,
parent.height - parent.paddingBottom)
} else {
left = 0
right = parent.width
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.bottom + child.translationY.roundToInt()
val top = bottom - mDivider!!.intrinsicHeight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val top: Int
val bottom: Int
if (parent.clipToPadding) {
top = parent.paddingTop
bottom = parent.height - parent.paddingBottom
canvas.clipRect(parent.paddingLeft, top,
parent.width - parent.paddingRight, bottom)
} else {
top = 0
bottom = parent.height
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDivider!!.intrinsicWidth
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
if (mDivider == null) {
outRect[0, 0, 0] = 0
return
}
outRect[0, 0, 0] = mDivider!!.intrinsicHeight
outRect[0, 0, mDivider!!.intrinsicWidth] = 0
}
companion object {
private const val TAG = "DividerItem"
private val ATTRS = intArrayOf(R.attr.listDivider)
}
init {
val a = context.obtainStyledAttributes(ATTRS)
mDivider = a.getDrawable(0)
if (mDivider == null) {
Log.w(TAG, "#android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()")
}
a.recycle()
}
}

Set runtime margin to any view using Kotlin

I am a beginner in Kotlin .I am not too much familier with this language. I am making one example and playing with code. I Just want to set runtime margin to any view. I also trying to google it but not getting any proper solution for this task.
Requirement
Set runtime margin to any View.
Description
I have taking one xml file which is contain on Button and I want to set runtime margin to this button.
Code
I also try below thing but it's not work.
class MainActivity : AppCompatActivity() {
//private lateinit var btnClickMe: Button
//var btnClickMe=Button();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//For setting runtime text to any view.
btnClickMe.text = "Chirag"
//For getting runtime text to any view
var str: String = btnClickMe.text as String;
//For setting runtimer drawable
btnClickMe.background=ContextCompat.getDrawable(this,R.drawable.abc_ab_share_pack_mtrl_alpha)//this.getDrawable(R.drawable.abc_ab_share_pack_mtrl_alpha)
/*
//For Setting Runtime Margine to any view.
var param:GridLayout.LayoutParams
param.setMargins(10,10,10,10);
btnClickMe.left=10;
btnClickMe.right=10;
btnClickMe.top=10;
btnClickMe.bottom=10;
*/
// Set OnClick Listener.
btnClickMe.setOnClickListener {
Toast.makeText(this,str,5000).show();
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
tools:context="chirag.iblazing.com.stackoverflowapp.MainActivity"
android:layout_height="match_parent">
<Button
android:id="#+id/btnClickMe"
android:text="Click Me"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
How can I proceed?
You need to get the layoutParams object from button and cast it to ViewGroup.MarginLayoutParams (which is a parent class of LinearLayout.LayoutParams, RelativeLayout.LayoutParams and others and you don't have to check which is btnClickMe's actual parent) and set margins to whatever you want.
Check following code:
val param = btnClickMe.layoutParams as ViewGroup.MarginLayoutParams
param.setMargins(10,10,10,10)
btnClickMe.layoutParams = param // Tested!! - You need this line for the params to be applied.
This is how I would like to do in Kotlin -
fun View.margin(left: Float? = null, top: Float? = null, right: Float? = null, bottom: Float? = null) {
layoutParams<ViewGroup.MarginLayoutParams> {
left?.run { leftMargin = dpToPx(this) }
top?.run { topMargin = dpToPx(this) }
right?.run { rightMargin = dpToPx(this) }
bottom?.run { bottomMargin = dpToPx(this) }
}
}
inline fun <reified T : ViewGroup.LayoutParams> View.layoutParams(block: T.() -> Unit) {
if (layoutParams is T) block(layoutParams as T)
}
fun View.dpToPx(dp: Float): Int = context.dpToPx(dp)
fun Context.dpToPx(dp: Float): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
now we just have to call this on a view like
textView.margin(left = 16F)
Here's a useful Kotlin extension method:
fun View.setMargins(
left: Int = this.marginLeft,
top: Int = this.marginTop,
right: Int = this.marginRight,
bottom: Int = this.marginBottom,
) {
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(left, top, right, bottom)
}
}
Use it like this:
myView.setMargins(
top = someOtherView.height
bottom = anotherView.height
)
EDIT: the solution is similar to the answer from Hitesh, but I'm using the (original) ViewGroup.setMargins in pixels. Of course you can make your own setMarginsDp variant based on these examples, or use Hitesh's dpToPx extension before calling my implementation. Whichever solution you choose depends on your own taste.
Also take note that my solution (re)sets all margins, although this won't be an issue in most cases.
If you want to change specific margin like top or bottom you can use below code with Data binding .
#BindingAdapter("android:layout_marginTop")
#JvmStatic
fun setLayoutMarginTop(view: View, marginTop: Float) {
val layoutParams = view.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = marginTop.toInt()
view.layoutParams = layoutParams
}
and in .xml file you can write like below code
<ImageView
android:id="#+id/imageView3"
android:layout_width="#dimen/_15dp"
android:layout_height="#dimen/_15dp"
android:layout_marginTop="#{homeViewModel.getLanguage() ? #dimen/_14dp : #dimen/_32dp }"
android:contentDescription="#string/health_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/imageView1"
app:layout_constraintEnd_toStartOf="#+id/textView3"
android:src="#{ homeViewModel.remoteStatusVisible ? #drawable/green_rectangle : #drawable/gray_rectangle}"/>
Here is another sample of CardView
myCardView.elevation = 0F
myCardView.radius = 0F
val param = (myCardView.layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(0,0,0,0)
}
myCardView.layoutParams = param

Categories

Resources