本文翻譯自Geometric Android Animations using the Canvas,不過很多與技術無關的段落沒有翻譯,只保留了技術部分。
1 創建圓形動畫
首先需要畫一些同心圓,并添加動畫將同心圓的半徑逐漸增加,即從同心圓中心向四周擴散的動畫。
需要定義一些屬性包括:同心圓間隔、圓線顏色、圓線寬度:
<attr name="wavesViewStyle" format="reference" />
<declare-styleable name="WavesView">
<attr name="waveColor" format="color" />
<attr name="waveStrokeWidth" format="dimension" />
<attr name="waveGap" format="dimension" />
</declare-styleable>
<style name="Widget.WaveView" parent="@android:style/Widget">
<item name="waveStrokeWidth">1dp</item>
<item name="waveColor">@color/black</item>
<item name="waveGap">16dp</item>
</style>
其次,需要定義一個layout布局和自定義View:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.android.waves.WavesView
style="@style/Widget.WaveView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
class WavesView
@JvmOverloads
constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.wavesViewStyle
) : View(context, attrs, defStyleAttr) {
private val wavePaint: Paint
private val waveGap: Float
private var maxRadius = 0f
private var center = PointF(0f, 0f)
private var initialRadius = 0f
init {
val attrs = context.obtainStyledAttributes(attrs, R.styleable.WavesView, defStyleAttr, 0)
//init paint with custom attrs
wavePaint = Paint(ANTI_ALIAS_FLAG).apply {
color = attrs.getColor(R.styleable.WavesView_waveColor, 0)
strokeWidth = attrs.getDimension(R.styleable.WavesView_waveStrokeWidth, 0f)
style = Paint.Style.STROKE
}
waveGap = attrs.getDimension(R.styleable.WavesView_waveGap, 50f)
attrs.recycle()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
//set the center of all circles to be center of the view
center.set(w / 2f, h / 2f)
maxRadius = Math.hypot(center.x.toDouble(), center.y.toDouble()).toFloat()
initialRadius = w / waveGap
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//draw circles separated by a space the size of waveGap
var currentRadius = initialRadius
while (currentRadius < maxRadius) {
canvas.drawCircle(center.x, center.y, currentRadius, wavePaint)
currentRadius += waveGap
}
}
}
在自定義的View中,我們做了:
- 根據自定義屬性初始化畫筆屬性
- 定義最小圓和最大圓半徑
- 定義開始畫圓的位置
- 從最小半徑到最大半徑畫同心圓,圓間隔是waveGap。
private var waveAnimator: ValueAnimator? = null
private var waveRadiusOffset = 0f
set(value) {
field = value
postInvalidateOnAnimation()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
waveAnimator = ValueAnimator.ofFloat(0f, waveGap).apply {
addUpdateListener {
waveRadiusOffset = it.animatedValue as Float
}
duration = 1500L
repeatMode = ValueAnimator.RESTART
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
start()
}
}
override fun onDetachedFromWindow() {
waveAnimator?.cancel()
super.onDetachedFromWindow()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//draw circles separated by a space the size of waveGap
var currentRadius = initialRadius + waveRadiusOffset
while (currentRadius < maxRadius) {
canvas.drawCircle(center.x, center.y, currentRadius, wavePaint)
currentRadius += waveGap
}
}
上面代碼創建一個運行1.5秒的屬性動畫,在無限循環中重復來完成的。在每個動畫幀上,更新waveRadiusOffset,我們在setter中調用postInvalidateOnAnimation()來重繪我們視圖的下一幀。 ·最后,onDraw使用新的偏移量運行以重繪。。
2 不僅僅是圓形
圓形其實也很美,但是有時候動畫需要特定的形狀,這里我來畫出一個十角星的形狀。
我們利用三角函數(還記得sohcahtoa嗎?)來繪制十角星。需要溫習高中知識的話可以看下這個文獻:https://en.wikipedia.org/wiki/Trigonometry#Mnemonics
繪制星星的邊的動圖:。
首先要計算出每個點的位置,需要每個點的弧度(或者角度)和到圓形的長度這兩個變量才能計算出其位置。由于圓的整個弧度是,那么每個點的弧度就是當前點的索引除以
:
private val wavePath = Path()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//draw circles separated by a space the size of waveGap
val path = createStarPath(width/2f, wavePath)
canvas.drawPath(path, wavePaint)
}
private fun createStarPath(
radius: Float,
path: Path = Path(),
points: Int = 20
): Path {
path.reset()
val pointDelta = 0.7f // difference between the "far" and "close" points from the center
val angleInRadians = 2.0 * Math.PI / points // essentially 360/20 or 18 degrees, angle each line should be drawn
val startAngleInRadians = 0.0 //starting to draw star at 0 degrees
//move pointer to 0 degrees relative to the center of the screen
path.moveTo(
center.x + (radius * pointDelta * Math.cos(startAngleInRadians)).toFloat(),
center.y + (radius * pointDelta * Math.sin(startAngleInRadians)).toFloat()
)
//create a line between all the points in the star
for (i in 1 until points) {
val hypotenuse = if (i % 2 == 0) {
//by reducing the distance from the circle every other points, we create the "dip" in the star
pointDelta * radius
} else {
radius
}
val nextPointX = center.x + (hypotenuse * Math.cos(startAngleInRadians - angleInRadians * i)).toFloat()
val nextPointY = center.y + (hypotenuse * Math.sin(startAngleInRadians - angleInRadians * i)).toFloat()
path.lineTo(nextPointX, nextPointY)
}
path.close()
return path
}
onDraw
方法也要更改下:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//draw circles separated by a space the size of waveGap
var currentRadius = initialRadius + waveRadiusOffset
while (currentRadius < maxRadius) {
val path = createStarPath(currentRadius, wavePath)
canvas.drawPath(path, wavePaint)
currentRadius += waveGap
}
}
上面的代碼實現的效果如圖:。
3 漸變色
加點漸變色總是很酷的,我們的漸變色是通過繪制畫筆加到波紋上的而不是加到背景上的。為了加強落到漸變色上的波紋,我們使用PorterDuff.Mode.SRC_IN模式,關于更多模式可以參考Android矢量圖(二)--VectorDrawable所有屬性全解析,這篇文章對PorterDuff的十八種模式進行了比較詳細的分析解釋。下面的代碼定義一個漸變色的畫筆:
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
private val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
// Highlight only the areas already touched on the canvas
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}
我們用一個alpha數組作為畫筆的shader的參數來實現漸變功能:
import android.graphics.Color
import android.graphics.RadialGradient
import android.graphics.Shader
// gradient colors
private val green = Color.GREEN
// solid green in the center, transparent green at the edges
private val gradientColors =
intArrayOf(green, modifyAlpha(green, 0.10f),
modifyAlpha(green, 0.05f))
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
...
//Create gradient after getting sizing information
gradientPaint.shader = RadialGradient(
center.x, center.y, maxRadius,
gradientColors, null, Shader.TileMode.CLAMP
)
}
override fun onDraw(canvas: Canvas) {
...
canvas.drawPaint(gradientPaint)
}
最后我們需要給自定義的View加上layerType的屬性,默認的layerType是0,需要改成1(software)或者2(hardware),否則漸變色不會有效果:
<com.example.android.waves.WavesView
style="@style/Widget.WaveView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layerType="software" />
效果:。
4 移動
我們可以通過移動畫筆的shader的局部矩陣來實現漸變色的移動。
private val gradientMatrix = Matrix()
private fun updateGradient(x: Float, y: Float) {
gradientMatrix.setTranslate(x - center.x, y - center.y)
gradientPaint.shader.setLocalMatrix(gradientMatrix)
postInvalidateOnAnimation()
}
現在我們利用加速度和磁感應器來獲取手機的方向,以決定移動畫筆shader矩陣的具體的值。關于感應器的更多的知識:sensors_position。WaveTiltSensor
這個類用來獲取手機設備的方向角度并做相應的邏輯處理。
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
interface TiltListener {
fun onTilt(pitchRollRad: Pair<Double, Double>)
}
interface TiltSensor {
fun addListener(tiltListener: TiltListener)
fun register()
fun unregister()
}
class WaveTiltSensor(context: Context) : SensorEventListener, TiltSensor {
private val sensorManager: SensorManager
private val accSensor: Sensor
private val magneticSensor: Sensor
private var listeners = mutableListOf<TiltListener>()
init {
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
accSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
magneticSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
// Do nothing here
}
override fun onSensorChanged(event: SensorEvent) {
// TODO: Get orientation angles and notify listeners
}
override fun addListener(tiltListener: TiltListener) {
listeners.add(tiltListener)
}
override fun register() {
sensorManager.registerListener(this, accSensor, SensorManager.SENSOR_DELAY_UI)
sensorManager.registerListener(this, magneticSensor, SensorManager.SENSOR_DELAY_UI)
}
override fun unregister() {
listeners.clear()
sensorManager.unregisterListener(this, accSensor)
sensorManager.unregisterListener(this, magneticSensor)
}
}
主要的邏輯是放在onSensorChanged
方法里:
class WaveTiltSensor(context: Context) : SensorEventListener, TiltSensor {
private val rotationMatrix = FloatArray(9)
private val accelerometerValues = FloatArray(3)
private val magneticValues = FloatArray(3)
private val orientationAngles = FloatArray(3)
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(event.values, 0, accelerometerValues, 0, accelerometerValues.size)
} else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(event.values, 0, magneticValues, 0, magneticValues.size)
}
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerValues, magneticValues)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val pitchInRad = orientationAngles[1].toDouble()
val rollInRad = orientationAngles[2].toDouble()
val pair = Pair(pitchInRad, rollInRad)
listeners.forEach {
it.onTilt(pair)
}
}
}
現在已經有了pitch(x軸的角度)和roll(y軸的角度)的弧度,只是2D的移動的話,不需要關心azimuth (z軸的角度)。 val tiltSensor = WaveTiltSensor(context)
override fun onTilt(pitchRollRad: Pair<Double, Double>) {
val pitchRad = pitchRollRad.first
val rollRad = pitchRollRad.second
// Use half view height/width to calculate offset instead of full view/device measurement
val maxYOffset = center.y.toDouble()
val maxXOffset = center.x.toDouble()
val yOffset = (Math.sin(pitchRad) * maxYOffset)
val xOffset = (Math.sin(rollRad) * maxXOffset)
updateGradient(xOffset.toFloat() + center.x, yOffset.toFloat() + center.y)
}
override fun onAttachedToWindow() {
tiltSensor.addListener(this)
tiltSensor.register()
}
override fun onDetachedFromWindow() {
tiltSensor.unregister()
}
5 總結
下面是可以運行的demo源碼的git地址,希望本篇文章能讓你覺得使用三角函數和陀螺儀制作動畫更簡單!