Last active
February 3, 2022 09:21
-
-
Save sofakingforever/24507173b7743784303ea1bbf8e9e6bb to your computer and use it in GitHub Desktop.
Draw animated stars on Android view canvas - written in Kotlin - crafted with ❤️ by sofakingforever
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.sofaking.moonworshipper.view | |
import android.content.Context | |
import android.graphics.Canvas | |
import android.graphics.Paint | |
import android.graphics.RectF | |
import android.util.AttributeSet | |
import android.view.View | |
import java.util.* | |
import java.util.concurrent.Executors | |
import kotlin.concurrent.timerTask | |
/** | |
* Kotlin Android view that draws animated stars on a canvas | |
* | |
* Used in Wakey - Beautiful Alarm Clock for Android: http://bit.ly/2uI8pgL | |
* Check out the article on Medium: http://bit.ly/2NlFJBW | |
* Or see what it looks like on YouTube: https://www.youtube.com/watch?v=v1-228CkoQc | |
* | |
* Don't forget to call the view's onStart() and onStop() from their respective Activity's lifecycle methods. | |
* | |
* Crafted with ❤️ by sofakingforever | |
*/ | |
class AnimatedStarsView | |
@kotlin.jvm.JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { | |
private val fps: Long = 1000 / 60 | |
private val defaultStarCount: Int = 25 | |
private val threadExecutor = Executors.newSingleThreadExecutor() | |
private var starCount: Int | |
private var starColors: IntArray | |
private var bigStarThreshold: Int | |
private var minStarSize: Int | |
private var maxStarSize: Int | |
private var starsCalculatedFlag: Boolean = false | |
private var viewWidth: Int = 0 | |
private var viewHeight: Int = 0 | |
private var stars: ArrayList<Star> = ArrayList() | |
private var starConstraints: StarConstraints | |
private lateinit var timer: Timer | |
private lateinit var task: TimerTask | |
private val random: Random = Random() | |
private var initiated: Boolean = false | |
/** | |
* init view's attributes | |
*/ | |
init { | |
val array = context.obtainStyledAttributes(attrs, R.styleable.AnimatedStarsView, defStyleAttr, 0) | |
starColors = intArrayOf() | |
starCount = array.getInt(R.styleable.AnimatedStarsView_starsView_starCount, defaultStarCount) | |
minStarSize = array.getDimensionPixelSize(R.styleable.AnimatedStarsView_starsView_minStarSize, 4) | |
maxStarSize = array.getDimensionPixelSize(R.styleable.AnimatedStarsView_starsView_maxStarSize, 24) | |
bigStarThreshold = array.getDimensionPixelSize(R.styleable.AnimatedStarsView_starsView_bigStarThreshold, Integer.MAX_VALUE) | |
starConstraints = StarConstraints(minStarSize, maxStarSize, bigStarThreshold) | |
val starColorsArrayId = array.getResourceId(R.styleable.AnimatedStarsView_starsView_starColors, 0) | |
if (starColorsArrayId != 0) { | |
starColors = context.resources.getIntArray(starColorsArrayId) | |
} | |
array.recycle() | |
} | |
/** | |
* Must call this in Activity's onStart | |
*/ | |
fun onStart() { | |
timer = Timer() | |
task = timerTask { | |
invalidateStars() | |
} | |
timer.scheduleAtFixedRate(task, 0, fps) | |
} | |
/** | |
* Must call this in Activity's onStop | |
*/ | |
fun onStop() { | |
task.cancel() | |
timer.cancel() | |
} | |
/** | |
* get view's size and init stars every time the size of the view has changed | |
*/ | |
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { | |
super.onSizeChanged(w, h, oldw, oldh) | |
viewWidth = w | |
viewHeight = h | |
if (viewWidth > 0 && viewHeight > 0) { | |
// init stars every time the size of the view has changed | |
initStars() | |
} | |
} | |
/** | |
* Draw stars on view's canvas | |
*/ | |
override fun onDraw(canvas: Canvas?) { | |
// create a variable canvas object | |
var newCanvas = canvas | |
// draw each star on the canvas | |
stars.forEach { newCanvas = it.draw(newCanvas) } | |
// reset flag | |
starsCalculatedFlag = false | |
// finish drawing view | |
super.onDraw(newCanvas) | |
} | |
/** | |
* create x stars with a random point location and opacity | |
*/ | |
private fun initStars() { | |
// map stars instead of adding via loop - courtesy of Dominik Mičuta & Arek Olek | |
stars = List(starCount) { | |
Star( | |
starConstraints, | |
Math.round(Math.random() * viewWidth).toInt(), | |
Math.round(Math.random() * viewHeight).toInt(), | |
Math.random(), | |
starColors[it % starColors.size], | |
viewWidth, | |
viewHeight, | |
{ starColors[random.nextInt(starColors.size)] } | |
) | |
} | |
initiated = true | |
} | |
/** | |
* calculate and invalidate all stars for the next frame | |
*/ | |
private fun invalidateStars() { | |
if (!initiated){ | |
return | |
} | |
// new background thread | |
threadExecutor.execute({ | |
// recalculate stars position and alpha on a background thread | |
stars.forEach { it.calculateFrame(viewWidth, viewHeight) } | |
starsCalculatedFlag = true | |
// then post to ui thread | |
postInvalidate() | |
}) | |
} | |
} | |
/** | |
* Single star in sky view | |
*/ | |
private class Star(val starConstraints: StarConstraints, var x: Int, var y: Int, var opacity: Double, var color: Int, viewWidth: Int, viewHeight: Int, val colorListener: () -> Int) { | |
var alpha: Int = 0 | |
var factor: Int = 1 | |
var increment: Double | |
val length: Double = (starConstraints.minStarSize + Math.random() * (starConstraints.maxStarSize - starConstraints.minStarSize)) | |
val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG) | |
val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) | |
private var shape: StarShape | |
private lateinit var hRect: RectF | |
private lateinit var vRect: RectF | |
/** | |
* init paint, shape and some parameters | |
*/ | |
init { | |
// init fill paint for small and big stars | |
fillPaint.color = color | |
// init stroke paint for the circle stars | |
strokePaint.color = color | |
strokePaint.style = Paint.Style.STROKE | |
strokePaint.strokeWidth = length.toFloat() / 4f | |
// init shape of star according to random size | |
shape = if (length >= starConstraints.bigStarThreshold) { | |
// big star ones will randomly be Star or Circle | |
if (Math.random() < < 0.7) { | |
StarShape.Star | |
} else { | |
StarShape.Circle | |
} | |
} else { | |
// small ones will be dots | |
StarShape.Dot | |
} | |
// the alpha incerment speed will be decided according to the star's size | |
increment = when (shape) { | |
StarShape.Circle -> { | |
Math.random() * .025 | |
} | |
StarShape.Star -> { | |
Math.random() * .030 | |
} | |
StarShape.Dot -> { | |
Math.random() * .045 | |
} | |
} | |
initLocationAndRectangles(viewWidth, viewHeight) | |
} | |
/** | |
* calculate single frame for star (factor, opacity, and location if needed) | |
*/ | |
fun calculateFrame(viewWidth: Int, viewHeight: Int) { | |
// calculate direction / factor of opacity | |
if (opacity >= 1 || opacity <= 0) { | |
factor *= -1 | |
} | |
// calculate new opacity for star | |
opacity += increment * factor | |
// convert to int-based alpha | |
alpha = (opacity * 255.0).toInt() | |
when { | |
alpha > 255 -> { | |
// reset alpha to full | |
alpha = 255 | |
} | |
alpha <= 0 -> { | |
// reset alpha to 0 | |
alpha = 0 | |
// and relocate star | |
initLocationAndRectangles(viewWidth, viewHeight) | |
color = colorListener.invoke() | |
// init fill paint for small and big stars | |
fillPaint.color = color | |
// init stroke paint for the circle stars | |
strokePaint.color = color | |
strokePaint.style = Paint.Style.STROKE | |
strokePaint.strokeWidth = length.toFloat() / 4f | |
} | |
} | |
} | |
/** | |
* init star's position and rectangles if needed | |
*/ | |
private fun initLocationAndRectangles(viewWidth: Int, viewHeight: Int) { | |
// randomize location | |
x = Math.round(Math.random() * viewWidth).toInt() | |
y = Math.round(Math.random() * viewHeight).toInt() | |
// calculate rectangles for big stars | |
if (shape == StarShape.Star) { | |
val hLeft = (x - length / 2).toFloat() | |
val hRight = (x + length / 2).toFloat() | |
val hTop = (y - length / 6).toFloat() | |
val hBottom = (y + length / 6).toFloat() | |
hRect = RectF(hLeft, hTop, hRight, hBottom) | |
val vLeft = (x - length / 6).toFloat() | |
val vRight = (x + length / 6).toFloat() | |
val vTop = (y - length / 2).toFloat() | |
val vBottom = (y + length / 2).toFloat() | |
vRect = RectF(vLeft, vTop, vRight, vBottom) | |
} | |
} | |
internal fun draw(canvas: Canvas?): Canvas? { | |
// set current alpha to paint | |
fillPaint.alpha = alpha | |
strokePaint.alpha = alpha | |
// draw according to shape | |
when (shape) { | |
StarShape.Dot -> { | |
canvas?.drawCircle(x.toFloat(), y.toFloat(), length.toFloat() / 2f, fillPaint) | |
} | |
StarShape.Star -> { | |
canvas?.drawRoundRect(hRect, 6f, 6f, fillPaint) | |
canvas?.drawRoundRect(vRect, 6f, 6f, fillPaint) | |
} | |
StarShape.Circle -> { | |
canvas?.drawCircle(x.toFloat(), y.toFloat(), length.toFloat() / 2f, strokePaint) | |
} | |
} | |
return canvas | |
} | |
private enum class StarShape { | |
Circle, Star, Dot | |
} | |
interface Listener { | |
fun getNewColor(): Int | |
} | |
} | |
private class StarConstraints(val minStarSize: Int, val maxStarSize: Int, val bigStarThreshold: Int) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<declare-styleable name="AnimatedStarsView"> | |
<attr name="starsView_starCount" format="integer" /> | |
<attr name="starsView_starColors" format="reference" /> | |
<attr name="starsView_minStarSize" format="dimension"/> | |
<attr name="starsView_maxStarSize" format="dimension"/> | |
<attr name="starsView_bigStarThreshold" format="dimension"/> | |
</declare-styleable> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<FrameLayout | |
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:id="@+id/stars_container" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent"> | |
<com.sofaking.moonworshipper.view.AnimatedStarsView | |
android:id="@+id/stars_big" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
app:starsView_bigStarThreshold="8dp" | |
app:starsView_maxStarSize="16dp" | |
app:starsView_minStarSize="1dp" | |
app:starsView_starColors="@array/star_colors" | |
app:starsView_starCount="35" /> | |
</FrameLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<integer-array name="star_colors"> | |
<!-- This is how you can configure the ratio of star colors--> | |
<item>@color/star_color_1</item> | |
<item>@color/star_color_1</item> | |
<item>@color/star_color_1</item> | |
<item>@color/star_color_1</item> | |
<item>@color/star_color_2</item> | |
<item>@color/star_color_3</item> | |
</integer-array> | |
</resources> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Don't forget to call the view's onStart() and onStop() from their respective Activity's lifecycle methods.