Repo created
This commit is contained in:
parent
d6b5d53060
commit
d90a1dc8df
2145 changed files with 210227 additions and 2 deletions
1
ui-weather-view/.gitignore
vendored
Normal file
1
ui-weather-view/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
16
ui-weather-view/build.gradle.kts
Normal file
16
ui-weather-view/build.gradle.kts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
id("breezy.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.breezyweather.ui.theme.weatherView"
|
||||
|
||||
defaultConfig {
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.core.ktx)
|
||||
}
|
||||
0
ui-weather-view/consumer-rules.pro
Normal file
0
ui-weather-view/consumer-rules.pro
Normal file
21
ui-weather-view/proguard-rules.pro
vendored
Normal file
21
ui-weather-view/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.hardware.SensorManager
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
/**
|
||||
* Duplicate of existing extensions, so that the lib can be compiled separately
|
||||
* TODO: Move into a dedicated module to avoid duplicate
|
||||
*/
|
||||
val Context.isLandscape: Boolean
|
||||
get() = this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
fun Context.dpToPx(dp: Float): Float {
|
||||
return dp * (this.resources.displayMetrics.densityDpi / 160f)
|
||||
}
|
||||
|
||||
val Context.sensorManager: SensorManager?
|
||||
get() = getSystemService()
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
|
||||
interface WeatherThemeDelegate {
|
||||
|
||||
fun getWeatherView(context: Context): WeatherView
|
||||
|
||||
/**
|
||||
* @return colors[] {
|
||||
* theme color,
|
||||
* color of daytime chart line,
|
||||
* color of nighttime chart line
|
||||
* }
|
||||
*/
|
||||
@ColorInt
|
||||
@Size(3)
|
||||
fun getThemeColors(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): IntArray
|
||||
|
||||
fun isLightBackground(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Boolean
|
||||
|
||||
@ColorInt
|
||||
fun getBackgroundColor(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Int
|
||||
|
||||
@ColorInt
|
||||
fun getOnBackgroundColor(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Int
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
|
||||
/**
|
||||
* Weather view.
|
||||
*
|
||||
* This view is used to draw the weather phenomenon.
|
||||
*/
|
||||
interface WeatherView {
|
||||
@IntDef(
|
||||
WEATHER_KIND_NULL,
|
||||
WEATHER_KIND_CLEAR,
|
||||
WEATHER_KIND_CLOUD,
|
||||
WEATHER_KIND_CLOUDY,
|
||||
WEATHER_KIND_RAINY,
|
||||
WEATHER_KIND_SNOW,
|
||||
WEATHER_KIND_SLEET,
|
||||
WEATHER_KIND_HAIL,
|
||||
WEATHER_KIND_FOG,
|
||||
WEATHER_KIND_HAZE,
|
||||
WEATHER_KIND_THUNDER,
|
||||
WEATHER_KIND_THUNDERSTORM,
|
||||
WEATHER_KIND_WIND
|
||||
)
|
||||
annotation class WeatherKindRule
|
||||
|
||||
fun setWeather(@WeatherKindRule weatherKind: Int, daytime: Boolean, darkMode: Boolean)
|
||||
|
||||
fun onScroll(scrollY: Int)
|
||||
|
||||
@get:WeatherKindRule
|
||||
val weatherKind: Int
|
||||
fun setDrawable(drawable: Boolean)
|
||||
fun setDoAnimate(animate: Boolean)
|
||||
fun setGravitySensorEnabled(enabled: Boolean)
|
||||
|
||||
companion object {
|
||||
const val WEATHER_KIND_NULL = 0
|
||||
const val WEATHER_KIND_CLEAR = 1
|
||||
const val WEATHER_KIND_CLOUD = 2 // Partly cloudy
|
||||
const val WEATHER_KIND_CLOUDY = 3 // Cloudy
|
||||
const val WEATHER_KIND_RAINY = 4
|
||||
const val WEATHER_KIND_SNOW = 5
|
||||
const val WEATHER_KIND_SLEET = 6
|
||||
const val WEATHER_KIND_HAIL = 7
|
||||
const val WEATHER_KIND_FOG = 8
|
||||
const val WEATHER_KIND_HAZE = 9
|
||||
const val WEATHER_KIND_THUNDER = 10
|
||||
const val WEATHER_KIND_THUNDERSTORM = 11
|
||||
const val WEATHER_KIND_WIND = 12
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.RotateController
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Delay Rotate controller.
|
||||
*/
|
||||
class DelayRotateController(
|
||||
initRotation: Double,
|
||||
) : RotateController() {
|
||||
private var mTargetRotation: Double = getRotationInScope(initRotation)
|
||||
private var mCurrentRotation: Double = mTargetRotation
|
||||
private var mVelocity: Double = 0.0
|
||||
private var mAcceleration: Double = 0.0
|
||||
|
||||
override fun updateRotation(rotation: Double, interval: Double) {
|
||||
mTargetRotation = getRotationInScope(rotation)
|
||||
val rotationDiff = mTargetRotation - mCurrentRotation
|
||||
|
||||
// no need to move
|
||||
if (rotationDiff == 0.0) {
|
||||
mAcceleration = 0.0
|
||||
mVelocity = 0.0
|
||||
return
|
||||
}
|
||||
|
||||
val accelSign = when (mTargetRotation > mCurrentRotation) {
|
||||
true -> 1
|
||||
false -> -1
|
||||
}
|
||||
val oldVelocity = mVelocity
|
||||
|
||||
if (mVelocity == 0.0 || rotationDiff * mVelocity < 0) {
|
||||
// start or turn around.
|
||||
mAcceleration = accelSign * DEFAULT_ABS_ACCELERATION
|
||||
mVelocity = mAcceleration * interval
|
||||
} else if (mVelocity.pow(2.0) / (2.0 * DEFAULT_ABS_ACCELERATION) < abs(rotationDiff)) {
|
||||
// speed up
|
||||
mAcceleration = accelSign * DEFAULT_ABS_ACCELERATION
|
||||
mVelocity += mAcceleration * interval
|
||||
} else {
|
||||
// slow down
|
||||
mAcceleration = -1 * accelSign * mVelocity.pow(2.0) / (2.0 * abs(rotationDiff))
|
||||
mVelocity += mAcceleration * interval
|
||||
}
|
||||
|
||||
val distance = oldVelocity * interval + mAcceleration * interval.pow(2.0) / 2.0
|
||||
|
||||
if (abs(distance) > abs(rotationDiff)) {
|
||||
mAcceleration = 0.0
|
||||
mCurrentRotation = mTargetRotation
|
||||
mVelocity = 0.0
|
||||
} else {
|
||||
mCurrentRotation += distance
|
||||
}
|
||||
}
|
||||
|
||||
override val rotation: Double
|
||||
get() = mCurrentRotation
|
||||
|
||||
private fun getRotationInScope(rotationP: Double): Double {
|
||||
var rotation = rotationP
|
||||
rotation %= 180.0
|
||||
return if (abs(rotation) <= 90) {
|
||||
rotation
|
||||
} else { // abs(rotation) < 180
|
||||
if (rotation > 0) {
|
||||
90 - (rotation - 90)
|
||||
} else {
|
||||
-90 - (rotation + 90)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ABS_ACCELERATION = 90.0 / 200.0 / 800.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
class IntervalComputer {
|
||||
private var mCurrentTime: Long = 0
|
||||
private var mLastTime: Long = 0
|
||||
var interval = 0.0
|
||||
private set
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
mCurrentTime = -1
|
||||
mLastTime = -1
|
||||
interval = 0.0
|
||||
}
|
||||
|
||||
fun invalidate() {
|
||||
mCurrentTime = System.currentTimeMillis()
|
||||
interval = (if (mLastTime == -1L) 0 else mCurrentTime - mLastTime).toDouble()
|
||||
mLastTime = mCurrentTime
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.Size
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.withTranslation
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView.WeatherKindRule
|
||||
import org.breezyweather.ui.theme.weatherView.isLandscape
|
||||
import org.breezyweather.ui.theme.weatherView.sensorManager
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.acos
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class MaterialPainterView(
|
||||
context: Context,
|
||||
|
||||
@WeatherKindRule private var weatherKind: Int,
|
||||
private var daylight: Boolean,
|
||||
|
||||
isDrawable: Boolean,
|
||||
currentScrollRate: Float,
|
||||
|
||||
var gravitySensorEnabled: Boolean,
|
||||
var animatable: Boolean,
|
||||
) : View(context) {
|
||||
|
||||
private var intervalComputer: IntervalComputer? = null
|
||||
|
||||
private var impl: MaterialWeatherView.WeatherAnimationImplementor? = null
|
||||
private var rotators: Array<MaterialWeatherView.RotateController>? = null
|
||||
|
||||
private var gravitySensor: Sensor? = null
|
||||
|
||||
@Size(2)
|
||||
private var canvasSize = IntArray(2)
|
||||
private var rotation2D = 0f
|
||||
private var rotation3D = 0f
|
||||
|
||||
@FloatRange(from = 0.0)
|
||||
private var lastScrollRate = 0f
|
||||
|
||||
@FloatRange(from = 0.0)
|
||||
var scrollRate = 0f
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
if (lastScrollRate >= 1 && field < 1) {
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
var drawable = false
|
||||
set(value) {
|
||||
if (field == value) {
|
||||
return
|
||||
}
|
||||
field = value
|
||||
|
||||
if (value) {
|
||||
resetDrawer()
|
||||
return
|
||||
}
|
||||
|
||||
context.sensorManager?.unregisterListener(mGravityListener, gravitySensor)
|
||||
orientationListener.disable()
|
||||
}
|
||||
|
||||
private var hasDrawn = false
|
||||
private var mDeviceOrientation: DeviceOrientation? = null
|
||||
|
||||
private enum class DeviceOrientation {
|
||||
TOP,
|
||||
LEFT,
|
||||
BOTTOM,
|
||||
RIGHT,
|
||||
}
|
||||
|
||||
private val mGravityListener: SensorEventListener = object : SensorEventListener {
|
||||
|
||||
override fun onSensorChanged(ev: SensorEvent) {
|
||||
// x : (+) fall to the left / (-) fall to the right.
|
||||
// y : (+) stand / (-) head stand.
|
||||
// z : (+) look down / (-) look up.
|
||||
// rotation2D : (+) anticlockwise / (-) clockwise.
|
||||
// rotation3D : (+) look down / (-) look up.
|
||||
if (gravitySensorEnabled) {
|
||||
val aX = ev.values[0]
|
||||
val aY = ev.values[1]
|
||||
val aZ = ev.values[2]
|
||||
|
||||
val g2D = sqrt((aX * aX + aY * aY).toDouble())
|
||||
val g3D = sqrt((aX * aX + aY * aY + aZ * aZ).toDouble())
|
||||
|
||||
val cos2D = 1.0.coerceAtMost(aY / g2D).coerceAtLeast(-1.0)
|
||||
val cos3D = 1.0.coerceAtMost(g2D / g3D).coerceAtLeast(-1.0)
|
||||
|
||||
rotation2D = Math.toDegrees(acos(cos2D)).toFloat() * if (aX >= 0) 1 else -1
|
||||
rotation3D = Math.toDegrees(acos(cos3D)).toFloat() * if (aZ >= 0) 1 else -1
|
||||
|
||||
when (mDeviceOrientation) {
|
||||
DeviceOrientation.TOP -> {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
DeviceOrientation.LEFT -> {
|
||||
rotation2D -= 90f
|
||||
}
|
||||
|
||||
DeviceOrientation.RIGHT -> {
|
||||
rotation2D += 90f
|
||||
}
|
||||
|
||||
DeviceOrientation.BOTTOM -> {
|
||||
if (rotation2D > 0) {
|
||||
rotation2D -= 180f
|
||||
} else {
|
||||
rotation2D += 180f
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
if (60 < abs(rotation3D) && abs(rotation3D) < 120) {
|
||||
rotation2D *= (abs(abs(rotation3D) - 90) / 30.0).toFloat()
|
||||
}
|
||||
} else {
|
||||
rotation2D = 0f
|
||||
rotation3D = 0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, i: Int) {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
private val orientationListener: OrientationEventListener = object : OrientationEventListener(
|
||||
getContext()
|
||||
) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
mDeviceOrientation = getDeviceOrientation(orientation)
|
||||
}
|
||||
|
||||
private fun getDeviceOrientation(orientation: Int): DeviceOrientation {
|
||||
return if (context.isLandscape) {
|
||||
if (orientation in 1..179) {
|
||||
DeviceOrientation.RIGHT
|
||||
} else {
|
||||
DeviceOrientation.LEFT
|
||||
}
|
||||
} else {
|
||||
if (270 < orientation || orientation < 90) {
|
||||
DeviceOrientation.TOP
|
||||
} else {
|
||||
DeviceOrientation.BOTTOM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
gravitySensor = context.sensorManager?.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
|
||||
val metrics = resources.displayMetrics
|
||||
canvasSize = intArrayOf(
|
||||
metrics.widthPixels,
|
||||
metrics.heightPixels
|
||||
)
|
||||
|
||||
drawable = isDrawable
|
||||
lastScrollRate = currentScrollRate
|
||||
scrollRate = currentScrollRate
|
||||
mDeviceOrientation = DeviceOrientation.TOP
|
||||
|
||||
background = getWeatherBackgroundDrawable(weatherKind, daylight)
|
||||
}
|
||||
|
||||
fun update(
|
||||
@WeatherKindRule weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
gravitySensorEnabled: Boolean,
|
||||
animate: Boolean,
|
||||
) {
|
||||
this.weatherKind = weatherKind
|
||||
this.daylight = daylight
|
||||
this.gravitySensorEnabled = gravitySensorEnabled
|
||||
this.animatable = animate
|
||||
|
||||
if (drawable) {
|
||||
setWeatherImplementor()
|
||||
setIntervalComputer()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
background = getWeatherBackgroundDrawable(weatherKind, daylight)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
|
||||
if (measuredWidth != 0 && measuredHeight != 0) {
|
||||
val width = measuredWidth
|
||||
val height = measuredHeight
|
||||
|
||||
if (canvasSize[0] != width || canvasSize[1] != height) {
|
||||
canvasSize[0] = width
|
||||
canvasSize[1] = height
|
||||
setWeatherImplementor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is inefficient for cases when animations are disabled,
|
||||
// as basic view clears canvas on each call, forcing us to redraw with same data
|
||||
// maybe use TextureView/SurfaceView or save to bitmap?
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (intervalComputer == null || rotators == null || impl == null) {
|
||||
return
|
||||
}
|
||||
|
||||
intervalComputer!!.invalidate()
|
||||
rotators!![0].updateRotation(rotation2D.toDouble(), intervalComputer!!.interval)
|
||||
rotators!![1].updateRotation(rotation3D.toDouble(), intervalComputer!!.interval)
|
||||
|
||||
var interval = intervalComputer!!.interval
|
||||
if (!animatable) {
|
||||
if (hasDrawn) {
|
||||
interval = 0.0
|
||||
} else {
|
||||
hasDrawn = true
|
||||
}
|
||||
}
|
||||
|
||||
impl!!.updateData(
|
||||
canvasSize,
|
||||
interval.toLong(),
|
||||
rotators!![0].rotation.toFloat(),
|
||||
rotators!![1].rotation.toFloat()
|
||||
)
|
||||
|
||||
if (impl != null && rotators != null) {
|
||||
canvas.withTranslation(
|
||||
(measuredWidth - canvasSize[0]) / 2f,
|
||||
(measuredHeight - canvasSize[1]) / 2f
|
||||
) {
|
||||
impl!!.draw(
|
||||
canvasSize,
|
||||
this,
|
||||
scrollRate,
|
||||
rotators!![0].rotation.toFloat(),
|
||||
rotators!![1].rotation.toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawable) {
|
||||
return
|
||||
}
|
||||
if (lastScrollRate >= 1 && scrollRate >= 1) {
|
||||
lastScrollRate = scrollRate
|
||||
setIntervalComputer()
|
||||
return
|
||||
}
|
||||
lastScrollRate = scrollRate
|
||||
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private fun resetDrawer() {
|
||||
rotation2D = 0f
|
||||
rotation3D = 0f
|
||||
|
||||
context.sensorManager?.registerListener(
|
||||
mGravityListener,
|
||||
gravitySensor,
|
||||
SensorManager.SENSOR_DELAY_FASTEST
|
||||
)
|
||||
|
||||
if (orientationListener.canDetectOrientation()) {
|
||||
orientationListener.enable()
|
||||
}
|
||||
|
||||
setWeatherImplementor()
|
||||
setIntervalComputer()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private fun getWeatherBackgroundDrawable(
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
) = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
WeatherImplementorFactory.getBackgroundId(weatherKind, daylight),
|
||||
null
|
||||
)
|
||||
|
||||
private fun setWeatherImplementor() {
|
||||
hasDrawn = false
|
||||
impl = WeatherImplementorFactory.getWeatherImplementor(
|
||||
context,
|
||||
weatherKind,
|
||||
daylight,
|
||||
canvasSize,
|
||||
animatable
|
||||
)
|
||||
rotators = arrayOf(
|
||||
DelayRotateController(
|
||||
rotation2D.toDouble()
|
||||
),
|
||||
DelayRotateController(
|
||||
rotation3D.toDouble()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setIntervalComputer() {
|
||||
if (intervalComputer == null) {
|
||||
intervalComputer =
|
||||
IntervalComputer()
|
||||
} else {
|
||||
intervalComputer!!.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherThemeDelegate
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView.WeatherKindRule
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.CloudImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.HailImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.MeteorShowerImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.RainImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.SnowImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.SunImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.WindImplementor
|
||||
|
||||
class MaterialWeatherThemeDelegate : WeatherThemeDelegate {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getBrighterColor(color: Int): Int {
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(color, hsv)
|
||||
hsv[1] = hsv[1] - 0.25f
|
||||
hsv[2] = hsv[2] + 0.25f
|
||||
return Color.HSVToColor(hsv)
|
||||
}
|
||||
|
||||
private fun innerGetBackgroundColor(
|
||||
@WeatherKindRule weatherKind: Int,
|
||||
daytime: Boolean,
|
||||
): Int = when (weatherKind) {
|
||||
WeatherView.WEATHER_KIND_CLEAR -> if (daytime) {
|
||||
SunImplementor.themeColor
|
||||
} else {
|
||||
MeteorShowerImplementor.themeColor
|
||||
}
|
||||
WeatherView.WEATHER_KIND_CLOUD -> CloudImplementor.getThemeColor(CloudImplementor.TYPE_CLOUD, daytime)
|
||||
WeatherView.WEATHER_KIND_CLOUDY -> CloudImplementor.getThemeColor(CloudImplementor.TYPE_CLOUDY, daytime)
|
||||
WeatherView.WEATHER_KIND_FOG -> CloudImplementor.getThemeColor(CloudImplementor.TYPE_FOG, daytime)
|
||||
WeatherView.WEATHER_KIND_HAIL -> HailImplementor.getThemeColor(daytime)
|
||||
WeatherView.WEATHER_KIND_HAZE -> CloudImplementor.getThemeColor(CloudImplementor.TYPE_HAZE, daytime)
|
||||
WeatherView.WEATHER_KIND_RAINY -> RainImplementor.getThemeColor(RainImplementor.TYPE_RAIN, daytime)
|
||||
WeatherView.WEATHER_KIND_SLEET -> RainImplementor.getThemeColor(RainImplementor.TYPE_SLEET, daytime)
|
||||
WeatherView.WEATHER_KIND_SNOW -> SnowImplementor.getThemeColor(daytime)
|
||||
WeatherView.WEATHER_KIND_THUNDERSTORM ->
|
||||
RainImplementor.getThemeColor(RainImplementor.TYPE_THUNDERSTORM, daytime)
|
||||
WeatherView.WEATHER_KIND_THUNDER -> CloudImplementor.getThemeColor(CloudImplementor.TYPE_THUNDER, daytime)
|
||||
WeatherView.WEATHER_KIND_WIND -> WindImplementor.getThemeColor(daytime)
|
||||
else -> Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
|
||||
override fun getWeatherView(context: Context): WeatherView = MaterialWeatherView(context)
|
||||
|
||||
override fun getThemeColors(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): IntArray {
|
||||
var color = innerGetBackgroundColor(weatherKind, daylight)
|
||||
if (!daylight) {
|
||||
color = getBrighterColor(color)
|
||||
}
|
||||
return intArrayOf(
|
||||
color,
|
||||
color,
|
||||
ColorUtils.setAlphaComponent(color, (0.5 * 255).toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override fun isLightBackground(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Boolean {
|
||||
return daylight &&
|
||||
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) !=
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
|
||||
override fun getBackgroundColor(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Int {
|
||||
return innerGetBackgroundColor(weatherKind, daylight)
|
||||
}
|
||||
|
||||
override fun getOnBackgroundColor(
|
||||
context: Context,
|
||||
weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Int {
|
||||
return if (isLightBackground(context, weatherKind, daylight)) Color.BLACK else Color.WHITE
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.annotation.Size
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView.WeatherKindRule
|
||||
import kotlin.math.min
|
||||
|
||||
class MaterialWeatherView(
|
||||
context: Context,
|
||||
) : ViewGroup(context), WeatherView {
|
||||
private var mCurrentView: MaterialPainterView? = null
|
||||
private var mPreviousView: MaterialPainterView? = null
|
||||
private var mSwitchAnimator: Animator? = null
|
||||
|
||||
@WeatherKindRule
|
||||
override var weatherKind = 0
|
||||
private set
|
||||
private var mDaytime = false
|
||||
private var mDarkMode = false
|
||||
private var mFirstCardMarginTop = 0
|
||||
private var mGravitySensorEnabled: Boolean = true
|
||||
private var mAnimate: Boolean = true
|
||||
private var mDrawable: Boolean = false
|
||||
|
||||
/**
|
||||
* This class is used to implement different kinds of weather animations.
|
||||
*/
|
||||
abstract class WeatherAnimationImplementor {
|
||||
abstract fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
)
|
||||
|
||||
// return true if finish drawing.
|
||||
abstract fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
)
|
||||
}
|
||||
|
||||
abstract class RotateController {
|
||||
abstract fun updateRotation(rotation: Double, interval: Double)
|
||||
abstract val rotation: Double
|
||||
}
|
||||
|
||||
init {
|
||||
setWeather(WeatherView.WEATHER_KIND_NULL, daytime = true, darkMode = false)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
// At what position will animations disappear
|
||||
for (index in 0 until childCount) {
|
||||
val child = getChildAt(index)
|
||||
child.measure(
|
||||
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
val insets = ViewCompat.getRootWindowInsets(this)
|
||||
val i = insets?.getInsets(WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
// TODO: Arbitrary value. If too high, will blink. See also #2241 for attempt at a more effective fix
|
||||
mFirstCardMarginTop = ((i?.top ?: 0) + 500) // 0.66
|
||||
}
|
||||
|
||||
override fun onLayout(b: Boolean, i: Int, i1: Int, i2: Int, i3: Int) {
|
||||
for (index in 0 until childCount) {
|
||||
val child = getChildAt(index)
|
||||
child.layout(
|
||||
0,
|
||||
0,
|
||||
child.measuredWidth,
|
||||
child.measuredHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// interface.
|
||||
// weather view.
|
||||
override fun setWeather(
|
||||
@WeatherKindRule weatherKind: Int,
|
||||
daytime: Boolean,
|
||||
darkMode: Boolean,
|
||||
) {
|
||||
// do nothing if weather not change.
|
||||
if (this.weatherKind == weatherKind && mDaytime == daytime && mDarkMode == darkMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// cache weather state.
|
||||
this.weatherKind = weatherKind
|
||||
mDaytime = daytime
|
||||
mDarkMode = darkMode
|
||||
|
||||
// cancel the previous switch animation if necessary.
|
||||
mSwitchAnimator?.let {
|
||||
it.cancel()
|
||||
mSwitchAnimator = null
|
||||
}
|
||||
|
||||
// stop current painting work.
|
||||
mCurrentView?.let {
|
||||
it.drawable = false
|
||||
}
|
||||
|
||||
// generate new painter view or update painter cache.
|
||||
val prev = mPreviousView
|
||||
mPreviousView = mCurrentView
|
||||
mCurrentView = prev
|
||||
mCurrentView?.let {
|
||||
it.update(weatherKind, daytime, mGravitySensorEnabled, mAnimate)
|
||||
it.drawable = mDrawable
|
||||
} ?: run {
|
||||
mCurrentView = MaterialPainterView(
|
||||
context,
|
||||
weatherKind,
|
||||
daytime,
|
||||
mDrawable,
|
||||
mPreviousView?.scrollRate ?: 0f,
|
||||
mGravitySensorEnabled,
|
||||
mAnimate
|
||||
)
|
||||
addView(mCurrentView)
|
||||
}
|
||||
|
||||
// execute switch animation.
|
||||
mPreviousView?.let {
|
||||
mSwitchAnimator = AnimatorSet().apply {
|
||||
duration = SWITCH_ANIMATION_DURATION
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(mCurrentView as MaterialPainterView, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(
|
||||
mPreviousView as MaterialPainterView,
|
||||
"alpha",
|
||||
it.alpha,
|
||||
0f
|
||||
)
|
||||
)
|
||||
}.also { it.start() }
|
||||
} ?: run {
|
||||
mCurrentView?.alpha = 1f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScroll(scrollY: Int) {
|
||||
val scrollRate = min(1.0, 1.0 * scrollY / mFirstCardMarginTop).toFloat()
|
||||
mCurrentView?.let {
|
||||
it.scrollRate = scrollRate
|
||||
}
|
||||
mPreviousView?.let {
|
||||
it.scrollRate = scrollRate
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDrawable(drawable: Boolean) {
|
||||
if (mDrawable == drawable) {
|
||||
return
|
||||
}
|
||||
mDrawable = drawable
|
||||
mCurrentView?.let {
|
||||
it.drawable = drawable
|
||||
}
|
||||
mPreviousView?.let {
|
||||
it.drawable = drawable
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDoAnimate(animate: Boolean) {
|
||||
mAnimate = animate
|
||||
}
|
||||
|
||||
override fun setGravitySensorEnabled(enabled: Boolean) {
|
||||
mGravitySensorEnabled = enabled
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SWITCH_ANIMATION_DURATION: Long = 300
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.R
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherView.WeatherKindRule
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.CloudImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.HailImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.MeteorShowerImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.RainImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.SnowImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.SunImplementor
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor.WindImplementor
|
||||
|
||||
object WeatherImplementorFactory {
|
||||
|
||||
fun getWeatherImplementor(
|
||||
context: Context,
|
||||
@WeatherKindRule weatherKind: Int,
|
||||
daytime: Boolean,
|
||||
@Size(2) sizes: IntArray,
|
||||
animate: Boolean,
|
||||
): WeatherAnimationImplementor? {
|
||||
val darkMode = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
return when (weatherKind) {
|
||||
WeatherView.WEATHER_KIND_CLEAR -> if (daytime) {
|
||||
SunImplementor(
|
||||
sizes,
|
||||
animate
|
||||
)
|
||||
} else {
|
||||
MeteorShowerImplementor(
|
||||
sizes,
|
||||
animate
|
||||
)
|
||||
}
|
||||
|
||||
WeatherView.WEATHER_KIND_CLOUD ->
|
||||
CloudImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
CloudImplementor.TYPE_CLOUD,
|
||||
daytime,
|
||||
darkMode
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_CLOUDY ->
|
||||
CloudImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
CloudImplementor.TYPE_CLOUDY,
|
||||
daytime,
|
||||
darkMode
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_FOG ->
|
||||
CloudImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
CloudImplementor.TYPE_FOG,
|
||||
daytime,
|
||||
darkMode
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_HAZE ->
|
||||
CloudImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
CloudImplementor.TYPE_HAZE,
|
||||
daytime,
|
||||
darkMode
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_RAINY ->
|
||||
RainImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
RainImplementor.TYPE_RAIN,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_SLEET ->
|
||||
RainImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
RainImplementor.TYPE_SLEET,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_SNOW ->
|
||||
SnowImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_HAIL ->
|
||||
HailImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_THUNDERSTORM ->
|
||||
RainImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
RainImplementor.TYPE_THUNDERSTORM,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_THUNDER ->
|
||||
CloudImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
CloudImplementor.TYPE_THUNDER,
|
||||
daytime
|
||||
)
|
||||
|
||||
WeatherView.WEATHER_KIND_WIND ->
|
||||
WindImplementor(
|
||||
sizes,
|
||||
animate,
|
||||
daytime
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getBackgroundId(
|
||||
@WeatherKindRule weatherKind: Int,
|
||||
daylight: Boolean,
|
||||
): Int = when (weatherKind) {
|
||||
WeatherView.WEATHER_KIND_CLEAR -> if (daylight) {
|
||||
R.drawable.weather_background_clear_day
|
||||
} else {
|
||||
R.drawable.weather_background_clear_night
|
||||
}
|
||||
WeatherView.WEATHER_KIND_CLOUD -> R.drawable.weather_background_partly_cloudy
|
||||
WeatherView.WEATHER_KIND_CLOUDY -> R.drawable.weather_background_cloudy
|
||||
WeatherView.WEATHER_KIND_FOG -> R.drawable.weather_background_fog
|
||||
WeatherView.WEATHER_KIND_HAIL -> R.drawable.weather_background_hail
|
||||
WeatherView.WEATHER_KIND_HAZE -> R.drawable.weather_background_haze
|
||||
WeatherView.WEATHER_KIND_RAINY -> R.drawable.weather_background_rain
|
||||
WeatherView.WEATHER_KIND_SLEET -> R.drawable.weather_background_sleet
|
||||
WeatherView.WEATHER_KIND_SNOW -> R.drawable.weather_background_snow
|
||||
WeatherView.WEATHER_KIND_THUNDER, WeatherView.WEATHER_KIND_THUNDERSTORM -> R.drawable.weather_background_thunder
|
||||
WeatherView.WEATHER_KIND_WIND -> R.drawable.weather_background_wind
|
||||
else -> R.drawable.weather_background_default
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,603 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.Size
|
||||
import androidx.core.graphics.toColorInt
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
class CloudImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
@TypeRule type: Int,
|
||||
daylight: Boolean,
|
||||
darkMode: Boolean = !daylight,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private var mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private var mClouds: Array<Cloud> = emptyArray()
|
||||
private var mStars: Array<Star> = emptyArray()
|
||||
private var mThunder: Thunder? = null
|
||||
private val mRandom: Random
|
||||
|
||||
@IntDef(TYPE_CLOUD, TYPE_CLOUDY, TYPE_THUNDER, TYPE_FOG, TYPE_HAZE)
|
||||
internal annotation class TypeRule
|
||||
private class Cloud(
|
||||
private val mInitCX: Float,
|
||||
private val mInitCY: Float,
|
||||
var radius: Float,
|
||||
val scaleRatio: Float,
|
||||
val moveFactor: Float,
|
||||
@ColorInt val color: Int,
|
||||
val alpha: Float,
|
||||
val duration: Long,
|
||||
initProgress: Long,
|
||||
) {
|
||||
var centerX: Float = mInitCX
|
||||
var centerY: Float = mInitCY
|
||||
var initRadius: Float = radius
|
||||
var progress: Long = initProgress % duration
|
||||
|
||||
init {
|
||||
computeRadius(duration, progress)
|
||||
}
|
||||
|
||||
fun move(interval: Long, rotation2D: Float, rotation3D: Float) {
|
||||
centerX = (mInitCX + sin(rotation2D * Math.PI / 180.0) * 0.40 * radius * moveFactor).toFloat()
|
||||
centerY = (mInitCY - sin(rotation3D * Math.PI / 180.0) * 0.50 * radius * moveFactor).toFloat()
|
||||
progress = (progress + interval) % duration
|
||||
computeRadius(duration, progress)
|
||||
}
|
||||
|
||||
private fun computeRadius(duration: Long, progress: Long) {
|
||||
radius = if (progress < 0.5 * duration) {
|
||||
(initRadius * (1 + (scaleRatio - 1) * progress / 0.5 / duration)).toFloat()
|
||||
} else {
|
||||
(initRadius * (scaleRatio - (scaleRatio - 1) * (progress - 0.5 * duration) / 0.5 / duration)).toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Star(
|
||||
val centerX: Float,
|
||||
val centerY: Float,
|
||||
radius: Float,
|
||||
@field:ColorInt @param:ColorInt val color: Int,
|
||||
val duration: Long,
|
||||
val animate: Boolean,
|
||||
) {
|
||||
var radius: Float
|
||||
var alpha = 0f
|
||||
var progress: Long = 0
|
||||
|
||||
init {
|
||||
this.radius = (radius * (0.7 + 0.3 * Random().nextFloat())).toFloat()
|
||||
if (!animate) {
|
||||
alpha = Random().nextFloat()
|
||||
} else {
|
||||
computeAlpha(duration, progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun shine(interval: Long) {
|
||||
if (!animate) return
|
||||
|
||||
progress = (progress + interval) % duration
|
||||
computeAlpha(duration, progress)
|
||||
}
|
||||
|
||||
private fun computeAlpha(duration: Long, progress: Long) {
|
||||
alpha = if (progress < 0.5 * duration) {
|
||||
(progress / 0.5 / duration).toFloat()
|
||||
} else {
|
||||
(1 - (progress - 0.5 * duration) / 0.5 / duration).toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Thunder {
|
||||
var r = 81
|
||||
var g = 67
|
||||
var b = 168
|
||||
var alpha = 0f
|
||||
private var progress: Long = 0
|
||||
private var duration: Long = 0
|
||||
private var delay: Long = 0
|
||||
|
||||
init {
|
||||
init()
|
||||
computeFrame()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
progress = 0
|
||||
duration = 300
|
||||
delay = (Random().nextInt(5000) + 2000).toLong()
|
||||
}
|
||||
|
||||
private fun computeFrame() {
|
||||
alpha = if (progress < duration) {
|
||||
if (progress < 0.25 * duration) {
|
||||
(progress / 0.25 / duration).toFloat()
|
||||
} else if (progress < 0.5 * duration) {
|
||||
(1 - (progress - 0.25 * duration) / 0.25 / duration).toFloat()
|
||||
} else if (progress < 0.75 * duration) {
|
||||
((progress - 0.5 * duration) / 0.25 / duration).toFloat()
|
||||
} else {
|
||||
(1 - (progress - 0.75 * duration) / 0.25 / duration).toFloat()
|
||||
}
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
fun shine(interval: Long) {
|
||||
progress += interval
|
||||
if (progress > duration + delay) {
|
||||
init()
|
||||
}
|
||||
computeFrame()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val viewWidth = canvasSizes[0]
|
||||
val viewHeight = canvasSizes[1]
|
||||
mRandom = Random()
|
||||
if (type == TYPE_FOG || type == TYPE_HAZE) {
|
||||
val cloudColors = if (type == TYPE_FOG) {
|
||||
if (daylight && !darkMode) {
|
||||
intArrayOf(
|
||||
"#93B9FF".toColorInt(), // -0x8e8260,
|
||||
"#93B9FF".toColorInt(), // -0x8e8260,
|
||||
"#93B9FF".toColorInt() // -0x8e8260
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
"#2A5476".toColorInt(), // Color.rgb(85, 99, 110),
|
||||
"#2A5476".toColorInt(), // Color.rgb(91, 104, 114),
|
||||
"#2A5476".toColorInt() // Color.rgb(99, 113, 123)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (daylight && !darkMode) {
|
||||
intArrayOf(
|
||||
"#FFDDA1".toColorInt(), // -0x53627e
|
||||
"#FFDDA1".toColorInt(), // -0x53627e
|
||||
"#FFDDA1".toColorInt() // -0x53627e
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
"#4D3314".toColorInt(), // Color.rgb(179, 158, 132)
|
||||
"#4D3314".toColorInt(), // Color.rgb(179, 158, 132)
|
||||
"#4D3314".toColorInt() // Color.rgb(179, 158, 132)
|
||||
)
|
||||
}
|
||||
}
|
||||
val cloudAlphas = floatArrayOf(0.3f, 0.3f, 0.3f)
|
||||
val clouds = arrayOf(
|
||||
Cloud(
|
||||
viewWidth * 1.0699f,
|
||||
viewWidth * (1.1900f * 0.2286f + 0.11f),
|
||||
viewWidth * (0.4694f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
9000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.4866f,
|
||||
viewWidth * (0.4866f * 0.6064f + 0.085f),
|
||||
viewWidth * (0.3946f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
10500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.0351f,
|
||||
viewWidth * (0.1701f * 1.4327f + 0.11f),
|
||||
viewWidth * (0.4627f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
9000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.8831f,
|
||||
viewWidth * (1.0270f * 0.1671f + 0.07f),
|
||||
viewWidth * (0.3238f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
7000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.4663f,
|
||||
viewWidth * (0.4663f * 0.3520f + 0.050f),
|
||||
viewWidth * (0.2906f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
8500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.1229f,
|
||||
viewWidth * (0.0234f * 5.7648f + 0.07f),
|
||||
viewWidth * (0.2972f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
7000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.9250f,
|
||||
viewWidth * (0.9250f * 0.0249f + 0.1500f),
|
||||
viewWidth * 0.3166f,
|
||||
1.15f,
|
||||
getRandomFactor(1.8f, 2.2f),
|
||||
cloudColors[2],
|
||||
cloudAlphas[2],
|
||||
7000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.4694f,
|
||||
viewWidth * (0.4694f * 0.0489f + 0.1500f),
|
||||
viewWidth * 0.3166f,
|
||||
1.15f,
|
||||
getRandomFactor(1.8f, 2.2f),
|
||||
cloudColors[2],
|
||||
cloudAlphas[2],
|
||||
8200,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.0250f,
|
||||
viewWidth * (0.0250f * 0.6820f + 0.1500f),
|
||||
viewWidth * 0.3166f,
|
||||
1.15f,
|
||||
getRandomFactor(1.8f, 2.2f),
|
||||
cloudColors[2],
|
||||
cloudAlphas[2],
|
||||
7700,
|
||||
0
|
||||
)
|
||||
)
|
||||
initialize(clouds)
|
||||
} else if (type == TYPE_CLOUDY || type == TYPE_THUNDER) {
|
||||
var cloudColors = IntArray(2)
|
||||
var cloudAlphas = FloatArray(2)
|
||||
when (type) {
|
||||
TYPE_CLOUDY -> {
|
||||
cloudColors = if (daylight && !darkMode) {
|
||||
intArrayOf(
|
||||
Color.rgb(160, 179, 191),
|
||||
Color.rgb(160, 179, 191)
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
Color.rgb(95, 104, 108),
|
||||
Color.rgb(95, 104, 108)
|
||||
)
|
||||
}
|
||||
cloudAlphas = floatArrayOf(0.3f, 0.3f)
|
||||
}
|
||||
TYPE_THUNDER -> {
|
||||
cloudColors = if (daylight && !darkMode) {
|
||||
intArrayOf(
|
||||
"#AB90DB".toColorInt(), // -0x43523f,
|
||||
"#AB90DB".toColorInt() // -0x43523f
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
"#2C1C4D".toColorInt(), // Color.rgb(43, 30, 66),
|
||||
"#2C1C4D".toColorInt() // Color.rgb(43, 30, 66)
|
||||
)
|
||||
}
|
||||
cloudAlphas = floatArrayOf(0.3f, 0.3f)
|
||||
// cloudAlphas = if (daylight && !darkMode) floatArrayOf(0.2f, 0.3f) else floatArrayOf(0.8f, 0.8f)
|
||||
}
|
||||
}
|
||||
val clouds = arrayOf(
|
||||
Cloud(
|
||||
viewWidth * 1.0699f,
|
||||
viewWidth * (1.1900f * 0.2286f + 0.11f),
|
||||
viewWidth * (0.4694f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
9000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.4866f,
|
||||
viewWidth * (0.4866f * 0.6064f + 0.085f),
|
||||
viewWidth * (0.3946f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
10500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.0351f,
|
||||
viewWidth * (0.1701f * 1.4327f + 0.11f),
|
||||
viewWidth * (0.4627f * 0.9f),
|
||||
1.1f,
|
||||
getRandomFactor(1.3f, 1.8f),
|
||||
cloudColors[0],
|
||||
cloudAlphas[0],
|
||||
9000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.8831f,
|
||||
viewWidth * (1.0270f * 0.1671f + 0.07f),
|
||||
viewWidth * (0.3238f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
7000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.4663f,
|
||||
viewWidth * (0.4663f * 0.3520f + 0.050f),
|
||||
viewWidth * (0.2906f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
8500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
viewWidth * 0.1229f,
|
||||
viewWidth * (0.0234f * 5.7648f + 0.07f),
|
||||
viewWidth * (0.2972f * 0.9f),
|
||||
1.15f,
|
||||
getRandomFactor(1.6f, 2f),
|
||||
cloudColors[1],
|
||||
cloudAlphas[1],
|
||||
7000,
|
||||
0
|
||||
)
|
||||
)
|
||||
initialize(clouds)
|
||||
} else {
|
||||
val cloudColor = if (daylight && !darkMode) {
|
||||
Color.rgb(203, 245, 255)
|
||||
} else {
|
||||
Color.rgb(151, 168, 202)
|
||||
}
|
||||
val cloudAlphas: FloatArray = floatArrayOf(0.40f, 0.10f)
|
||||
val clouds = arrayOf(
|
||||
Cloud(
|
||||
(viewWidth * 0.1529).toFloat(),
|
||||
(viewWidth * 0.1529 * 0.5568 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.2649).toFloat(),
|
||||
1.20f,
|
||||
getRandomFactor(1.5f, 1.8f),
|
||||
cloudColor,
|
||||
cloudAlphas[0],
|
||||
7000,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
(viewWidth * 0.4793).toFloat(),
|
||||
(viewWidth * 0.4793 * 0.2185 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.2426).toFloat(),
|
||||
1.20f,
|
||||
getRandomFactor(1.5f, 1.8f),
|
||||
cloudColor,
|
||||
cloudAlphas[0],
|
||||
8500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
(viewWidth * 0.8531).toFloat(),
|
||||
(viewWidth * 0.8531 * 0.1286 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.2970).toFloat(),
|
||||
1.20f,
|
||||
getRandomFactor(1.5f, 1.8f),
|
||||
cloudColor,
|
||||
cloudAlphas[0],
|
||||
7050,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
(viewWidth * 0.0551).toFloat(),
|
||||
(viewWidth * 0.0551 * 2.8600 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.4125).toFloat(),
|
||||
1.15f,
|
||||
getRandomFactor(1.3f, 1.5f),
|
||||
cloudColor,
|
||||
cloudAlphas[1],
|
||||
9500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
(viewWidth * 0.4928).toFloat(),
|
||||
(viewWidth * 0.4928 * 0.3897 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.3521).toFloat(),
|
||||
1.15f,
|
||||
getRandomFactor(1.3f, 1.5f),
|
||||
cloudColor,
|
||||
cloudAlphas[1],
|
||||
10500,
|
||||
0
|
||||
),
|
||||
Cloud(
|
||||
(viewWidth * 1.0499).toFloat(),
|
||||
(viewWidth * 1.0499 * 0.1875 + viewWidth * 0.050).toFloat(),
|
||||
(viewWidth * 0.4186).toFloat(),
|
||||
1.15f,
|
||||
getRandomFactor(1.3f, 1.5f),
|
||||
cloudColor,
|
||||
cloudAlphas[1],
|
||||
9000,
|
||||
0
|
||||
)
|
||||
)
|
||||
if (daylight) {
|
||||
initialize(clouds)
|
||||
} else {
|
||||
val colors = intArrayOf(
|
||||
Color.rgb(210, 247, 255),
|
||||
Color.rgb(208, 233, 255),
|
||||
Color.rgb(175, 201, 228),
|
||||
Color.rgb(164, 194, 220),
|
||||
Color.rgb(97, 171, 220),
|
||||
Color.rgb(74, 141, 193),
|
||||
Color.rgb(54, 66, 119),
|
||||
Color.rgb(34, 48, 74),
|
||||
Color.rgb(236, 234, 213),
|
||||
Color.rgb(240, 220, 151)
|
||||
)
|
||||
val r = Random()
|
||||
val canvasSize = sqrt(viewWidth.toDouble().pow(2.0) + viewHeight.toDouble().pow(2.0)).toInt()
|
||||
val width = (1.0 * canvasSize).toInt()
|
||||
val height = ((canvasSize - viewHeight) * 0.5 + viewWidth * 1.1111).toInt()
|
||||
val radius = (0.00125 * canvasSize * (0.5 + r.nextFloat())).toFloat()
|
||||
val stars = Array(50) { i ->
|
||||
val x = (r.nextInt(width) - 0.5 * (canvasSize - viewWidth)).toInt()
|
||||
val y = (r.nextInt(height) - 0.5 * (canvasSize - viewHeight)).toInt()
|
||||
val duration = (2500 + r.nextFloat() * 2500).toLong()
|
||||
Star(
|
||||
x.toFloat(),
|
||||
y.toFloat(),
|
||||
radius,
|
||||
colors[i % colors.size],
|
||||
duration,
|
||||
mAnimate
|
||||
)
|
||||
}
|
||||
initialize(clouds, stars)
|
||||
}
|
||||
}
|
||||
mThunder = if (type == TYPE_THUNDER) Thunder() else null
|
||||
}
|
||||
|
||||
private fun initialize(clouds: Array<Cloud>, stars: Array<Star> = emptyArray()) {
|
||||
mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
mClouds = clouds
|
||||
mStars = stars
|
||||
}
|
||||
|
||||
private fun getRandomFactor(from: Float, to: Float): Float {
|
||||
return from + mRandom.nextFloat() % (to - from)
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (c in mClouds) {
|
||||
c.move(interval, rotation2D, rotation3D)
|
||||
}
|
||||
for (s in mStars) {
|
||||
s.shine(interval)
|
||||
}
|
||||
mThunder?.shine(interval)
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
if (scrollRate < 1) {
|
||||
mThunder?.let {
|
||||
canvas.drawColor(
|
||||
Color.argb(
|
||||
((1 - scrollRate) * it.alpha * 255 * 0.66).toInt(),
|
||||
it.r,
|
||||
it.g,
|
||||
it.b
|
||||
)
|
||||
)
|
||||
}
|
||||
for (s in mStars) {
|
||||
mPaint.color = s.color
|
||||
mPaint.alpha = ((1 - scrollRate) * s.alpha * 255).toInt()
|
||||
canvas.drawCircle(s.centerX, s.centerY, s.radius, mPaint)
|
||||
}
|
||||
for (c in mClouds) {
|
||||
mPaint.color = c.color
|
||||
mPaint.alpha = ((1 - scrollRate) * c.alpha * 255).toInt()
|
||||
canvas.drawCircle(c.centerX, c.centerY, c.radius, mPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_CLOUD = 1
|
||||
const val TYPE_CLOUDY = 3
|
||||
const val TYPE_THUNDER = 5
|
||||
const val TYPE_FOG = 6
|
||||
const val TYPE_HAZE = 7
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColor(@TypeRule type: Int, daylight: Boolean): Int {
|
||||
return when (type) {
|
||||
TYPE_CLOUDY -> if (daylight) -0x62503f else -0xd9cdc8
|
||||
TYPE_THUNDER -> if (daylight) -0x4d6943 else -0xdce8c7
|
||||
TYPE_FOG -> if (daylight) -0x5c513e else -0xb0a298
|
||||
TYPE_HAZE -> if (daylight) -0x1e3767 else -0x93a3b7
|
||||
// TYPE_CLOUD:
|
||||
else -> if (daylight) -0xff5a27 else -0xddd2bd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Hail implementor.
|
||||
*/
|
||||
class HailImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
daylight: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val mHails: Array<Hail>
|
||||
private var mLastRotation3D: Float
|
||||
|
||||
private class Hail(
|
||||
private val mViewWidth: Int,
|
||||
private val mViewHeight: Int,
|
||||
@field:ColorInt @param:ColorInt val color: Int,
|
||||
val scale: Float,
|
||||
) {
|
||||
var cx = 0f
|
||||
var cy = 0f
|
||||
var centerX = 0f
|
||||
var centerY = 0f
|
||||
var size: Float
|
||||
var rotation = 0f
|
||||
var speedY: Float
|
||||
var speedX = 0f
|
||||
var speedRotation = 0f
|
||||
var rectF = RectF()
|
||||
private val mCanvasSize: Int
|
||||
|
||||
init {
|
||||
mCanvasSize = (mViewWidth * mViewWidth + mViewHeight * mViewHeight).toDouble().pow(0.5).toInt()
|
||||
size = (0.0324 * min(mViewWidth, mViewHeight)).toFloat() * 0.8f
|
||||
speedY = min(mViewWidth, mViewHeight) / 150f
|
||||
init(true)
|
||||
}
|
||||
|
||||
private fun init(firstTime: Boolean) {
|
||||
val r = Random()
|
||||
cx = r.nextInt(mCanvasSize).toFloat()
|
||||
cy = if (firstTime) {
|
||||
(r.nextInt((mCanvasSize - size).toInt()) - mCanvasSize).toFloat()
|
||||
} else {
|
||||
-size
|
||||
}
|
||||
rotation = 360 * r.nextFloat()
|
||||
speedRotation = 360f / 500f * r.nextFloat()
|
||||
speedX = 0.75f * (r.nextFloat() * speedY * if (r.nextBoolean()) 1 else -1)
|
||||
computeCenterPosition()
|
||||
}
|
||||
|
||||
private fun computeCenterPosition() {
|
||||
centerX = (cx - (mCanvasSize - mViewWidth) * 0.5).toFloat()
|
||||
centerY = (cy - (mCanvasSize - mViewHeight) * 0.5).toFloat()
|
||||
}
|
||||
|
||||
fun move(interval: Long, deltaRotation3D: Float) {
|
||||
cx += (speedX * interval * scale.toDouble().pow(1.5)).toFloat()
|
||||
cy +=
|
||||
(speedY * interval * (scale.toDouble().pow(1.5) - 5 * sin(deltaRotation3D * Math.PI / 180.0))).toFloat()
|
||||
rotation = (rotation + speedRotation * interval) % 360
|
||||
if (cy - size >= mCanvasSize) {
|
||||
init(false)
|
||||
} else {
|
||||
computeCenterPosition()
|
||||
}
|
||||
rectF.set(cx - size * scale, cy - size * scale, cx + size * scale, cy + size * scale)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val colors: IntArray = if (daylight) {
|
||||
intArrayOf(
|
||||
Color.rgb(128, 197, 255),
|
||||
Color.rgb(185, 222, 255),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
Color.rgb(40, 102, 155),
|
||||
Color.rgb(99, 144, 182),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
}
|
||||
val scales = floatArrayOf(0.6f, 0.8f, 1f)
|
||||
mHails = Array(HAIL_COUNT) { i ->
|
||||
Hail(
|
||||
canvasSizes[0],
|
||||
canvasSizes[1],
|
||||
colors[i * 3 / HAIL_COUNT],
|
||||
scales[i * 3 / HAIL_COUNT]
|
||||
)
|
||||
}
|
||||
mLastRotation3D = INITIAL_ROTATION_3D
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (h in mHails) {
|
||||
h.move(interval, if (mLastRotation3D == INITIAL_ROTATION_3D) 0f else rotation3D - mLastRotation3D)
|
||||
}
|
||||
mLastRotation3D = rotation3D
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
if (scrollRate < 1) {
|
||||
canvas.rotate(
|
||||
rotation2D,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (h in mHails) {
|
||||
mPaint.color = h.color
|
||||
mPaint.alpha = ((1 - scrollRate) * 255).toInt()
|
||||
canvas.rotate(h.rotation, h.cx, h.cy)
|
||||
canvas.drawRect(h.rectF, mPaint)
|
||||
canvas.rotate(-h.rotation, h.cx, h.cy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_ROTATION_3D = 1000f
|
||||
private const val HAIL_COUNT = 51
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColor(daylight: Boolean): Int {
|
||||
return if (daylight) -0x974501 else -0xe5a46e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Meteor shower implementor.
|
||||
*/
|
||||
class MeteorShowerImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val mMeteors: Array<Meteor>
|
||||
private val mStars: Array<Star>
|
||||
private var mLastRotation3D: Float
|
||||
|
||||
private class Meteor(
|
||||
private val mViewWidth: Int,
|
||||
private val mViewHeight: Int,
|
||||
@ColorInt val color: Int,
|
||||
val scale: Float,
|
||||
) {
|
||||
var x = 0f
|
||||
var y = 0f
|
||||
var width: Float
|
||||
var height = 0f
|
||||
var rectF: RectF = RectF()
|
||||
var speed: Float
|
||||
private var progress: Long = 0
|
||||
private var delay: Long = 0
|
||||
|
||||
private val random: Random = Random()
|
||||
|
||||
private val mCanvasSize: Int
|
||||
private val maxHeight: Float
|
||||
private val minHeight: Float
|
||||
|
||||
init { // 1, 0.7, 0.4
|
||||
mCanvasSize = (mViewWidth * mViewWidth + mViewHeight * mViewHeight).toDouble().pow(0.5).toInt()
|
||||
width = (mViewWidth * 0.0088 * scale).toFloat()
|
||||
speed = mViewWidth / 200f
|
||||
maxHeight = (1.1 * mViewWidth / cos(60.0 * Math.PI / 180.0)).toFloat()
|
||||
minHeight = (maxHeight * 0.7).toFloat()
|
||||
|
||||
init(true)
|
||||
}
|
||||
|
||||
private fun init(firstTime: Boolean) {
|
||||
progress = 0
|
||||
delay = (random.nextInt(METEOR_REVIVE_SEC_MAX - METEOR_REVIVE_SEC_MIN) + METEOR_REVIVE_SEC_MIN)
|
||||
.seconds.inWholeMilliseconds
|
||||
|
||||
x = random.nextInt(mCanvasSize).toFloat()
|
||||
y = if (!firstTime) {
|
||||
random.nextInt(mCanvasSize) - maxHeight - mCanvasSize
|
||||
} else {
|
||||
// prevents spawning all at once
|
||||
mCanvasSize.toFloat() * 2
|
||||
}
|
||||
|
||||
height = minHeight + random.nextFloat() * (maxHeight - minHeight)
|
||||
buildRectF()
|
||||
}
|
||||
|
||||
private fun buildRectF() {
|
||||
val x = (x - (mCanvasSize - mViewWidth) * 0.5).toFloat()
|
||||
val y = (y - (mCanvasSize - mViewHeight) * 0.5).toFloat()
|
||||
rectF.set(x, y, x + width, y + height)
|
||||
}
|
||||
|
||||
fun update(interval: Long, deltaRotation3D: Float) {
|
||||
if (y > mCanvasSize) {
|
||||
progress += interval
|
||||
if (progress > delay) init(false)
|
||||
return
|
||||
}
|
||||
move(interval, deltaRotation3D)
|
||||
buildRectF()
|
||||
}
|
||||
|
||||
private fun move(interval: Long, deltaRotation3D: Float) {
|
||||
x -= (speed * interval * 5 * sin(deltaRotation3D * Math.PI / 180.0) * cos(60 * Math.PI / 180.0))
|
||||
.toFloat()
|
||||
y += speed
|
||||
.times(interval)
|
||||
.times(
|
||||
scale.toDouble().pow(0.5) - 5 * sin(deltaRotation3D * Math.PI / 180.0) * sin(60 * Math.PI / 180.0)
|
||||
).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
private class Star(
|
||||
var centerX: Float,
|
||||
var centerY: Float,
|
||||
radius: Float,
|
||||
@field:ColorInt @param:ColorInt var color: Int,
|
||||
var duration: Long,
|
||||
) {
|
||||
var radius: Float
|
||||
var alpha = 0f
|
||||
var progress: Long = 0
|
||||
|
||||
init {
|
||||
this.radius = (radius * (0.7 + 0.3 * Random().nextFloat())).toFloat()
|
||||
computeAlpha(duration, progress)
|
||||
}
|
||||
|
||||
fun shine(interval: Long) {
|
||||
progress = (progress + interval) % duration
|
||||
computeAlpha(duration, progress)
|
||||
}
|
||||
|
||||
private fun computeAlpha(duration: Long, progress: Long) {
|
||||
alpha = if (progress < 0.5 * duration) {
|
||||
(progress / 0.5 / duration).toFloat()
|
||||
} else {
|
||||
(1 - (progress - 0.5 * duration) / 0.5 / duration).toFloat()
|
||||
}
|
||||
alpha = alpha * 0.66f + 0.33f
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val random = Random()
|
||||
val viewWidth = canvasSizes[0]
|
||||
val viewHeight = canvasSizes[1]
|
||||
val colors = intArrayOf(
|
||||
Color.rgb(210, 247, 255),
|
||||
Color.rgb(208, 233, 255),
|
||||
Color.rgb(175, 201, 228),
|
||||
Color.rgb(164, 194, 220),
|
||||
Color.rgb(97, 171, 220),
|
||||
Color.rgb(74, 141, 193),
|
||||
Color.rgb(54, 66, 119),
|
||||
Color.rgb(34, 48, 74),
|
||||
Color.rgb(236, 234, 213),
|
||||
Color.rgb(240, 220, 151)
|
||||
)
|
||||
|
||||
mMeteors = Array(if (mAnimate) 10 else 0) {
|
||||
Meteor(
|
||||
viewWidth,
|
||||
viewHeight,
|
||||
colors[random.nextInt(colors.size)],
|
||||
random.nextFloat()
|
||||
)
|
||||
}
|
||||
val canvasSize = (viewWidth.toDouble().pow(2.0) + viewHeight.toDouble().pow(2.0)).pow(0.5).toInt()
|
||||
val width = (1.0 * canvasSize).toInt()
|
||||
val height = ((canvasSize - viewHeight) * 0.5 + viewWidth * 1.1111).toInt()
|
||||
val radius = (0.00125 * canvasSize * (0.5 + random.nextFloat())).toFloat()
|
||||
mStars = Array(70) { i ->
|
||||
val x = (random.nextInt(width) - 0.5 * (canvasSize - viewWidth)).toInt()
|
||||
val y = (random.nextInt(height) - 0.5 * (canvasSize - viewHeight)).toInt()
|
||||
val duration = (2500 + random.nextFloat() * 2500).toLong()
|
||||
Star(
|
||||
x.toFloat(),
|
||||
y.toFloat(),
|
||||
radius,
|
||||
colors[i % colors.size],
|
||||
duration
|
||||
)
|
||||
}
|
||||
mLastRotation3D = INITIAL_ROTATION_3D
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (m in mMeteors) {
|
||||
m.update(
|
||||
interval,
|
||||
if (mLastRotation3D == INITIAL_ROTATION_3D) 0f else rotation3D - mLastRotation3D
|
||||
)
|
||||
}
|
||||
for (s in mStars) {
|
||||
s.shine(interval)
|
||||
}
|
||||
mLastRotation3D = rotation3D
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
if (scrollRate < 1) {
|
||||
canvas.rotate(
|
||||
rotation2D,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (s in mStars) {
|
||||
mPaint.apply {
|
||||
color = s.color
|
||||
alpha = ((1 - scrollRate) * s.alpha * 255).toInt()
|
||||
strokeWidth = s.radius * 2
|
||||
}
|
||||
canvas.drawPoint(s.centerX, s.centerY, mPaint)
|
||||
}
|
||||
canvas.rotate(
|
||||
60f,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (m in mMeteors) {
|
||||
mPaint.apply {
|
||||
color = m.color
|
||||
strokeWidth = m.rectF.width()
|
||||
alpha = ((1 - scrollRate) * 255).toInt()
|
||||
}
|
||||
canvas.drawLine(
|
||||
m.rectF.centerX(),
|
||||
m.rectF.top,
|
||||
m.rectF.centerX(),
|
||||
m.rectF.bottom,
|
||||
mPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_ROTATION_3D = 1000f
|
||||
private const val METEOR_REVIVE_SEC_MIN = 5
|
||||
private const val METEOR_REVIVE_SEC_MAX = 25
|
||||
|
||||
@get:ColorInt
|
||||
val themeColor: Int
|
||||
get() = Color.rgb(20, 28, 44)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Rain implementor.
|
||||
*/
|
||||
class RainImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
@TypeRule type: Int,
|
||||
daylight: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val mRains: Array<Rain>
|
||||
private var mThunder: Thunder? = null
|
||||
private var mLastRotation3D: Float
|
||||
|
||||
@IntDef(TYPE_RAIN, TYPE_THUNDERSTORM, TYPE_SLEET)
|
||||
internal annotation class TypeRule
|
||||
private class Rain(
|
||||
private val mViewWidth: Int,
|
||||
private val mViewHeight: Int,
|
||||
@ColorInt val color: Int,
|
||||
val scale: Float,
|
||||
@TypeRule type: Int,
|
||||
) {
|
||||
var x = 0f
|
||||
var y = 0f
|
||||
var width = 0f
|
||||
var height = 0f
|
||||
var rectF: RectF = RectF()
|
||||
var speed: Float
|
||||
|
||||
private val mCanvasSize: Int
|
||||
private val maxWidth: Float
|
||||
private val minWidth: Float
|
||||
private val maxHeight: Float
|
||||
private val minHeight: Float
|
||||
|
||||
init {
|
||||
mCanvasSize = (mViewWidth * mViewWidth + mViewHeight * mViewHeight).toDouble().pow(0.5).toInt()
|
||||
val velocity = if (type == TYPE_SLEET) 3.0 else 5.0
|
||||
speed = (mCanvasSize / (1000.0 * (1.75 + Random().nextDouble())) * velocity).toFloat()
|
||||
maxWidth = ((if (type == TYPE_SLEET) 0.006 else 0.003) * mCanvasSize).toFloat()
|
||||
minWidth = ((if (type == TYPE_SLEET) 0.004 else 0.002) * mCanvasSize).toFloat()
|
||||
maxHeight = maxWidth * 10
|
||||
minHeight = minWidth * 6
|
||||
init(true)
|
||||
}
|
||||
|
||||
private fun init(firstTime: Boolean) {
|
||||
val r = Random()
|
||||
x = r.nextInt(mCanvasSize).toFloat()
|
||||
y = if (firstTime) {
|
||||
(r.nextInt((mCanvasSize - maxHeight).toInt()) - mCanvasSize).toFloat()
|
||||
} else {
|
||||
-maxHeight * (1 + 2 * r.nextFloat())
|
||||
}
|
||||
width = minWidth + r.nextFloat() * (maxWidth - minWidth)
|
||||
height = minHeight + r.nextFloat() * (maxHeight - minHeight)
|
||||
buildRectF()
|
||||
}
|
||||
|
||||
private fun buildRectF() {
|
||||
val x = (x - (mCanvasSize - mViewWidth) * 0.5).toFloat()
|
||||
val y = (y - (mCanvasSize - mViewHeight) * 0.5).toFloat()
|
||||
rectF.set(x, y, x + width * scale, y + height * scale)
|
||||
}
|
||||
|
||||
fun move(interval: Long, deltaRotation3D: Float) {
|
||||
y += speed
|
||||
.times(interval)
|
||||
.times(
|
||||
scale.toDouble().pow(1.5) - 5 * sin(deltaRotation3D * Math.PI / 180.0) * cos(8 * Math.PI / 180.0)
|
||||
).toFloat()
|
||||
x -= (speed * interval * 5 * sin(deltaRotation3D * Math.PI / 180.0) * sin(8 * Math.PI / 180.0))
|
||||
.toFloat()
|
||||
if (y >= mCanvasSize) {
|
||||
init(false)
|
||||
} else {
|
||||
buildRectF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Thunder {
|
||||
var r = 81
|
||||
var g = 67
|
||||
var b = 168
|
||||
var alpha = 0f
|
||||
private var progress: Long = 0
|
||||
private var duration: Long = 0
|
||||
private var delay: Long = 0
|
||||
|
||||
init {
|
||||
init()
|
||||
computeFrame()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
progress = 0
|
||||
duration = 300
|
||||
delay = (Random().nextInt(5000) + 3000).toLong()
|
||||
}
|
||||
|
||||
private fun computeFrame() {
|
||||
alpha = if (progress < duration) {
|
||||
if (progress < 0.25 * duration) {
|
||||
(progress / 0.25 / duration).toFloat()
|
||||
} else if (progress < 0.5 * duration) {
|
||||
(1 - (progress - 0.25 * duration) / 0.25 / duration).toFloat()
|
||||
} else if (progress < 0.75 * duration) {
|
||||
((progress - 0.5 * duration) / 0.25 / duration).toFloat()
|
||||
} else {
|
||||
(1 - (progress - 0.75 * duration) / 0.25 / duration).toFloat()
|
||||
}
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
fun shine(interval: Long) {
|
||||
progress += interval
|
||||
if (progress > duration + delay) {
|
||||
init()
|
||||
}
|
||||
computeFrame()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (mAnimate) {
|
||||
var colors = IntArray(3)
|
||||
var rainCount = RAIN_COUNT
|
||||
when (type) {
|
||||
TYPE_RAIN -> if (daylight) {
|
||||
rainCount = RAIN_COUNT
|
||||
mThunder = null
|
||||
colors = intArrayOf(
|
||||
Color.rgb(223, 179, 114),
|
||||
Color.rgb(152, 175, 222),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
} else {
|
||||
rainCount = RAIN_COUNT
|
||||
mThunder = null
|
||||
colors = intArrayOf(
|
||||
Color.rgb(182, 142, 82),
|
||||
Color.rgb(88, 92, 113),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
}
|
||||
|
||||
TYPE_THUNDERSTORM -> if (daylight) {
|
||||
rainCount = RAIN_COUNT
|
||||
mThunder = Thunder()
|
||||
colors = intArrayOf(
|
||||
Color.rgb(182, 142, 82),
|
||||
-0x93aa6e,
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
} else {
|
||||
rainCount = RAIN_COUNT
|
||||
mThunder = Thunder()
|
||||
colors = intArrayOf(
|
||||
Color.rgb(182, 142, 82),
|
||||
Color.rgb(88, 92, 113),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
}
|
||||
|
||||
TYPE_SLEET -> if (daylight) {
|
||||
rainCount = SLEET_COUNT
|
||||
mThunder = null
|
||||
colors = intArrayOf(
|
||||
Color.rgb(128, 197, 255),
|
||||
Color.rgb(185, 222, 255),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
} else {
|
||||
rainCount = SLEET_COUNT
|
||||
mThunder = null
|
||||
colors = intArrayOf(
|
||||
Color.rgb(40, 102, 155),
|
||||
Color.rgb(99, 144, 182),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
}
|
||||
}
|
||||
val scales = floatArrayOf(0.6f, 0.8f, 1f)
|
||||
mRains = Array(rainCount) { i ->
|
||||
Rain(
|
||||
canvasSizes[0],
|
||||
canvasSizes[1],
|
||||
colors[i * 3 / rainCount],
|
||||
scales[i * 3 / rainCount],
|
||||
type
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mRains = emptyArray()
|
||||
}
|
||||
mLastRotation3D = INITIAL_ROTATION_3D
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
// do not display any rain effects if animations are turned off
|
||||
if (!mAnimate) return
|
||||
|
||||
for (r in mRains) {
|
||||
r.move(
|
||||
interval,
|
||||
if (mLastRotation3D == INITIAL_ROTATION_3D) 0f else rotation3D - mLastRotation3D
|
||||
)
|
||||
}
|
||||
mThunder?.shine(interval)
|
||||
mLastRotation3D = rotation3D
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
var rotation2Dc = rotation2D
|
||||
if (scrollRate < 1) {
|
||||
rotation2Dc += 8f
|
||||
canvas.rotate(
|
||||
rotation2Dc,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (r in mRains) {
|
||||
mPaint.color = r.color
|
||||
mPaint.alpha = ((1 - scrollRate) * 255).toInt()
|
||||
canvas.drawRoundRect(r.rectF, r.width / 2f, r.width / 2f, mPaint)
|
||||
}
|
||||
mThunder?.let {
|
||||
canvas.drawColor(
|
||||
Color.argb(
|
||||
((1 - scrollRate) * it.alpha * 255 * 0.66).toInt(),
|
||||
it.r,
|
||||
it.g,
|
||||
it.b
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_ROTATION_3D = 1000f
|
||||
const val TYPE_RAIN = 1
|
||||
const val TYPE_THUNDERSTORM = 3
|
||||
const val TYPE_SLEET = 4
|
||||
private const val RAIN_COUNT = 75
|
||||
private const val SLEET_COUNT = 45
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColor(@TypeRule type: Int, daylight: Boolean): Int {
|
||||
return when (type) {
|
||||
TYPE_SLEET -> if (daylight) -0x974501 else -0xe5a46e
|
||||
TYPE_THUNDERSTORM -> if (daylight) -0x4d6943 else -0xdce8c7
|
||||
// TYPE_RAIN:
|
||||
else -> if (daylight) -0xbd6819 else -0xd9b171
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Snow implementor.
|
||||
*/
|
||||
class SnowImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
daylight: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val mSnows: Array<Snow>
|
||||
private var mLastRotation3D: Float
|
||||
|
||||
private class Snow(
|
||||
private val mViewWidth: Int,
|
||||
private val mViewHeight: Int,
|
||||
@field:ColorInt @param:ColorInt val color: Int,
|
||||
val scale: Float,
|
||||
) {
|
||||
private var mCX = 0f
|
||||
private var mCY = 0f
|
||||
var centerX = 0f
|
||||
var centerY = 0f
|
||||
var radius: Float
|
||||
var speedX = 0f
|
||||
var speedY: Float
|
||||
private val mCanvasSize: Int
|
||||
|
||||
init {
|
||||
mCanvasSize = (mViewWidth * mViewWidth + mViewHeight * mViewHeight).toDouble().pow(0.5).toInt()
|
||||
radius = (mCanvasSize * (0.005 + Random().nextDouble() * 0.007) * scale).toFloat()
|
||||
speedY = (mCanvasSize / (1000.0 * (2.5 + Random().nextDouble())) * SNOW_SPEED).toFloat()
|
||||
init(true)
|
||||
}
|
||||
|
||||
private fun init(firstTime: Boolean) {
|
||||
val r = Random()
|
||||
mCX = r.nextInt(mCanvasSize).toFloat()
|
||||
mCY = if (firstTime) {
|
||||
(r.nextInt((mCanvasSize - radius).toInt()) - mCanvasSize).toFloat()
|
||||
} else {
|
||||
-radius
|
||||
}
|
||||
speedX = r.nextInt((2 * speedY).toInt()) - speedY
|
||||
computeCenterPosition()
|
||||
}
|
||||
|
||||
private fun computeCenterPosition() {
|
||||
centerX = (mCX - (mCanvasSize - mViewWidth) * 0.5).toInt().toFloat()
|
||||
centerY = (mCY - (mCanvasSize - mViewHeight) * 0.5).toInt().toFloat()
|
||||
}
|
||||
|
||||
fun move(interval: Long, deltaRotation3D: Float) {
|
||||
mCX += (speedX * interval * scale.toDouble().pow(1.5)).toFloat()
|
||||
mCY +=
|
||||
(speedY * interval * (scale.toDouble().pow(1.5) - 5 * sin(deltaRotation3D * Math.PI / 180.0))).toFloat()
|
||||
if (centerY >= mCanvasSize) {
|
||||
init(false)
|
||||
} else {
|
||||
computeCenterPosition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val colors = if (daylight) {
|
||||
intArrayOf(
|
||||
Color.rgb(190, 225, 255),
|
||||
Color.rgb(211, 233, 255),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
Color.rgb(111, 133, 155),
|
||||
Color.rgb(140, 161, 182),
|
||||
Color.rgb(255, 255, 255)
|
||||
)
|
||||
}
|
||||
val scales = floatArrayOf(0.6f, 0.8f, 1f)
|
||||
mSnows = Array(SNOW_COUNT) { i ->
|
||||
Snow(
|
||||
canvasSizes[0],
|
||||
canvasSizes[1],
|
||||
colors[i * 3 / SNOW_COUNT],
|
||||
scales[i * 3 / SNOW_COUNT]
|
||||
)
|
||||
}
|
||||
mLastRotation3D = INITIAL_ROTATION_3D
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (s in mSnows) {
|
||||
s.move(interval, if (mLastRotation3D == INITIAL_ROTATION_3D) 0f else rotation3D - mLastRotation3D)
|
||||
}
|
||||
mLastRotation3D = rotation3D
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
if (scrollRate < 1) {
|
||||
canvas.rotate(
|
||||
rotation2D,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (s in mSnows) {
|
||||
mPaint.color = s.color
|
||||
mPaint.alpha = ((1 - scrollRate) * 255).toInt()
|
||||
canvas.drawCircle(s.centerX, s.centerY, s.radius, mPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_ROTATION_3D = 1000f
|
||||
private const val SNOW_COUNT = 50
|
||||
private const val SNOW_SPEED = 1.5
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColor(daylight: Boolean): Int {
|
||||
return if (daylight) -0x974501 else -0xe5a46e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* Clear day implementor.
|
||||
*/
|
||||
class SunImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = Color.WHITE
|
||||
}
|
||||
private val mAngles = FloatArray(3)
|
||||
private val mUnitSizes: FloatArray = floatArrayOf(
|
||||
(0.5 * 0.47 * canvasSizes[0]).toFloat(),
|
||||
(1.7794 * 0.5 * 0.47 * canvasSizes[0]).toFloat(),
|
||||
(3.0594 * 0.5 * 0.47 * canvasSizes[0]).toFloat()
|
||||
)
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (i in mAngles.indices) {
|
||||
mAngles[i] = ((mAngles[i] + 90.0 / (3000 + 1000 * i) * interval) % 90).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
if (scrollRate < 1) {
|
||||
val deltaX = (sin(rotation2D * Math.PI / 180.0) * 0.3 * canvasSizes[0]).toFloat()
|
||||
val deltaY = (sin(rotation3D * Math.PI / 180.0) * -0.3 * canvasSizes[0]).toFloat()
|
||||
canvas.translate(canvasSizes[0] + deltaX, (SUN_POSITION * canvasSizes[0] + deltaY).toFloat())
|
||||
|
||||
arrayOf(SMALL_SUN_ALPHA, MEDIUM_SUN_ALPHA, LARGE_SUN_ALPHA).forEachIndexed { index, alpha ->
|
||||
mPaint.alpha = ((1 - scrollRate) * 255 * alpha).toInt()
|
||||
canvas.rotate(mAngles[index])
|
||||
for (i in 0..3) {
|
||||
canvas.drawRect(
|
||||
-mUnitSizes[index],
|
||||
-mUnitSizes[index],
|
||||
mUnitSizes[index],
|
||||
mUnitSizes[index],
|
||||
mPaint
|
||||
)
|
||||
canvas.rotate(22.5f)
|
||||
}
|
||||
canvas.rotate(-90 - mAngles[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SUN_POSITION = 0.5 // Old: 0.0333. Moved lower to avoid overlap with top icons
|
||||
const val SMALL_SUN_ALPHA = 0.16
|
||||
const val MEDIUM_SUN_ALPHA = 0.08
|
||||
const val LARGE_SUN_ALPHA = 0.04
|
||||
|
||||
@get:ColorInt
|
||||
val themeColor: Int
|
||||
get() = Color.rgb(253, 188, 76)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.ui.theme.weatherView.materialWeatherView.implementor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Size
|
||||
import androidx.core.graphics.toColorInt
|
||||
import org.breezyweather.ui.theme.weatherView.materialWeatherView.MaterialWeatherView.WeatherAnimationImplementor
|
||||
import java.util.Random
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
|
||||
class WindImplementor(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
animate: Boolean,
|
||||
daylight: Boolean,
|
||||
) : WeatherAnimationImplementor() {
|
||||
private val mAnimate = animate
|
||||
private val mPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
alpha = ((if (daylight) 1f else 0.33f) * 255).toInt()
|
||||
}
|
||||
private val mWinds: Array<Wind>
|
||||
private var mLastRotation3D: Float
|
||||
|
||||
private class Wind(
|
||||
private val mViewWidth: Int,
|
||||
private val mViewHeight: Int,
|
||||
@ColorInt val color: Int,
|
||||
val scale: Float,
|
||||
) {
|
||||
var x = 0f
|
||||
var y = 0f
|
||||
var width = 0f
|
||||
var height = 0f
|
||||
var rectF: RectF = RectF()
|
||||
var speed: Float
|
||||
|
||||
private val mCanvasSize: Int
|
||||
private val maxWidth: Float
|
||||
private val minWidth: Float
|
||||
private val maxHeight: Float
|
||||
private val minHeight: Float
|
||||
|
||||
init {
|
||||
mCanvasSize = (mViewWidth * mViewWidth + mViewHeight * mViewHeight).toDouble().pow(0.5).toInt()
|
||||
speed = (mCanvasSize / (1000.0 * (0.5 + Random().nextDouble())) * 6.0).toFloat()
|
||||
maxHeight = 0.007f * mCanvasSize
|
||||
minHeight = 0.005f * mCanvasSize
|
||||
maxWidth = maxHeight * 10
|
||||
minWidth = minHeight * 6
|
||||
init(true)
|
||||
}
|
||||
|
||||
private fun init(firstTime: Boolean) {
|
||||
val r = Random()
|
||||
y = r.nextInt(mCanvasSize).toFloat()
|
||||
x = if (firstTime) {
|
||||
(r.nextInt((mCanvasSize - maxHeight).toInt()) - mCanvasSize).toFloat()
|
||||
} else {
|
||||
-maxHeight
|
||||
}
|
||||
width = minWidth + r.nextFloat() * (maxWidth - minWidth)
|
||||
height = minHeight + r.nextFloat() * (maxHeight - minHeight)
|
||||
buildRectF()
|
||||
}
|
||||
|
||||
private fun buildRectF() {
|
||||
val x = (x - (mCanvasSize - mViewWidth) * 0.5).toFloat()
|
||||
val y = (y - (mCanvasSize - mViewHeight) * 0.5).toFloat()
|
||||
rectF.set(x, y, x + width * scale, y + height * scale)
|
||||
}
|
||||
|
||||
fun move(interval: Long, deltaRotation3D: Float) {
|
||||
x += speed
|
||||
.times(interval)
|
||||
.times(
|
||||
scale.toDouble().pow(1.5) + 5 * sin(deltaRotation3D * Math.PI / 180.0) * cos(16 * Math.PI / 180.0)
|
||||
).toFloat()
|
||||
y -= (speed * interval * 5 * sin(deltaRotation3D * Math.PI / 180.0) * sin(16 * Math.PI / 180.0))
|
||||
.toFloat()
|
||||
if (x >= mCanvasSize) {
|
||||
init(false)
|
||||
} else {
|
||||
buildRectF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val colors = if (daylight) {
|
||||
intArrayOf(
|
||||
"#C2E4CA".toColorInt(), // Color.rgb(240, 200, 148),
|
||||
"#B2E0BA".toColorInt(), // Color.rgb(237, 178, 100),
|
||||
"#D2F0DA".toColorInt() // Color.rgb(209, 142, 54)
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
"#313E3A".toColorInt(), // Color.rgb(240, 200, 148),
|
||||
"#529B73".toColorInt(), // Color.rgb(237, 178, 100),
|
||||
"#638170".toColorInt() // Color.rgb(209, 142, 54)
|
||||
)
|
||||
}
|
||||
val scales = floatArrayOf(0.6f, 0.8f, 1f)
|
||||
mWinds = Array(WIND_COUNT) { i ->
|
||||
Wind(
|
||||
canvasSizes[0],
|
||||
canvasSizes[1],
|
||||
colors[i * 3 / WIND_COUNT],
|
||||
scales[i * 3 / WIND_COUNT]
|
||||
)
|
||||
}
|
||||
mLastRotation3D = INITIAL_ROTATION_3D
|
||||
}
|
||||
|
||||
override fun updateData(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
interval: Long,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
for (w in mWinds) {
|
||||
w.move(interval, if (mLastRotation3D == INITIAL_ROTATION_3D) 0f else rotation3D - mLastRotation3D)
|
||||
}
|
||||
mLastRotation3D = rotation3D
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
@Size(2) canvasSizes: IntArray,
|
||||
canvas: Canvas,
|
||||
scrollRate: Float,
|
||||
rotation2D: Float,
|
||||
rotation3D: Float,
|
||||
) {
|
||||
var rotation2Dc = rotation2D
|
||||
if (scrollRate < 1) {
|
||||
rotation2Dc -= 16f
|
||||
canvas.rotate(
|
||||
rotation2D,
|
||||
canvasSizes[0] * 0.5f,
|
||||
canvasSizes[1] * 0.5f
|
||||
)
|
||||
for (w in mWinds) {
|
||||
mPaint.color = w.color
|
||||
mPaint.alpha = ((1 - scrollRate) * 255).toInt()
|
||||
canvas.drawRect(w.rectF, mPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_ROTATION_3D = 1000f
|
||||
private const val WIND_COUNT = 160
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColor(daylight: Boolean): Int {
|
||||
return if (daylight) -0x15325d else -0x6a798b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#592B19"
|
||||
android:centerColor="#CC6143"
|
||||
android:endColor="#D18268" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#23262C"
|
||||
android:centerColor="#525D66"
|
||||
android:endColor="#6B8394" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#131B45"
|
||||
android:centerColor="#2A5476"
|
||||
android:endColor="#918EAF" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#335A7E"
|
||||
android:centerColor="#435A6F"
|
||||
android:endColor="#303742" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#4D3314"
|
||||
android:centerColor="#755125"
|
||||
android:endColor="#C5AF9D" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#1C2F75"
|
||||
android:centerColor="#585D6D"
|
||||
android:endColor="#747B99" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#233361"
|
||||
android:centerColor="#2F4997"
|
||||
android:endColor="#252731" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#23306B"
|
||||
android:centerColor="#3549A4"
|
||||
android:endColor="#23262F" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#353A47"
|
||||
android:centerColor="#455762"
|
||||
android:endColor="#414F83" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#2F2B38"
|
||||
android:centerColor="#50406D"
|
||||
android:endColor="#2C1C4D" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#313E3A"
|
||||
android:centerColor="#529B73"
|
||||
android:endColor="#638170" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#FFBA5E"
|
||||
android:centerColor="#FFC67E"
|
||||
android:endColor="#FFF0E2" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#171D52"
|
||||
android:centerColor="#3F4DBA"
|
||||
android:endColor="#5E68BD" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#AFBCC7"
|
||||
android:centerColor="#D2D6DB"
|
||||
android:endColor="#EBEFF8" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<solid android:color="@android:color/transparent" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#A2C3FF"
|
||||
android:centerColor="#B3AFD1"
|
||||
android:endColor="#E7BEB0" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#DDF7FF"
|
||||
android:centerColor="#E1E5E9"
|
||||
android:endColor="#A0AFC1" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#FFDDA1"
|
||||
android:centerColor="#D1C3AF"
|
||||
android:endColor="#B5E8EF" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#64DBFF"
|
||||
android:centerColor="#D6DEE0"
|
||||
android:endColor="#E2F0F6" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#97B6D1"
|
||||
android:centerColor="#A6BBCE"
|
||||
android:endColor="#97A8D7" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#B4D3ED"
|
||||
android:centerColor="#C5D2DC"
|
||||
android:endColor="#ADC3DE" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#93A2AD"
|
||||
android:centerColor="#E3E7EB"
|
||||
android:endColor="#EDF2F6" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#C697D7"
|
||||
android:centerColor="#C1A3CE"
|
||||
android:endColor="#AB90DB" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle"
|
||||
android:visible="true">
|
||||
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#96D0A3"
|
||||
android:centerColor="#DFFAE7"
|
||||
android:endColor="#E8F4EE" />
|
||||
|
||||
</shape>
|
||||
Loading…
Add table
Add a link
Reference in a new issue