自定义View(几何变换)
Canvas 的范围裁切
- clipRect() 矩形裁切
- clipPath() 路径裁切
- clipOutRect() / clipOutPath() 相反方向裁切
private val IMAGE_SIZE = 120.dp
private val IMAGE_PADDING = 50.dp
class ClipView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val path = Path()
init {
path.addOval(
IMAGE_PADDING, IMAGE_PADDING,
IMAGE_PADDING + IMAGE_SIZE,
IMAGE_PADDING + IMAGE_SIZE,
Path.Direction.CW
)
}
override fun onDraw(canvas: Canvas) {
canvas.clipPath(path)
canvas.drawBitmap(getAvatar(IMAGE_SIZE.toInt()), IMAGE_PADDING, IMAGE_PADDING, paint)
}
private fun getAvatar(width: Int): Bitmap {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.avatar, options)
options.inJustDecodeBounds = false
options.inDensity = options.outWidth
options.inTargetDensity = width.toInt()
return BitmapFactory.decodeResource(resources, R.drawable.avatar, options)
}
}
Canvas 的几何变换
- translate(x ,y) 位移
- rotate(degree) 旋转
- scale(x, y) 缩放
- skew(x, y) 斜切
注意:Canvas 的几何变换方法参照的是 View 的坐标系,而绘制方法 drawXxx() 使用的是 Canvas 自己的坐标系。
Canvas 的多重变换
Canvas 的变换方法多次调用的时候,由于 Canvas 的坐标系会整体被变换,因此当平移、旋转、放缩、错切等变换多重存在的时候,Canvas 的变换参数会非常难以计算,因此可以改用倒叙的理解方式。
将 Canvas 的变换理解为 Canvas 的坐标系不变,每次变换是 只对内部的绘制内容进行变换,同时把 Canvas 的变换顺序看做是倒叙的(即 写在下面的变换先执行),可以更加方便进行多重变换的参数计算。
Matrix 的几何变换
- preTranslate(x, y) / postTranslate(x, y)
- preRotate(degree) / postRotate(degree)
- preScale(x, y) / postScale(x, y)
- preSkew(x, y) / postSkew(x, y)
其中 preXxx() 效果和 Canvas 的准同名方法相同,postXxx() 效果和 Canvas 的准同名方法顺序相反。
如果多次绘制是重复使用 Matrix,在使用之前需要用 Matrix.reset() 来把 Matrix 重置。
使用 Camera 做三维旋转
Camera 三维坐标系,向右为 x轴 正方向,向上为 y轴 正方向,向内为 z轴 正方向。
- rotate() / rotateX() / rotateY() / rotateZ() // 旋转
- translate() // 位移
- setLocation()// 设置虚拟相机的距离,虚拟机相机处于 z轴 的负方向上
一般只用 rotateX() 和 rorateY() 来做沿 x 轴 或 y 轴的旋转,以及使用 setLocation() 来调整放缩的视觉幅度。
对 Camera 变换之后,要用 Camera.applyToCanvas(Canvas) 来应用到 Canvas .
Camera .setLocation()
这个⽅法⼀般前两个参数都填 0,第三个参数为负值。由于这个值的单位是硬编码写死的,因此像素密度越⾼的⼿机,相当于 Camera 距离 View 越近,所以最好把这个值写成与机器的 density 成正⽐的⼀个负值,例如 -6 * density .
private val IMAGE_SIZE = 150.dp
private val IMAGE_PADDING = 100.dp
class CameraView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val bitmap = getAvatar(IMAGE_SIZE.toInt())
private val camera = Camera()
var topFlip = 0f
set(value) {
field = value
invalidate()
}
var bottomFlip = 30f
set(value) {
field = value
invalidate()
}
var flipRotation = 30f
set(value) {
field = value
invalidate()
}
init {
camera.setLocation(0f, 0f, -6 * resources.displayMetrics.density)
}
override fun onDraw(canvas: Canvas) {
// 上半部分
canvas.withSave {
canvas.translate((IMAGE_PADDING + IMAGE_SIZE / 2f), (IMAGE_PADDING + IMAGE_SIZE / 2f))
canvas.rotate(-flipRotation)
camera.save()
camera.rotateX(topFlip)
camera.applyToCanvas(canvas)
camera.restore()
canvas.clipRect(-IMAGE_SIZE, -IMAGE_SIZE, IMAGE_SIZE, 0f)
canvas.rotate(flipRotation)
canvas.translate(-(IMAGE_PADDING + IMAGE_SIZE / 2f), -(IMAGE_PADDING + IMAGE_SIZE / 2f))
canvas.drawBitmap(bitmap, IMAGE_PADDING, IMAGE_PADDING, paint)
}
// 下半部分
canvas.withSave {
canvas.translate((IMAGE_PADDING + IMAGE_SIZE / 2f), (IMAGE_PADDING + IMAGE_SIZE / 2f))
canvas.rotate(-flipRotation)
camera.save()
camera.rotateX(bottomFlip)
camera.applyToCanvas(canvas)
camera.restore()
canvas.clipRect(-IMAGE_SIZE, 0f, IMAGE_SIZE, IMAGE_SIZE)
canvas.rotate(flipRotation)
canvas.translate(-(IMAGE_PADDING + IMAGE_SIZE / 2f), -(IMAGE_PADDING + IMAGE_SIZE / 2f))
canvas.drawBitmap(bitmap, IMAGE_PADDING, IMAGE_PADDING, paint)
}
}
}
属性动画
ViewPropertyAnimator
使用 View.animate() 创建对象,以及使用 ViewPropertyAnimator.translationX() 等方法设置动画。
ObjectAnimator
使用 ObjectAnimator.ofXxx() 创建对象,以及使用 ObjectAnimator.start() 来主动启动动画,它的优势在于,可以为自定义属性设置动画。
// 为自定义的CircleView添加动画
val animator = ObjectAnimator.ofFloat(view, "radius", 100.dp)
animator.startDelay = 1000
animator.duration = 500
animator.start()
// CircleView
class CircleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#ec5f66")
}
private var radius = 20.dp
set(value) {
field = value
invalidate()// 触发重绘
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f, height / 2f, radius, paint)
}
}
Interpolator
插值器,用于设置时间完成度到动画完成度的计算公式,即设置动画的速度曲线,通过 setInterpolator(Interpolator) 方法来设置。
常用的用 AccelerateDecelerateInterpolator(先加速再减速)、AccelerateInterpolator(加速)、DecelerateInterpolator(减速)、LinearInterpolator(匀速)。
PropertyValuesHolder
用于设置更加详细的动画,例如多个属性应用于同一个对象。
val bottomFlipHolder = PropertyValuesHolder.ofFloat("bottomFlip", 60f)
val flipRotationHolder = PropertyValuesHolder.ofFloat("flipRotation", 360f)
val topFlipHolder = PropertyValuesHolder.ofFloat("topFlip", -60f)
val holderAnimator = ObjectAnimator.ofPropertyValuesHolder(
view,
bottomFlipHolder,
flipRotationHolder,
topFlipHolder
)
holderAnimator.duration = 2000
holderAnimator.startDelay = 500
holderAnimator.start()
或者 配合使用 Keyframe ,对一个属性分多个段
val length = 200.dp
val keyframe1 = Keyframe.ofFloat(0f, 0f)
val keyframe2 = Keyframe.ofFloat(0.2f, 0.2f * length)
val keyframe3 = Keyframe.ofFloat(0.8f, 1.6f * length)
val keyframe4 = Keyframe.ofFloat(1.0f, 1.0f * length)
val keyframeHolder = PropertyValuesHolder.ofKeyframe(
"translationX",
keyframe1,
keyframe2,
keyframe3,
keyframe4
)
val animator = ObjectAnimator.ofPropertyValuesHolder(view, keyframeHolder)
animator.duration = 2000
animator.startDelay = 500
animator.repeatCount = Animation.INFINITE
animator.start()
AnimatorSet
将多个 Animator 合并在一起使用,先后执行或并列执行都可以
val animatorSet = AnimatorSet()
animatorSet.playSequentially(bottomFlipAnimator, flipRotationAnimator, topFlipAnimator)
animatorSet.start()
TypeEvaluator
用于设置动画完成度到属性具体值的计算公式。默认的 ofInt()、ofFloat() 使用的是自带的 IntEvaluator 和 FloatEvaluator。有时需要自己设置 Evaluator,例如对于颜色,需要为 int 类型 的颜色设置 ArgbEvaluator.
另外,对于不⽀持的类型,也可以使⽤ ofObject() 来在创建 Animator 的同时就设置上 Evaluator .
val animator = ObjectAnimator.ofObject(view, "point", PointFEvaluator(), PointF(200.dp, 300.dp))
animator.startDelay = 1000
animator.duration = 500
animator.start()
// PointFView
class PointFView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 20.dp
strokeCap = Paint.Cap.ROUND
color = Color.parseColor("#ec5f66")
}
var point = PointF(0f, 0f)
set(value) {
field = value
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawPoint(point.x, point.y, paint)
}
}
ValueAnimator
最基本的 Animator,它不和具体的某个对象联动,⽽是直接对两个数值进⾏渐变计算。
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.addUpdateListener {
Log.e("animator", "value: ${it.animatedValue}")
}
animator.duration = 1000
animator.start()