Issue
How to create a selector programmatically (Kotlin) for an ImageButton with the following requirements.
- Only the background scales by 25% while the image remains the same size while the button is at the
Pressed
state. - The background colour of the button at the
Pressed
state is different from that of the normal (default) state. - able to set the background colour programmatically for the button so that the same button can be reused.
Thank you for all the help in advance
Solution
You will need to recreate the following drawable programmatically:
<selector>
<item android:drawable="@android:color/holo_blue_light" android:state_pressed="true" />
<item>
<inset android:drawable="@android:color/holo_red_light" android:inset="12.5%" />
</item>
</selector>
This StateListDrawable uses an InsetDrawable to set the background color to something smaller than the view width and height.
In the following layout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray">
<View
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@android:color/holo_green_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/imageButton"
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@drawable/selector_drawable"
android:src="@drawable/ic_launcher_foreground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
the background behaves as follows:
The green square is there just to show the true extent of the ImageButton.
Here is the code to create the StateListDrawable programmatically and to assign it to the ImageButton:
val blue = ContextCompat.getColor(requireContext(), android.R.color.holo_blue_light)
val red = ContextCompat.getColor(requireContext(), android.R.color.holo_red_light)
// Create the inset drawable that is inset 12.5% on each side. This will be the default state.
val colorDrawable = ColorDrawable(red)
val insetDrawable = InsetDrawable(colorDrawable, 0.12f)
// Create the drawable that will be the pressed state background.
val pressedStateDrawable = ColorDrawable(blue)
val bg = StateListDrawable()
bg.addState(intArrayOf(android.R.attr.state_pressed), pressedStateDrawable)
bg.addState(intArrayOf(), insetDrawable)
binding.imageButton.background = bg
If you want to animate the background, you can use a ScaleDrawable with a ValueAnimator. Here is some sample code:
val baseLevel = 7500
val topLevel = 10000
val animationDuration = 250L
val redColor = ContextCompat.getColor(requireContext(), android.R.color.holo_red_light)
val colorDrawable = ColorDrawable(redColor)
val blueColor = ContextCompat.getColor(requireContext(), android.R.color.holo_blue_light)
val scaleDrawable = ScaleDrawable(colorDrawable, Gravity.CENTER, 1f, 1f)
scaleDrawable.level = baseLevel
binding.imageButton.background = scaleDrawable
binding.imageButton.setOnClickListener {
ValueAnimator.ofInt(baseLevel, topLevel).apply {
duration = animationDuration
doOnStart {
(scaleDrawable.drawable as ColorDrawable).color = blueColor
scaleDrawable.level = baseLevel
}
addUpdateListener {
scaleDrawable.level = it.animatedValue as Int
binding.imageButton.invalidate()
}
doOnEnd {
(scaleDrawable.drawable as ColorDrawable).color = redColor
scaleDrawable.level = baseLevel
}
start()
}
}
A still better way (IMO) is to encapsulate the expanding drawable as a custom drawable.
ExpandingBackgroundDrawable.kt
class ExpandingBackgroundDrawable(
matchStates: IntArray,
activeColor: Int,
insetPercentage: Float,
animationDuration: Long
) : Drawable(), Animatable, TimeAnimator.TimeListener {
private val mMatchStates = matchStates
private val mInsetPercentage = insetPercentage
private val mAnimDuration = animationDuration
private var mRunning = false
private var mStartTime = 0L
private val mTimeAnimator: TimeAnimator = TimeAnimator().also {
it.setTimeListener(this)
}
private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = activeColor
style = Paint.Style.FILL
}
private val mRectMin = RectF()
private val mRectCurrent = RectF()
// Notify this Drawable when the View's state changes.
override fun isStateful() = true
override fun onStateChange(states: IntArray): Boolean {
return if (StateSet.stateSetMatches(mMatchStates, states)) {
start()
true
} else {
false
}
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
mRectMin.set(
0f, 0f, bounds.right - mInsetPercentage * bounds.right * 2,
bounds.bottom - mInsetPercentage * bounds.bottom * 2
)
}
override fun draw(canvas: Canvas) {
canvas.withTranslation(
(bounds.width() - mRectCurrent.right) / 2,
(bounds.height() - mRectCurrent.bottom) / 2
) {
canvas.drawRect(mRectCurrent, mPaint)
}
}
override fun setAlpha(alpha: Int) {
mPaint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.colorFilter = colorFilter
}
override fun getOpacity() = PixelFormat.TRANSLUCENT
override fun start() {
if (isRunning) return
mRunning = true
mStartTime = SystemClock.uptimeMillis()
invalidateSelf()
mTimeAnimator.duration = mAnimDuration
mTimeAnimator.start()
}
override fun stop() {
if (!isRunning) return
mTimeAnimator.cancel()
mRunning = false
invalidateSelf()
}
override fun isRunning() = mRunning
override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) {
val progress = totalTime.toFloat() / animation.duration
mRectCurrent.right = mRectMin.right + (bounds.right - mRectMin.right) * progress
mRectCurrent.bottom = mRectMin.bottom + (bounds.bottom - mRectMin.bottom) * progress
if (progress >= 1F) {
stop()
} else {
invalidateSelf()
}
}
fun setColor(color: Int) {
mPaint.color = color
invalidateSelf()
}
}
The custom drawable would be created as follows:
imageButton.background = getBackground(context)
private fun getBackground(view: View): Drawable {
val context = view.context
// Create the inset drawable that is inset 12.5% on each side.
// This will be the default state.
val red = ContextCompat.getColor(context, android.R.color.holo_red_light)
val colorDrawable = ColorDrawable(red)
val insetDrawable = InsetDrawable(colorDrawable, 0.125f)
// Get the expanding background drawable that is the pressed state drawable.
val expandingStates = intArrayOf(
android.R.attr.state_pressed,
android.R.attr.state_accelerated
)
val mExpandingBackground = ExpandingBackgroundDrawable(
expandingStates,
ContextCompat.getColor(context, R.color.accent),
0.125f,
context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
)
return StateListDrawable().apply {
addState(expandingStates, mExpandingBackground)
addState(StateSet.WILD_CARD, insetDrawable)
}
}
While the state of the ImageButton is "pressed", the background will start as the size of the InsetDrawable and expand to the size of the outer square. When the "pressed" state is removed, the drawable will disappear.
Answered By - Cheticamp
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.