How to do wrap content on half circle custom view? - android

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.

Related

Custom view will not update if run after onViewCreated()

I have built a custom view class called progressCircle which is a snapchat-like, circular progress bar - this is inside a constraint layout, over a circular button.
This view has parameter angle which when called from onViewCreated(), will work perfectly if run
progressCircle.angle = 100f
However, I am trying to animate this onClick. If I run this same code, onClick, the progressCircle will not show up?! After trial and error, I found that updating the background colour here made the view visible & it was updated. IE;
button.setOnClickListener {
progressCircle.setBackgroundColor(android.R.color.transparent)
progressCircle.angle = 270f
}
Whats going on here, and how can I fix this so I can animate it properly...
Edit:
class ProgressCircle(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint: Paint
private val rect: RectF
private val fillPaint: Paint
private val fillRect: RectF
var angle: Float
var startAngle: Float = 0f
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircle)
angle = typedArray.getFloat(R.styleable.ProgressCircle_angle, 0f)
typedArray.recycle()
val offsetAngle = 0f
val color = getColor(context, R.color.outputON)
val strokeWidth = 15f
val circleSize = 276f
paint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.STROKE)
setStrokeWidth(strokeWidth)
setColor(color)
}
rect = RectF(
strokeWidth,
strokeWidth,
(circleSize - strokeWidth),
(circleSize - strokeWidth)
)
fillPaint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.FILL)
setColor(getColor(context, R.color.flat_blue_1))
}
val offsetFill = strokeWidth
fillRect = RectF(
offsetFill,
offsetFill,
(circleSize - offsetFill),
(circleSize - offsetFill)
)
//Initial Angle (optional, it can be zero)
angle = offsetAngle
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (getColor(context, R.color.flat_blue_1) > 0) {
canvas.drawArc(rect, 0f, 360f, false, fillPaint)
}
canvas.drawArc(rect, startAngle, angle, false, paint)
}
}
TIA
By default, a View only redraws itself when something has changed - i.e., when the view is "invalidated" as per the View documentation:
Drawing is handled by walking the tree and recording the drawing commands of any View that needs to update. After this, the drawing commands of the entire tree are issued to screen, clipped to the newly damaged area.
If you want your custom view to redraw itself when the angle property is changed, you need to call invalidate():
var angle: Float
set(value) {
// Do the default behavior of setting the value
field = value
// Then call invalidate() to force a redraw
invalidate()
}
}

Custom View In a Custom View Group Does Not Drawn

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!

Dynamically calculate text size to fill the space of rectangle

I have rectf which is drawn in canvas, based on this rectf I have drawn simple bubble alike. I want my text should be inside this bubble.
Here is what I have tried.
I have a textPaint which is initialized with textSize 30, and I'm building static layout like this.
staticLayout = StaticLayout.Builder.obtain(text, start, end, textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setTextDirection(TextDirectionHeuristics.LTR)
.setLineSpacing(0, 1.0f)
.setIncludePad(false)
.setEllipsizedWidth(10)
.setEllipsize(TextUtils.TruncateAt.START)
.setMaxLines(Integer.MAX_VALUE)
.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
.setJustificationMode(Layout.JUSTIFICATION_MODE_NONE)
.build()
From this static layout I'm getting my TextLayout's height as staticLayout.height
I want my text layout height should not exceed by 70% of my bounds height, so that my text will be always inside my bounds.
float maxTextLayoutHeight = bounds.height() * 0.70f;
And my actual static layout's height
height = staticLayout.height;
From these two values I recalculate text size and applied it to my text paint with following code.
annotationTextSize = (maxTextLayoutHeight / height) * 13;
annotationTextSize = annotationTextSize * 0.80f;
if (annotationTextSize < 25) annotationTextSize = 25; //Limited to min 25
if (annotationTextSize > 99) annotationTextSize = 99; //Limited to max 99
textPaint.setTextSize(annotationTextSize);
Here is some images
With small text size:
Text size looks perfect:
Text size looks bigger:
Any improved calculation will be much helpful.
Unfortunately, I don't think that you will be able to determine the size of the laid out text until it is laid out. Internally, autosized TextViews do measurements of the text to find the best fit through a binary search that lays out the text for each candidate text size. (See AppCompatTextViewAutoSizeHelper here.
You can borrow some code from the AppCompatTextViewAutoSizeHelper or use something simpler as the following although it will not be as quick:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
var textSize = 99
while (mStaticLayout == null) {
var layout: StaticLayout
textPaint.textSize = textSize.toFloat()
layout = makeStaticLayout(mText, 0, mText.length, textPaint, width)
if (layout.height < height || textSize == 25) {
mStaticLayout = layout
} else {
textSize--
}
}
mStaticLayout?.draw(canvas)
}
You could also use the internal workings of TextView to get a StaticLayout that can be drawn to a canvas.
private fun makeAutoSizeView(
#StringRes textId: Int, width: Int, height: Int
): AppCompatTextView {
return AppCompatTextView(this).apply {
layoutParams = ConstraintLayout.LayoutParams(width, height)
TextViewCompat.setAutoSizeTextTypeWithDefaults(
this,
TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM
)
text = resources.getString(textId)
}
}
private fun getAutoSizeLayout(width: Int, height: Int): StaticLayout {
val autoSizeTextView = makeAutoSizeView(R.string.good_to_see_you, width, height)
val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
autoSizeTextView.measure(widthSpec, heightSpec)
autoSizeTextView.layout(0, 0, width, height)
return autoSizeTextView.layout as StaticLayout
}
I calculated the height of text to be rendered using the following method. Might be helpful
private fun getMultilineTextHeight(text: CharSequence, textPaint: TextPaint, width: Int): Int {
val alignment = Layout.Alignment.ALIGN_NORMAL
val spacingMultiplier = 1f
val spacingAddition = 0f
val includePadding = false
val staticLayout =
StaticLayout(text, textPaint, width, alignment, spacingMultiplier, spacingAddition, includePadding)
return staticLayout.height
}

How to fit content of GIF animation in View and in live wallpaper?

Background
I have a small live wallpaper app, that I want to add support for it to show GIF animations.
For this, I've found various solutions. There is the solution of showing a GIF animation in a view (here), and there is even a solution for showing it in a live wallpaper (here).
However, for both of them, I can't find how to fit the content of the GIF animation nicely in the space it has, meaning any of the following:
center-crop - fits to 100% of the container (the screen in this case), cropping on sides (top&bottom or left&right) when needed. Doesn't stretch anything. This means the content seems fine, but not all of it might be shown.
fit-center - stretch to fit width/height
center-inside - set as original size, centered, and stretch to fit width/height only if too large.
The problem
None of those is actually about ImageView, so I can't just use the scaleType attribute.
What I've found
There is a solution that gives you a GifDrawable (here), which you can use in ImageView, but it seems it's pretty slow in some cases, and I can't figure out how to use it in LiveWallpaper and then fit it.
The main code of the LiveWallpaper GIF handling is as such (here) :
class GIFWallpaperService : WallpaperService() {
override fun onCreateEngine(): WallpaperService.Engine {
val movie = Movie.decodeStream(resources.openRawResource(R.raw.cinemagraphs))
return GIFWallpaperEngine(movie)
}
private inner class GIFWallpaperEngine(private val movie: Movie) : WallpaperService.Engine() {
private val frameDuration = 20
private var holder: SurfaceHolder? = null
private var visible: Boolean = false
private val handler: Handler = Handler()
private val drawGIF = Runnable { draw() }
private fun draw() {
if (visible) {
val canvas = holder!!.lockCanvas()
canvas.save()
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
}
override fun onVisibilityChanged(visible: Boolean) {
this.visible = visible
if (visible)
handler.post(drawGIF)
else
handler.removeCallbacks(drawGIF)
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(drawGIF)
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
}
}
}
The main code for handling GIF animation in a view is as such:
class CustomGifView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var gifMovie: Movie? = null
var movieWidth: Int = 0
var movieHeight: Int = 0
var movieDuration: Long = 0
var mMovieStart: Long = 0
init {
isFocusable = true
val gifInputStream = context.resources.openRawResource(R.raw.test)
gifMovie = Movie.decodeStream(gifInputStream)
movieWidth = gifMovie!!.width()
movieHeight = gifMovie!!.height()
movieDuration = gifMovie!!.duration().toLong()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(movieWidth, movieHeight)
}
override fun onDraw(canvas: Canvas) {
val now = android.os.SystemClock.uptimeMillis()
if (mMovieStart == 0L) { // first time
mMovieStart = now
}
if (gifMovie != null) {
var dur = gifMovie!!.duration()
if (dur == 0) {
dur = 1000
}
val relTime = ((now - mMovieStart) % dur).toInt()
gifMovie!!.setTime(relTime)
gifMovie!!.draw(canvas, 0f, 0f)
invalidate()
}
}
}
The questions
Given a GIF animation, how can I scale it in each of the above ways?
Is it possible to have a single solution for both cases?
Is it possible to use GifDrawable library (or any other drawable for the matter) for the live wallpaper, instead of the Movie class? If so, how?
EDIT: after finding how to scale for 2 kinds, I still need to know how to scale according to the third type, and also want to know why it keeps crashing after orientation changes, and why it doesn't always show the preview right away.
I'd also like to know what's the best way to show the GIF animation here, because currently I just refresh the canvas ~60fps (1000/60 waiting between each 2 frames), without consideration of what's in the file.
Project is available here.
If you have Glide in your project, You can easily load Gifs, as it provides drawing GIFs to your ImageViews and does support many scaling options (like center or a given width and ...).
Glide.with(context)
.load(imageUrl or resourceId)
.asGif()
.fitCenter() //or other scaling options as you like
.into(imageView);
OK I think I got how to scale the content. Not sure though why the app still crashes upon orientation change sometimes, and why the app doesn't show the preview right away sometimes.
Project is available here.
For center-inside, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center-inside
val scale = Math.min(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
For center-crop, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center crop
val scale = Math.max(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
for fit-center, I can use this:
val canvasWidth = canvas.width.toFloat()
val canvasHeight = canvas.height.toFloat()
val bitmapWidth = curBitmap.width.toFloat()
val bitmapHeight = curBitmap.height.toFloat()
val scaleX = canvasWidth / bitmapWidth
val scaleY = canvasHeight / bitmapHeight
scale = if (scaleX * curBitmap.height > canvas.height) scaleY else scaleX
x = (canvasWidth / 2f) - (bitmapWidth / 2f) * scale
y = (canvasHeight / 2f) - (bitmapHeight / 2f) * scale
...
Change the width and the height of the movie:
Add this code in onDraw method before movie.draw
canvas.scale((float)this.getWidth() / (float)movie.width(),(float)this.getHeight() / (float)movie.height());
or
canvas.scale(1.9f, 1.21f); //this changes according to screen size
Scale to fill and scale to fit:
There's already a good answer on that:
https://stackoverflow.com/a/38898699/5675325

How to draw sine wave curve using sin() function and canvas in android?

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"/>

Categories

Resources