Skip to content

Commit

Permalink
Support new attributes image, angle, speed
Browse files Browse the repository at this point in the history
  • Loading branch information
johnkil committed Dec 25, 2016
1 parent 361d1c8 commit 2b70d7c
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 67 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions snowfall-sample/src/main/res/layout/activity_snowfall.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
<com.jetradarmobile.snowfall.SnowfallView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:snowflakesNum="150"
app:snowflakeAlphaMin="150"
app:snowflakesNum="250"
app:snowflakeAlphaMin="200"
app:snowflakeAlphaMax="250"
app:snowflakeSizeMin="2dp"
app:snowflakeSizeMax="8dp"
app:snowflakeFadingEnabled="false"/>
app:snowflakeAngleMax="5"
app:snowflakeSizeMin="6dp"
app:snowflakeSizeMax="24dp"
app:snowflakeSpeedMin="4"
app:snowflakeSpeedMax="8"
app:snowflakeFadingEnabled="true"
app:snowflakeImage="@drawable/snowflake"/>

</FrameLayout>
42 changes: 42 additions & 0 deletions snowfall/src/main/java/com/jetradarmobile/snowfall/Drawables.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2016 JetRadar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.jetradarmobile.snowfall

import android.annotation.TargetApi
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build

internal fun Drawable.toBitmap(): Bitmap {
return when (this) {
is BitmapDrawable -> bitmap
is VectorDrawable -> toBitmap()
else -> throw IllegalArgumentException("Unsupported drawable type")
}
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
internal fun VectorDrawable.toBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
setBounds(0, 0, canvas.width, canvas.height)
draw(canvas)
return bitmap
}
49 changes: 49 additions & 0 deletions snowfall/src/main/java/com/jetradarmobile/snowfall/Randomizer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2016 JetRadar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.jetradarmobile.snowfall

import java.lang.Math.abs
import java.util.Random

internal class Randomizer {
private val random by lazy { Random() }

fun randomDouble(max: Int): Double {
return random.nextDouble() * (max + 1)
}

fun randomInt(min: Int, max: Int, gaussian: Boolean = false): Int {
return randomInt(max - min, gaussian) + min
}

fun randomInt(max: Int, gaussian: Boolean = false): Int {
if (gaussian) {
return (abs(randomGaussian()) * (max + 1)).toInt()
} else {
return random.nextInt(max + 1)
}
}

fun randomGaussian(): Double {
val gaussian = random.nextGaussian() / 3 // more 99% of instances in range (-1, 1)
return if (gaussian > -1 && gaussian < 1) gaussian else randomGaussian()
}

fun randomSignum(): Int {
return if (random.nextBoolean()) 1 else -1
}
}
62 changes: 30 additions & 32 deletions snowfall/src/main/java/com/jetradarmobile/snowfall/SnowfallView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,87 +17,85 @@
package com.jetradarmobile.snowfall

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.util.AttributeSet
import android.view.View
import java.util.ArrayList
import java.util.Random

class SnowfallView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val DEFAULT_SNOWFLAKES_NUM = 150
private val DEFAULT_SNOWFLAKES_NUM = 200
private val DEFAULT_SNOWFLAKE_ALPHA_MIN = 150
private val DEFAULT_SNOWFLAKE_ALPHA_MAX = 250
private val DEFAULT_SNOWFLAKE_ANGLE_MAX = 10
private val DEFAULT_SNOWFLAKE_SIZE_MIN_IN_DP = 2
private val DEFAULT_SNOWFLAKE_SIZE_MAX_IN_DP = 8
private val DEFAULT_SNOWFLAKE_SPEED_MIN = 2
private val DEFAULT_SNOWFLAKE_SPEED_MAX = 8
private val DEFAULT_SNOWFLAKE_FADING_ENABLED = false

private val snowflakesNum: Int
private val snowflakeImage: Bitmap?
private val snowflakeAlphaMin: Int
private val snowflakeAlphaMax: Int
private val snowflakeAngleMax: Int
private val snowflakeSizeMinInPx: Int
private val snowflakeSizeMaxInPx: Int
private val snowflakeSpeedMin: Int
private val snowflakeSpeedMax: Int
private val snowflakeFadingEnabled: Boolean

private val snowflakes: MutableList<Snowflake>
private val updateThread by lazy { UpdateThread() }
private val updateSnowflakesThread: UpdateSnowflakesThread

init {
updateThread.start()

val a = context.obtainStyledAttributes(attrs, R.styleable.SnowfallView)
snowflakesNum = a.getInt(R.styleable.SnowfallView_snowflakesNum, DEFAULT_SNOWFLAKES_NUM)
snowflakeImage = a.getDrawable(R.styleable.SnowfallView_snowflakeImage)?.toBitmap()
snowflakeAlphaMin = a.getInt(R.styleable.SnowfallView_snowflakeAlphaMin, DEFAULT_SNOWFLAKE_ALPHA_MIN)
snowflakeAlphaMax = a.getInt(R.styleable.SnowfallView_snowflakeAlphaMax, DEFAULT_SNOWFLAKE_ALPHA_MAX)
snowflakeAngleMax = a.getInt(R.styleable.SnowfallView_snowflakeAngleMax, DEFAULT_SNOWFLAKE_ANGLE_MAX)
snowflakeSizeMinInPx = a.getDimensionPixelSize(R.styleable.SnowfallView_snowflakeSizeMin, dpToPx(DEFAULT_SNOWFLAKE_SIZE_MIN_IN_DP))
snowflakeSizeMaxInPx = a.getDimensionPixelSize(R.styleable.SnowfallView_snowflakeSizeMax, dpToPx(DEFAULT_SNOWFLAKE_SIZE_MAX_IN_DP))
snowflakeSpeedMin = a.getInt(R.styleable.SnowfallView_snowflakeSpeedMin, DEFAULT_SNOWFLAKE_SPEED_MIN)
snowflakeSpeedMax = a.getInt(R.styleable.SnowfallView_snowflakeSpeedMax, DEFAULT_SNOWFLAKE_SPEED_MAX)
snowflakeFadingEnabled = a.getBoolean(R.styleable.SnowfallView_snowflakeFadingEnabled, DEFAULT_SNOWFLAKE_FADING_ENABLED)
a.recycle()

snowflakes = ArrayList(snowflakesNum)
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != oldw || h != oldh) {
snowflakes.clear()
snowflakes.addAll(Array(snowflakesNum, { createSnowflake() }))
}
}

private fun createSnowflake(): Snowflake {
val position = Point(random(width), random(height))
val size = random(snowflakeSizeMinInPx, snowflakeSizeMaxInPx)
val speed = size / 3
val angle = 0F
val alpha = random(snowflakeAlphaMin, snowflakeAlphaMax)
return Snowflake(position = position, size = size, speed = speed, angle = angle, alpha = alpha, fadingEnabled = snowflakeFadingEnabled)
}

private fun random(min: Int, max: Int): Int {
return random(max - min + 1) + min
}

private fun random(max: Int): Int {
return Random().nextInt(max)
updateSnowflakesThread = UpdateSnowflakesThread()
updateSnowflakesThread.start()
}

private fun dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
snowflakes.clear()
val snowflakeParams = Snowflake.Params(
parentWidth = width, parentHeight = height, image = snowflakeImage,
alphaMin = snowflakeAlphaMin, alphaMax = snowflakeAlphaMax, angleMax = snowflakeAngleMax,
sizeMinInPx = snowflakeSizeMinInPx, sizeMaxInPx = snowflakeSizeMaxInPx,
speedMin = snowflakeSpeedMin, speedMax = snowflakeSpeedMax,
fadingEnabled = snowflakeFadingEnabled)
snowflakes.addAll(Array(snowflakesNum, { Snowflake(snowflakeParams) }))
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
snowflakes.forEach { it.draw(canvas) }
}

private inner class UpdateThread : Thread() {
private inner class UpdateSnowflakesThread : Thread() {
private val FPS = 10L

override fun run() {
while (true) {
try { Thread.sleep(FPS) } catch (ignored: InterruptedException) {}
snowflakes.forEach { it.update(width, height) }
snowflakes.forEach { it.update() }
postInvalidateOnAnimation()
}
}
Expand Down
83 changes: 55 additions & 28 deletions snowfall/src/main/java/com/jetradarmobile/snowfall/Snowflake.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,85 @@

package com.jetradarmobile.snowfall

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.Style
import android.graphics.Point
import java.util.Random
import java.lang.Math.cos
import java.lang.Math.sin
import java.lang.Math.toRadians

internal class Snowflake(val params: Params) {
private var size: Int = 0
private var alpha: Int = 255
private var bitmap: Bitmap? = null
private var speedX: Double = 0.0
private var speedY: Double = 0.0
private var positionX: Double = 0.0
private var positionY: Double = 0.0

internal class Snowflake(val position: Point, val size: Int, val speed: Int, val angle: Float, val alpha: Int, val fadingEnabled: Boolean) {
private var speedX: Int = 0
private var speedY: Int = 0
private val paint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.rgb(255, 255, 255)
alpha = this@Snowflake.alpha
style = Style.FILL
}
}
private val randomizer by lazy { Randomizer() }

init {
calculateSpeed()
init()
}

fun update(width: Int, height: Int) {
var x = position.x + speedX
var y = position.y + speedY
if (y > height) {
calculateSpeed()
x = Random().nextInt(width)
y = -size - 1
private fun init() {
size = randomizer.randomInt(params.sizeMinInPx, params.sizeMaxInPx, gaussian = true)
if (params.image != null) {
bitmap = Bitmap.createScaledBitmap(params.image, size, size, false)
}

if (fadingEnabled) {
paint.alpha = (alpha * ((height.toFloat() - y.toFloat()) / (height.toFloat()))).toInt()
}
val speed = ((size - params.sizeMinInPx).toFloat() / (params.sizeMaxInPx - params.sizeMinInPx) *
(params.speedMax - params.speedMin) + params.speedMin)
val angle = toRadians(randomizer.randomDouble(params.angleMax) * randomizer.randomSignum())
speedX = speed * sin(angle)
speedY = speed * cos(angle)

alpha = randomizer.randomInt(params.alphaMin, params.alphaMax)
paint.alpha = alpha

position.set(x, y)
positionX = randomizer.randomDouble(params.parentWidth)
positionY = randomizer.randomDouble(params.parentHeight)
}

fun calculateSpeed() {
speedX = (speed * Math.sin(angle())).toInt()
speedY = (speed * Math.cos(angle())).toInt()
if (speedY == 0) {
speedY++
fun update() {
positionX += speedX
positionY += speedY
if (positionY > params.parentHeight) {
init()
positionY = -size.toDouble()
}
if (params.fadingEnabled) {
paint.alpha = (alpha * ((params.parentHeight - positionY).toFloat() / params.parentHeight)).toInt()
}
}

fun draw(canvas: Canvas) {
canvas.drawCircle(position.x.toFloat(), position.y.toFloat(), size.toFloat(), paint)
if (bitmap != null) {
canvas.drawBitmap(bitmap, positionX.toFloat(), positionY.toFloat(), paint)
} else {
canvas.drawCircle(positionX.toFloat(), positionY.toFloat(), size.toFloat(), paint)
}
}

private fun angle(): Double {
val angle = Random().nextInt(30) - 15
return Math.toRadians(angle.toDouble())
}
data class Params(
val parentWidth: Int,
val parentHeight: Int,
val image: Bitmap?,
val alphaMin: Int,
val alphaMax: Int,
val angleMax: Int,
val sizeMinInPx: Int,
val sizeMaxInPx: Int,
val speedMin: Int,
val speedMax: Int,
val fadingEnabled: Boolean)
}
8 changes: 6 additions & 2 deletions snowfall/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@

<declare-styleable name="SnowfallView">
<attr name="snowflakesNum" format="integer"/>
<attr name="snowflakeSizeMin" format="dimension"/>
<attr name="snowflakeSizeMax" format="dimension"/>
<attr name="snowflakeImage" format="reference"/>
<attr name="snowflakeAlphaMin" format="integer"/>
<attr name="snowflakeAlphaMax" format="integer"/>
<attr name="snowflakeAngleMax" format="integer"/>
<attr name="snowflakeSizeMin" format="dimension"/>
<attr name="snowflakeSizeMax" format="dimension"/>
<attr name="snowflakeSpeedMin" format="integer"/>
<attr name="snowflakeSpeedMax" format="integer"/>
<attr name="snowflakeFadingEnabled" format="boolean"/>
</declare-styleable>

Expand Down

0 comments on commit 2b70d7c

Please sign in to comment.