I want to draw a progress animation in a button from left to right somewhat like this:
Here the button nothing but a custom view. So the canvas should draw this and I have to extend view class only. I am not sure what should I use here I tried using canvas.drawPath using a value animator but no success.
I am not sure which canvas method should be used here and how I can animate it from left to right.
Can anyone help me here, please?
To start with the custom view, keep in mind of the following points
As it dictates it's a progress indicator view with text over it. So
choose TextView to extend the functionality.
The background animates here, that means our progress draw part has
to be done before original textview draw cycle.
It's wise to keep the progress update outside the custom view.
With this in mind the custom view will look as below
class DownloadButton : androidx.appcompat.widget.AppCompatTextView {
/// constructor
private val bgPaint: Paint = Paint().apply {
color = 0xff216353.toInt()
}
private val progressPaint: Paint = Paint().apply {
color = 0xff75daad.toInt()
}
var progress: Float = 0f
override fun onDraw(canvas: Canvas?) {
// Draw before original content drawn
// Compute the dx based on the progress
// and draw 2 rects
canvas?.let {
val dx = it.width * progress
it.drawRect(RectF(0f, 0f, dx, it.height * 1f), bgPaint)
it.drawRect(RectF(dx, 0f, it.width * 1f, it.height * 1f), progressPaint)
}
super.onDraw(canvas)
}
fun updateProgress(progress: Float) {
this.progress = progress
val percent = (progress * 100).toInt()
text = "Progress $percent%"
invalidate()
}
}
Use it in xml as any TextView used
<com.example.custom.DownloadButton
android:id="#+id/downloadButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="16dp"
android:text="Download"
android:textColor="#fff"
android:textSize="20sp"
android:textStyle="bold"
android:layout_margin="16dp"
android:typeface="monospace" />
Anywhere in the code, call downloadButton.updateProgress() to redraw the progress.
Note this is a bare minimum implementation where we haven't done computation optimization for edge cases (0 - 100) progress where drawing one rect would be suffice
This worked for me , hope it also help you out !
LoadingButton.kt
import android.animation.AnimatorInflater
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import kotlin.properties.Delegates
class LoadingButton #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var bgColor: Int = Color.BLACK
private var textColor: Int = Color.BLACK // default color
private var widthSize = 0
private var heightSize = 0
#Volatile
private var progress: Double = 0.0
private var valueAnimator: ValueAnimator
private var buttonState: ButtonState by Delegates.observable(ButtonState.Completed) {
p, old, new ->
}
private val updateListener = ValueAnimator.AnimatorUpdateListener {
progress = (it.animatedValue as Float).toDouble()
invalidate()
requestLayout()
}
fun hasCompletedDownload() {
// cancel the animation when file is downloaded
valueAnimator.cancel()
buttonState = ButtonState.Completed
invalidate()
requestLayout()
}
init {
isClickable = true
valueAnimator = AnimatorInflater.loadAnimator(
context, R.animator.loading_animation
) as ValueAnimator
valueAnimator.addUpdateListener(updateListener)
val attr = context.theme.obtainStyledAttributes(
attrs,
R.styleable.LoadingButton,
0,
0
)
try {
bgColor = attr.getColor(
R.styleable.LoadingButton_bgColor,
ContextCompat.getColor(context, R.color.colorPrimary)
)
textColor = attr.getColor(
R.styleable.LoadingButton_textColor,
ContextCompat.getColor(context, R.color.colorPrimary)
)
} finally {
attr.recycle()
}
}
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create("", Typeface.BOLD)
}
override fun performClick(): Boolean {
super.performClick()
if (buttonState == ButtonState.Completed) buttonState = ButtonState.Loading
animation()
return true
}
private fun animation() {
valueAnimator.start()
}
private val rect = RectF(
740f,
50f,
810f,
110f
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.strokeWidth = 0f
paint.color = bgColor
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
if (buttonState == ButtonState.Loading) {
paint.color = Color.parseColor("#004349")
canvas.drawRect(
0f, 0f,
(width * (progress / 100)).toFloat(), height.toFloat(), paint
)
paint.color = Color.parseColor("#F9A825")
canvas.drawArc(rect, 0f, (360 * (progress / 100)).toFloat(), true, paint)
}
val buttonText =
if (buttonState == ButtonState.Loading)
resources.getString(R.string.loading)
else resources.getString(R.string.download)
paint.color = textColor
canvas.drawText(buttonText, (width / 2).toFloat(), ((height + 30) / 2).toFloat(),
paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
val w: Int = resolveSizeAndState(minw, widthMeasureSpec, 1)
val h: Int = resolveSizeAndState(
MeasureSpec.getSize(w),
heightMeasureSpec,
0
)
widthSize = w
heightSize = h
setMeasuredDimension(w, h)
}
}
call hasCompletedDownload() function when file got downloaded in order to stop the animation.
Add a new file (say attrs.xml) in "values" folder under "res" folder to add attributes for custom button.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoadingButton">
<attr name="bgColor" format="integer" />
<attr name="textColor" format="integer" />
</declare-styleable>
</resources>
Add this in your layout
<com.android.example.LoadingButton
android:id="#+id/custom_button"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_margin="20dp"
app:textColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Add another folder (say animation) under "res" folder then add a file "loading_animation.xml" in order to add animation attributes for the custom button.
<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
android:valueFrom="0f"
android:valueTo="100f"
android:valueType="floatType" />
Thank You :)
Related
I want it to function like this: when the screen is opened then the dial points off. The user clicks it, the dial turns to ON and then remains there. When I click it again, the reverse happens. It is unclear to me why it snaps back to another value after the animation is done. I tried tweaking the angle but I had no luck. Thanks in advance!
OFF(R.string.fan_off),
ON(R.string.fan_on);
fun next() = when (this) {
OFF -> ON
ON -> OFF
}
}
private const val RADIUS_OFFSET_LABEL = 75
private const val RADIUS_OFFSET_INDICATOR = -85
class KnobView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0.0f // Radius of the circle.
private var socketOnOffSwitch = SocketMainSwich.OFF // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
// MAIN IMPORTANCE
// This one is actually used for the nice knob and cannot be used with the small black button
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
setBackground(ContextCompat.getDrawable(context, R.drawable.socket_transp2))
}
// used for text and for the black circle
private val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create("", Typeface.NORMAL)
}
init {
isClickable = true
}
override fun performClick(): Boolean {
if (super.performClick()) return true
// socketOnOffSwitch = socketOnOffSwitch.next()
// contentDescription = resources.getString(socketOnOffSwitch.label)
invalidate()
return true
}
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
private fun PointF.computeXYForSpeed(pos: SocketMainSwich, radius: Float) {
// Angles are in radians. // tweak the angle by adding 0.5 at start angle (9 + 0.5) and subtractig the same from angle
val startAngle = Math.PI * (9.5 / 8.0)
val angle = startAngle + pos.ordinal * (2.5 * Math.PI/ 4)
x = (radius * cos(angle)).toFloat() + width / 2
y = (radius * sin(angle)).toFloat() + height / 2
}
private val paint3 = Paint()
// this angle in correlation with the canvas.rotate method in onDraw realizes the first image of the dial
private var angle = 0f
private var rotateAnimation: RotateAnimation? = null
init {
// Set the paint color and stroke width
paint3.color = resources.getColor(androidx.appcompat.R.color.material_blue_grey_800)
paint3.strokeWidth = 4f
// Create a rotation animation
setOnClickListener {
clearAnimation()
if(angle == -60f) {
rotateAnimation = RotateAnimation(-60f, 60f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation?.duration = 3000
rotateAnimation?.interpolator = AccelerateDecelerateInterpolator()
startAnimation(rotateAnimation)
angle = 60f
} else {
rotateAnimation = RotateAnimation(60f, -60f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation?.duration = 3000
rotateAnimation?.interpolator = AccelerateDecelerateInterpolator()
startAnimation(rotateAnimation)
angle = -60f
}
}
// as above we need to switch to the next value
socketOnOffSwitch = socketOnOffSwitch.next()
contentDescription = resources.getString(socketOnOffSwitch.label)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// maybe we can get a rotation
// // Save the current canvas state
// canvas.save()
canvas.rotate(angle, width / 2f, height / 2f)
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR - 15
pointPosition.computeXYForSpeed(socketOnOffSwitch, markerRadius)
paint2.setColor(resources.getColor(R.color.cream))
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/10, paint2)
}```
I have a custom view named FloorView. I want this Floor view to be occupying the whole screen, and I need this dynamically (maybe match_parent). I don't want to hard code the size of the view.
I've tried doing this in FloorView.kt
private val rectangle = RectF(
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(),
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(), 0f, 0f)
I was thinking I could use the match_parent attribute, and match the ConstraintLayout of my MainActivity, but that didn't work, nothing was drawn, possibly because the ConstraintLayout doesn't exist yet
My code for the FloorView.kt is this
class FloorView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
color = Color.BLACK
}
private val rectangle = RectF(
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(),
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(), 0f, 0f)
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas!!.drawRect(rectangle, paint)
}
}
How can I make a custom view occupy all available screen space? (like match_parent)
You can do something like:
class FloorView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val displayMetrics by lazy { Resources.getSystem().displayMetrics }
private val deviceWidth by lazy { (displayMetrics.widthPixels).toFloat() }
private val deviceHeight by lazy { (displayMetrics.heightPixels).toFloat() }
// you can also use this.width & this.Height if this view already
// added in a ViewGroup via XML
private val rectangle by lazy { RectF(0f, 0f, deviceWidth, deviceHeight) }
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
color = Color.BLACK
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas!!.drawRect(rectangle, paint)
}
}
Hi guys meniman98 doesn't want a view of "screen size", just want the view to match parent view's size. Actually "screen size" is impossible -- a view can not exceed its parent ViewGroup's size.
The question is in
private val rectangle = RectF(
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(),
ViewGroup.LayoutParams.MATCH_PARENT.toFloat(), 0f, 0f)
this will not get expected size because MTACH_PARENT is just a const int to instruct the layout planner, not size.
View's size is first decided in view's onSizeChanged call, so you should override this function and create the rectangle there.
private var rectangle:RectF? = null
override fun onSizeChanged(int w, int h, int oldW, int oldH) {
rectangle = RectF(0f, 0f, w.toFloat(), h.toFloat())
}
i am beginner level android developer and i know basics of layouts and views.Now i want to go one step ahead and want to learn interactive designs like this image
https://ufile.io/uas4ljkr
In this image we have red area and white area and both are separated with a wave like shape not simple shape which we see in all layouts.Any help regarding this will be appreciated.Thanks
A good way of accomplishing this is to just to create your own custom view and override the onDraw() method.
https://developer.android.com/training/custom-views/custom-drawing
I did simple quick and dirty custom drawing for you that is similar to example you gave
MainActivity
class MainActivity : AppCompatActivity() {
private var waveView: WaveView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wave = WaveView(this)
waveView = wave
setContentView(wave)
}
}
WaveView
class WaveView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint()
private val path = Path()
init {
paint.style = Paint.Style.FILL
paint.isAntiAlias = true
paint.isDither = true
}
override fun onDraw(canvas: Canvas?) {
val point1X = width * 0.0f
val point1Y = height * 0.5f
val point2X = width * 0.35f
val point2Y = height * 0.45f
val point3X = width * 0.5f
val point3Y = height * 0.3f
val point4X = width * 0.75f
val point4Y = height * 0.1f
val point5Y = height * 0.25f
paint.color = Color.RED
canvas?.drawColor(Color.WHITE)
path.moveTo(0f, 0f)
path.lineTo(point1X, point1Y)
path.quadTo(point2X, point2Y, point3X, point3Y)
path.lineTo(width.toFloat(), point3Y)
path.lineTo(width.toFloat(), 0f)
canvas?.drawPath(path, paint)
path.reset()
paint.color = Color.WHITE
path.moveTo(point3X, height.toFloat())
path.lineTo(point3X, point3Y)
path.quadTo(point4X, point4Y, width.toFloat(), point5Y)
path.lineTo(width.toFloat(), height.toFloat())
canvas?.drawPath(path, paint)
}
}
custom wave
Hopefully this points you in the right direction.
I have developed a custom progress view using android canvas. Currently I am facing a issue where if the progress value below 3 it does not round the edge as in the image.5% to 100% all good. Following is my code. I have attached the image too hope can someone can help.
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import android.view.View
class DevProgressIndicator #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
private var color: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var bgColor: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
var percentage = 0f
set(value) {
if (value in 0f..100f) field = value
else throw IllegalArgumentException("Value should be more than 0 and less or equal 100")
}
init {
color.apply {
color = Color.RED
style = Paint.Style.FILL
}
bgColor.apply {
color = ContextCompat.getColor(context, R.color.material_grey_100)
style = Paint.Style.FILL
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
val centerY = (height / 2).toFloat()
val radius = centerY
val leftCircleX = radius
val rightCircleX = width - radius
canvas.drawCircle(leftCircleX, centerY, radius, bgColor)
canvas.drawCircle(rightCircleX, centerY, radius, bgColor)
canvas.drawRect(leftCircleX, 0f, rightCircleX, height.toFloat(), bgColor)
if (percentage == 0f) {
return
}
val leftIndicatorX = width - (width * percentage) / 100
canvas.drawCircle(rightCircleX, centerY, radius, color)
canvas.drawCircle(leftIndicatorX + radius, centerY, radius, color)
canvas.drawRect(leftIndicatorX + radius, 0f, rightCircleX, height.toFloat(), color)
}
fun setColor(colorId: Int) {
color.color = ContextCompat.getColor(context, colorId)
invalidate()
requestLayout()
}
}
If percentage = 0 then leftIndicatorX must be equal to rightCircleX - radius (so that left and right circles coincide). Try to replace
val leftIndicatorX = width - (width * percentage) / 100
with
val leftIndicatorX = rightCircleX - radius - (rightCircleX - leftCircleX) * percentage / 100
Can anyone help me out on how I going to draw semicircle of the picture below with canvas and also how to detect the collection of the drawing object?
I had try to draw it using XML and i dont know how to detect the collision of it. I just want to detect the collision of the black part but not the whole circle.
Thank you.
I've just done something similar, with a custom View:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
init {
paint.isAntiAlias = true
paint.color = ContextCompat.getColor(context, R.color.colorAquamarine)
paint.style = Paint.Style.STROKE
paint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, 90f, 180f, false, paint)
}
}
Add it you your XML layout like this:
<com.example.view.SemiCircleView
android:layout_width="120dp"
android:layout_height="120dp"/>
That's the result:
In my case I want to programmatically control the % of the circle being drawn, so I've added a method setArcProportion that controls that:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mainPaint: Paint = Paint()
private val backgroundPaint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
private var arcProportion: Float = 0f
init {
mainPaint.isAntiAlias = true
mainPaint.color = ContextCompat.getColor(context, R.color.colorLutea)
mainPaint.style = Paint.Style.STROKE
mainPaint.strokeWidth = 5.dpToPx()
backgroundPaint.isAntiAlias = true
backgroundPaint.color = ContextCompat.getColor(context, R.color.black_08)
backgroundPaint.style = Paint.Style.STROKE
backgroundPaint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, -90f, arcProportion * 360, false, mainPaint)
// This 2nd arc completes the circle. Remove it if you don't want it
canvas.drawArc(rectangle!!, -90f + arcProportion * 360, (1 - arcProportion) * 360, false, backgroundPaint)
}
/**
* #param arcProportion The proportion of the semi circle arc, from 0 to 1. Setting 0 makes the arc invisible, and 1
* makes a whole circle.
*/
fun setArcProportion(arcProportion: Float) {
this.arcProportion = arcProportion
invalidate()
}
}
So if I do semiCircleView.setArcProportion(0.62f) I have:
Bonus - To make the arc grow with an animation modify setArcProportion like this:
private const val ANIMATION_BASE_DURATION_MS: Long = 500 // milliseconds
fun setArcProportion(arcProportion: Float) {
ValueAnimator.ofFloat(0f, arcProportion).apply {
interpolator = DecelerateInterpolator()
// The animation duration is longer for a larger arc
duration = ANIMATION_BASE_DURATION_MS + (arcProportion * ANIMATION_BASE_DURATION_MS).toLong()
addUpdateListener { animator ->
this#SemiCircleView.arcProportion = animator.animatedValue as Float
this#SemiCircleView.invalidate()
}
start()
}
}
In your onDraw method draw a circle .
canvas.drawCircle(x, y, 10, paint);
Now draw a rect with your background colour
Paint fillBackgroundPaint = new Paint();
fillBackgroundPaint.setAntiAlias(true);
fillBackgroundPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(x,y+10,x+10,y-10);
This should serve the purpose.