本文代码中使用的是kotlin语法 还没了解kotlin的请先看下语法篇
kotlin语法总结
绘制基础
绘制一个三角形
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
var paint = Paint()
paint.setColor(Color.RED) // 设置画笔颜色
paint.style = Paint.Style.STROKE // 设置画笔样式
paint.strokeWidth = 5f // 设置画笔宽度
// 绘制三角形
var path = Path()
path.moveTo(10f, 10f) // 移动起点到 10,10
path.lineTo(10f, 100f) // 画第一条直线
path.lineTo(300f, 100f) // 画第二条直线
path.close() // 首尾连接 形成第三条直线
canvas.drawPath(path, paint)
}
绘制圆弧
var path = Path()
path.moveTo(10f,10f) // 定义起点
var recf = RectF(10f,10f,200f,200f)
path.arcTo(recf,0f,90f) // 根据矩形 起始角度 绘制的角度 绘制圆弧 但是起点和圆弧起点会出现一条直线
canvas.drawPath(path, paint)
// canvas.drawRect(recf,paint) //查看矩形范围
var path2 = Path()
path2.moveTo(210f,10f)
var recf2 = RectF(210f,10f ,400f,200f)
path2.arcTo(recf2,0f,90f,true) //相比上面的函数这个不会生成path起点到圆弧起点的直线
canvas.drawPath(path2, paint)
区域 region
抽取绘制区域方法
fun drawRegion(canvas:Canvas , rgn:Region , paint:Paint){
var iter = RegionIterator(rgn)
var r = Rect()
while (iter.next(r)){
canvas.drawRect(r,paint)
}
}
区域和rect 相似 只不过区域可以是不规则的形状 rect则必须是矩形
var region = Region(Rect(50,150,200,200))
drawRegion(canvas,region,paint)
//区域 rect
canvas.drawRect(Rect(50,50,200,100),paint)
region的几种方法 | 描述 |
---|---|
setEmpty | 将原来的区域变量变成空变量在用set函数重新构造区域 |
set(Region) | 利用新的区域替换原来的区域 |
set(Recf) | 利用矩形所代表的的区域替换原来的区域 |
set(left , top , right , bottom) | 根据矩形的两个角点构造出的矩形区域替换原来的区域 |
setPath(path , Region) | 根据路径的区域与某区域的交集构造出新的区域 |
其他方法都比较简单 这里以setpath 做示例
取椭圆的上半部分
paint.setColor(Color.RED) // 设置画笔颜色
paint.style = Paint.Style.FILL // 设置画笔样式
paint.strokeWidth = 5f // 设置画笔宽度
var ovalPath = Path()
var rect = RectF(50f,50f,200f,500f)
ovalPath.addOval(rect,Path.Direction.CCW)
// canvas.drawPath(ovalPath,paint) // 先绘制一个椭圆
var clipRect = RectF(50f,50f,200f,200f)
// canvas.drawRect(clipRect,paint) //在绘制一个矩形
var rgn = Region()
rgn.setPath(ovalPath , Region(50,50,200,200))
drawRegion(canvas,rgn, paint)
区域相交 union函数
// 该函数用于与指定矩形取并集 即将rect所指定的矩形加入当前区域中
var region = Region(10,10,200,100)
region.union(Rect(10,10,50,300))
drawRegion(canvas,region,paint)
/**
* 区域操作
* op(Rect , op)
* op(left,top,right,bottom,op)
* op(Region , op)
*/
public enum Op
枚举 | 描述 |
---|---|
DIFFERENCE(0) | r1与r2不同的区域 |
INTERSECT(1) | r1与r2交集 |
UNION(2) | r1与r2组合在一起的区域 |
XOR(3) | r1与r2交集之外的区域 |
REVERSE_DIFFERENCE(4) | r1与r2不同的区域 |
REPLACE(5) | r2 的区域 |
canvas
// 画布平移translate
var rect = Rect(0,0,200,200)
canvas.drawRect(rect,paint)
canvas.translate(100f,100f)
canvas.drawRect(rect,paint)
//画布裁剪 clip
canvas.drawColor(Color.RED)
canvas.clipRect(Rect(100,100,200,200))
canvas.drawColor(Color.GREEN)
//画布的保存与恢复
// 先使用save 保存当前的状态 在用restore恢复到该状态
canvas.drawColor(Color.RED)
canvas.save()
canvas.clipRect(Rect(100,100,200,200))
canvas.drawColor(Color.GREEN)
canvas.restore()
canvas.drawColor(Color.BLUE)
动画
/**
* 视图动画由五种类型组成
* alpha 渐变透明度动画
* scale 渐变尺寸伸缩动画
* translate 画面变化位置移动动画
* rotate 画面移动旋转动画
* set 定义动画集合
*/
/**
* Animation 属性
* Animation 是所有动画的基类
* duration 动画的持续时间,以毫秒为单位
* fillAfter 如果设置为true 则控件动画结束时,将保持动画结束时的状态
* fillBefore 如果设置为true 则空话结束时,将还原到初始状态
* fillEnabled 与fillBefore 效果相同
* repeatCount 用于指定动画的重复次数 当值为infinite时 表示无限循环
* repeatMode 用于设定重复的类型 reverse表示倒序 restart表示重放
* interpolator 用于设定插值器 其实就是指定的动画效果 ,比如弹跳 匀加速 匀减速 效果等
*/
动画的定义一般以xml的形式 在res目录下创建anim文件夹
xml示例
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="0.1"
android:duration="3000"
android:fillAfter="true"/>
<!--分别为-->
<!--透明度开始的大小-->
<!--透明度结束的大小-->
<!--动画持续时间-->
<!--动画结束后保持状态-->
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="-600"
android:duration="3000"
/>
<!--分别为-->
<!--旋转开始的角度-->
<!--旋转结束的角度-->
<!--动画持续时间-->
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="0.0"
android:toXScale="1.4"
android:fromYScale="0.0"
android:toYScale="1.4"
android:duration="700"
android:pivotX="50%p"
android:pivotY="50%p"/>
<!-- 分别为 -->
<!-- x开始的大小-->
<!-- x结束的大小-->
<!-- y开始的大小-->
<!-- y结束的大小-->
<!-- 动画持续的时间-->
<!-- 动画开始的x位置-->
<!-- 动画开始的y位置-->
<!-- pivotX 的三种写法-->
<!-- 50 原点的位置加50-->
<!-- 50% 原点位置加控件的50%-->
<!-- 50%p 原点位置加上父控件的50%-->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="-80"
android:fromYDelta="0"
android:toYDelta="-80"
android:duration="2000"/>
<!--分别为-->
<!--x轴开始的位置-->
<!--x轴结束的位置-->
<!--y轴开始的位置-->
<!--y轴结束的位置-->
<!--动画持续的时间-->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000">
<alpha
android:fromAlpha="0"
android:toAlpha="1"/>
<scale
android:fromYScale="0"
android:toYScale="2"
android:fromXScale="0"
android:toXScale="2"/>
<rotate
android:fromDegrees="0"
android:toDegrees="600"
android:pivotY="50%"
android:pivotX="50%"/>
</set>
代码中调用
btn是一个button tv是一个textview
btn?.setOnClickListener({
// var animation = AnimationUtils.loadAnimation(this , R.anim.scaleanim)
// var animation = AnimationUtils.loadAnimation(this , R.anim.alphaanim)
// var animation = AnimationUtils.loadAnimation(this , R.anim.translateanim)
var animation = AnimationUtils.loadAnimation(this , R.anim.setanim)
tv.startAnimation(animation)
})
代码实现动画
显示场景用也会出现只会使用一次的动画这时我们可以使用代码来创建动画并调用
先来了解一下 各种动画对应的类
标签 | 类 |
---|---|
scale | ScaleAnimation |
alpha | AlphaAnimation |
rotate | RotateAnimation |
translate | TranslateAnimation |
set | AnimationSet |
// 以ScaleAnimation 举例
这里以public ScaleAnimation(float fromX, float toX, float fromY, float toY,
int pivotXType, float pivotXValue, int pivotYType, float pivotYValue) 构造方法做示例 其他构造方法可自行尝试
btn?.setOnClickListener({
var scalAnim = ScaleAnimation(0f,1.4f,0f,1.4f,Animation.RELATIVE_TO_SELF ,0.5f ,
Animation.RELATIVE_TO_SELF , 0.5f )
scalAnim.duration = 700
tv.startAnimation(scalAnim)
// scalAnim.cancel() // 动画取消
// scalAnim.reset() // 动画重置
// 动画监听
scalAnim.setAnimationListener(object : Animation.AnimationListener{
override fun onAnimationRepeat(animation: Animation?) {
Log.e("animation","onAnimationRepeat")
}
override fun onAnimationEnd(animation: Animation?) {
Log.e("animation","onAnimationEnd")
}
override fun onAnimationStart(animation: Animation?) {
Log.e("animation","onAnimationStart")
}
})
})
镜头由远及近 BounceInterpolator 示例
var scaleAnimation = ScaleAnimation(
1f, 2.4f, 1f, 2.4f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.repeatCount = Animation.INFINITE
scaleAnimation.fillAfter = false
scaleAnimation.duration = 4000
scaleAnimation.interpolator = BounceInterpolator()
img.startAnimation(scaleAnimation)
loading框无限旋转
var rotateAnimation = RotateAnimation(0f,360f,Animation.RELATIVE_TO_SELF , 0.5f,
Animation.RELATIVE_TO_SELF,0.5f)
rotateAnimation.repeatCount = Animation.INFINITE
rotateAnimation.duration = 500
rotateAnimation.interpolator = LinearInterpolator()
loading.startAnimation(rotateAnimation)
水波纹动画
xml定义scale 和alpha 动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000">
<scale
android:repeatCount ="infinite"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="3"
android:toYScale="3"
android:pivotX="50%"
android:pivotY="50%"/>
<alpha
android:repeatCount ="infinite"
android:fromAlpha=".4"
android:toAlpha="0"/>
</set>
布局文件定义 四个imageview 和一个textview
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/circle1"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:src="@drawable/scan_circle"/>
<ImageView
android:id="@+id/circle2"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:src="@drawable/scan_circle"/>
<ImageView
android:id="@+id/circle3"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:src="@drawable/scan_circle"/>
<ImageView
android:id="@+id/circle4"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:src="@drawable/scan_circle"/>
<TextView
android:id="@+id/btn_circle"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:background="@mipmap/button"/>
</FrameLayout>
代码设置点击事件 点击图标开始 依次延迟播放动画
btn_circle.setOnClickListener({
var animation1 = AnimationUtils.loadAnimation(this, R.anim.button_set)
var animation2 = AnimationUtils.loadAnimation(this, R.anim.button_set)
var animation3 = AnimationUtils.loadAnimation(this, R.anim.button_set)
var animation4 = AnimationUtils.loadAnimation(this, R.anim.button_set)
circle1.startAnimation(animation1)
animation2.startOffset = 600
circle2.startAnimation(animation2)
animation3.startOffset = 1200
circle3.startAnimation(animation3)
animation4.startOffset = 1800
circle4.startAnimation(animation4)
})
AnimationDrawable 逐帧动画
xml 定义逐帧动画
// 布局文件定义imageview 并把xml动画设为drawable 代码中调用getDrawable 获取
<ImageView
android:id="@+id/ima_frame"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/animation_list"/>
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:duration="1000"
android:drawable="@drawable/scan_circle"/>
<item
android:duration="1000"
android:drawable="@drawable/scan_circle2"/>
<item
android:duration="1000"
android:drawable="@drawable/scan_circle3"/>
<item
android:duration="1000"
android:drawable="@drawable/scan_circle4"/>
<item
android:duration="1000"
android:drawable="@drawable/scan_circle5"/>
</animation-list>
代码调用
var animList = ima_frame.drawable
(animList as AnimationDrawable).start()
AnimationDrawable 的常用函数
函数名 | 描述 |
---|---|
void start () | 开始播放逐帧动画 |
void stop () | 停止播放逐帧动画 |
int getDuration (int index) | 得到指定帧的持续时间 |
Drawable getFrame (int index) | 得到执行帧的drawable对象 |
int getNumberOfFrames () | 得到所有帧的数量 |
boolean isRunning () | 判断当前动画是否在播放 |
void setOneShot (boolean oneShot) | 设置动画是否播放一次 true播放一次 false循环播放 |
boolean isOneShot () | 判断当前动画是否播放一次 |
void addFrame (Drawable frame, int duration) | 给动画添加一帧 以及设置该帧持续的时间 |
属性动画 ValueAnimation
// 属性动画可以在动画完成之后 保留动画的属性 比如可以在最终的位置保持点击事件 视图动画只能在原位置保持点击事件
textview.setOnClickListener({
Toast.makeText(this,"hhhh" , Toast.LENGTH_SHORT).show()
})
btn_start.setOnClickListener({
// var valueAnimation = ValueAnimator.ofInt(0,400)
//这里可以传入不同的参数 参数越多 动画的变化越复杂
var valueAnimation = ValueAnimator.ofFloat(0f,400f,50f,300f)
valueAnimation.duration = 3000
//实时监听属性动画的进度 同时改变textview 的位置
valueAnimation.addUpdateListener({
var curValue : Int = (it.animatedValue as Float).toInt()
textview.layout(curValue , curValue , curValue+textview.width , curValue + textview.height )
})
})
// 属性动画还有一个方法 传入 evaluator 和 可变传参object
示例 按钮的text从 A 到 Z
// evaluator 用于根据传入的起始值和终点值 以及动画当前的进度 计算当时应该得到的值
// 示例 textview 从 a 变化到 Z
var objAnimator = ValueAnimator.ofObject(CharEvaluator() , 'A','Z')
objAnimator.addUpdateListener({
var text = it.animatedValue
btn_start.text = text.toString()
})
objAnimator.interpolator = AccelerateDecelerateInterpolator()
objAnimator.duration = 5000
objAnimator.start()
class CharEvaluator : TypeEvaluator<Char> {
override fun evaluate(fraction: Float, startValue: Char?, endValue: Char?): Char {
var startInt = startValue?.toInt()?:0
var endInt = endValue?.toInt()?:0
var cur = (startInt + fraction*(endInt - startInt)).toInt()
var result = cur.toChar()
return result
}
}
ValueAnimation 函数汇总
函数 | 描述 |
---|---|
ValueAnimator setDuration(long duration) | 设置动画时长 单位-毫秒 |
Object getAnimationValue() | 获取当前动画进度的值 |
void start() | 开始动画 |
void setRepeatCount(int value) | 设置循环次数 |
void setRepeatMode(int value) | 设置循环模式 |
void cancel() | 取消动画 |
//ValueAnimation 的两个监听
// AnimatorUpdateListener 监听动画时时改变的值
// AnimationListener 监听动画的开始 结束 取消 重复 四个状态
// 示例: 弹跳加载中效果
class View3 : ImageView{
var mTop :Int = 0 // 当前控件的高度
constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
initView()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mTop = top //m
}
private fun initView() {
var animation = ValueAnimator.ofInt(0 , 150 , 0)
animation.repeatCount = ValueAnimator.INFINITE
animation.repeatMode = ValueAnimator.RESTART
animation.duration = 1000
animation.interpolator = AccelerateDecelerateInterpolator()
animation.addUpdateListener({
var dx = it.animatedValue as Int
top = mTop - dx //监听动画的进度并改变控件的高度
})
// 一共四张图片 每次重复动画次数累加 改变现实的图片
var mCuttentIndex = 0
val mCount = 4
animation.addListener(object :Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator?) {
mCuttentIndex++
when(mCuttentIndex % mCount){
0 -> setImageDrawable(resources.getDrawable(R.mipmap.animal1))
1 -> setImageDrawable(resources.getDrawable(R.mipmap.animal2))
2 -> setImageDrawable(resources.getDrawable(R.mipmap.animal3))
3 -> setImageDrawable(resources.getDrawable(R.mipmap.animal4))
}
}
override fun onAnimationEnd(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onAnimationCancel(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onAnimationStart(animation: Animator?) {
setImageDrawable(resources.getDrawable(R.mipmap.animal1))
}
})
animation.start()
}
}
ObjectAnimator
//使用ValueAnimator有一个缺点 那就是如果想要对哪个控件执行操作就需要监听ValueAnimator的动画过程
// 为了能让动画直接与对应控件想关联我们可以使用 ObjectAnimator 派生自ValueAnimator
// 示例 第一个参数为控件 第二个参数为属性名 之后是变化的值
var objanim = ObjectAnimator.ofFloat(btn_start , "alpha" , 1f,0f,1f)
objanim.duration = 2000
objanim.start()
// 第二个参数怎么来
// 其实所有的view都继承自view在view中有一下和动画相关的属性 第二个参数取以下的属性名即可
btn_start.alpha = 1f
btn_start.rotation = 30f
btn_start.rotationX = 10f
btn_start.rotationY = 10f
btn_start.translationX = 10f
btn_start.translationY = 10f
btn_start.translationZ = 10f
btn_start.scaleX = 2f
btn_start.scaleY = 2f
// 如果第一个参数控件是自定义的并且具有单独且可以通过set赋值的属性 那么该属性可以被设置在第二个参数
AnimatorSet - 组合动画
// 主要有两个方法
// var animatorSet = AnimatorSet()
// animatorSet.playTogether() // 所有动画一起播放
// animatorSet.playSequentially() // 按顺序播放动画
// AnimatorSet.Builder
// 如果我们想要先播放 动画a 然后在同时播放动画b 和动画c 我们就需要使用AnimatorSet.Builder
// 主要有一下几个函数
// var animatorsetBuilder = animatorSet.play(Animator) // 播放动画
// animatorsetBuilder.with(Animator) //和前面的动画一起执行
// animatorsetBuilder.before(Animator) // 先执行这个动画在执行前面的动画
// animatorsetBuilder.after(Animator) // 先执行前面的动画在执行该动画
// animatorsetBuilder.after(long) // 延迟n毫秒之后在执行动画
// 路劲动画
// ValueAnimation 和 ObjectAnimation 都拥有ofPropertyValuesHolder 函数 可以同时执行多个动画
var rotationHolder = PropertyValuesHolder.ofFloat("Rotation" , 60f,-60f,40f)
var alphaHolder = PropertyValuesHolder.ofFloat("alpha" , 0.1f,1f,0.5f)
var animator = ObjectAnimator.ofPropertyValuesHolder(tv , rotationHolder,alphaHolder)
animator.duration = 2000
animator.start()
KeyFrame - 关键帧 可以使用该类来生成动画
// 第一个参数表示动画的进度 第二个参数表示该进度动画的值
var keyframe0 = Keyframe.ofFloat(0f,0f)
var keyframe1 = Keyframe.ofFloat(0.5f,10f)
var keyframe2 = Keyframe.ofFloat(1f,100f)
// 通过制定关键帧的方式生成动画 类似flash制作动画
var frameHolder = PropertyValuesHolder.ofKeyframe("rotation" ,keyframe0,
keyframe1,keyframe2)
var frameAnimator = ObjectAnimator.ofPropertyValuesHolder(tv , frameHolder)
frameAnimator.duration = 2000
frameAnimator.start()
ViewPropertyAnimator
// view 可以通过animate函数得到ViewPropertyAnimator对象 调用ViewPropertyAnimator的函数也可生成动画
// ViewPropertyAnimator有很多的函数这里举例其中几个
tv.animate().x(10f).y(100f).alpha(0.5f)
// 也可以添加监听
tv.animate().setListener(object :Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onAnimationEnd(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onAnimationCancel(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onAnimationStart(animation: Animator?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
})
animateLayoutChanges 为viewgroup的组件添加动画
// 在布局xml中定义该属性为true 即可 动画不可自定义
var index = 0
btn_add.setOnClickListener({
index++
var button = Button(this)
button.text = "button $index"
var params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT )
button.layoutParams = params
container.addView(button,0)
})
btn_remove.setOnClickListener({
container.removeViewAt(0)
})
LayoutTransition 为viewgroup的组件自定义动画
var layoutTransition = LayoutTransition()
var animOut = ObjectAnimator.ofFloat(null , "rotationY",0f,90f,0f)
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING , animOut)
var animIn = ObjectAnimator.ofFloat(null , "scaleY",0f,3f,1f)
layoutTransition.setAnimator(LayoutTransition.APPEARING , animIn)
container.layoutTransition = layoutTransition
// LayoutTransition.DISAPPEARING //元素在容器内消失时的动画
// LayoutTransition.APPEARING //元素在容器内出现时的动画
// LayoutTransition.CHANGE_APPEARING //元素在容器内出现时其他需要变化的元素的动画
// LayoutTransition.CHANGE_DISAPPEARING //元素在容器内消失时其他需要变化的元素的动画
// CHANGE_APPEARING 和 CHANGE_DISAPPEARING 必须使用 PropertyValusHolder 所构造的动画才有效果
// 也就是说 ObjectAnimator构造的动画在这里没有效果 且动画开始的值和结束的值必须一致 不一致也不会有效果
// 同时 在构造PropertyValusHolder动画时 left 和 top 属性的变动是必须的 如果不需要变动则直接写为
//CHANGE_APPEARING
var pvLeft = PropertyValuesHolder.ofInt("left" , 0 , 0)
var pvTop = PropertyValuesHolder.ofInt("top" , 0 , 0)
var pvScalX = PropertyValuesHolder.ofFloat("scalX" , 1f , 0f, 1f)
var changeAppearAnim = ObjectAnimator.ofPropertyValuesHolder(container , pvLeft,pvTop,pvScalX)
//CHANGE_DISAPPEARING
var disAppearFrame0 = Keyframe.ofFloat(0f ,0f)
var disAppearFrame1 = Keyframe.ofFloat(0.2f ,0.2f)
var disAppearFrame2 = Keyframe.ofFloat(0.4f ,0.4f)
var disAppearFrame3 = Keyframe.ofFloat(0.6f ,0.6f)
var disAppearFrame4 = Keyframe.ofFloat(1f ,0f)
var disAppearHolder = PropertyValuesHolder.ofKeyframe("rotation" ,
disAppearFrame0 , disAppearFrame1,disAppearFrame2,disAppearFrame3,disAppearFrame4)
var changeDisAppearAnim = ObjectAnimator.ofPropertyValuesHolder(container,
pvLeft,pvTop , disAppearHolder)
var changeTransition = LayoutTransition()
changeTransition.setAnimator(LayoutTransition.CHANGE_APPEARING , changeAppearAnim)
changeTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING , changeDisAppearAnim)
container.layoutTransition = changeTransition
// LayoutTransition函数
// void setDuration(long duration) 给所有的动画设置时长
// void setDuration(int type,duration) 针对单个类型的动画设置时长
// void setInterpolator(int type , TimeInterpolator interpolator)给单个类型的动画设置差值器
// void setStartDelay(int type , long delay) 给单个类型的动画设置延迟
// void setStagger(int type , long duration) 针对单个动画设置每个item动画的时间间隔
//设置监听
layoutTransition.addTransitionListener(object :LayoutTransition.TransitionListener{
override fun startTransition(
transition: LayoutTransition?,
container: ViewGroup?,
view: View?,
transitionType: Int
) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun endTransition(
transition: LayoutTransition?,
container: ViewGroup?,
view: View?,
transitionType: Int
) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
})
开源动画库 NineOldAndroids http://nineoldandroids.com/
// 这是一个兼容安卓低版本的动画库 支持3.0以下的版本使用 Animation API 唯一不支持的是 LayoutTransition
//PathMeasure实现路径动画
PathMeasure(Path path, boolean forceClosed)
// 第一个参数为path
// 第二个参数为是否闭合 对path本身的绘制没有影响 但对测量的结果有影响 会包含最后一段闭合的路径与原来的path不同
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.translate(200f,200f)
var paint = Paint()
paint.strokeWidth = 10f
paint.style = Paint.Style.STROKE
paint.color = Color.RED
var path = Path()
path.moveTo(0f,0f)
path.lineTo(0f,100f)
path.lineTo(100f,100f)
path.lineTo(100f,0f)
// 第一个参数为path
// 第二个参数为是否闭合 对path本身的绘制没有影响 但对测量的结果有影响 会包含最后一段闭合的路径与原来的path不同
var measure1 = PathMeasure(path , true)
var measure2 = PathMeasure(path , false)
// 获取路径的长度
Log.e("view5","forceClosed=true ---->"+measure1.length) // 400
Log.e("view5","forceClosed=false ---->"+measure2.length) // 300
canvas?.drawPath(path, paint)
}
可以看见最后一个参数不一样 对路径的长度计算结果会造成影响
// measure1.isClosed 用于获取path测量的路径是否闭合
// measure1.nextContour() path可以由多条曲线构成 但是getlength或其他函数都只会针对第一条曲线进行计算
// 该函数就是用于跳转到下一条曲线的函数 成功返回true 失败则返回false
var path = Path()
// 如果都是Path.Direction.CW 发现只会绘制最大的一个矩形
path.addRect(-50f,-50f,50f,50f,Path.Direction.CCW)
path.addRect(-100f,-100f,100f,100f,Path.Direction.CW)
path.addRect(-120f,-120f,120f,120f,Path.Direction.CW)
var pathMeasure = PathMeasure(path , false)
do {
Log.e("view5","length=== ${pathMeasure.length}")
}while (pathMeasure.nextContour())
getSegment 用于截取整个path中的某个片段
// 通过startD 和 stopD 来控制截取的长度,并将截取后的path保存到参数dst中
// startWithMoveTo 表示起始点是否使用moveto 将路径的新起始点移动到结果path 的起始点 通常为true以保证每次截取的path都是完整的
// boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
var path = Path()
path.addRect(-50f,-50f,50f,50f,Path.Direction.CW)
var dst = Path()
// dst.lineTo(0f,200f) //如果dst本身就有路径 将会把截取的路径添加到原路径中
var pathmeasure = PathMeasure(path ,false)
pathmeasure.getSegment(0f,150f,dst,true)
canvas?.drawPath(dst,paint)
实现一条圆形路径从长度0慢慢增加到整个原 如此往复
var animValue : Float
var dst : Path
var circle :Path
var pathMeasure :PathMeasure
constructor(context: Context?, attrs: AttributeSet?):super(context,attrs){
// 需要关闭硬件加速功能 否则绘图会出现问题
setLayerType(LAYER_TYPE_SOFTWARE,null)
animValue = 0f
dst = Path()
circle = Path()
circle.addCircle(100f,100f,50f , Path.Direction.CW)
pathMeasure = PathMeasure(circle , true)
var animator = ValueAnimator.ofFloat(0f,1f)
animator.repeatCount = ValueAnimator.INFINITE
animator.addUpdateListener({
animValue = it.animatedValue as Float
invalidate()
})
animator.duration = 2000
animator.start()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.translate(200f,200f)
var paint = Paint()
paint.strokeWidth = 10f
paint.style = Paint.Style.STROKE
paint.color = Color.RED
canvas?.drawColor(Color.WHITE)
var stop = pathMeasure.length * animValue
dst.reset()
var start = 0f
start = ( stop - (0.5 - Math.abs(animValue-0.5)) * pathMeasure.length).toFloat()
pathMeasure.getSegment(start,stop , dst,true)
canvas?.drawPath(dst , paint)
}
getPosTan 函数 用于得到路径上某一长度的位置以及该位置的正切值
// boolean getPosTan(float distance, float pos[], float tan[])
// pos[0]为x坐标 pos[1]为y坐标 tan表示该点的正切值 tan为直角三角形的三角函数 对边/邻边
// Math 有两个可以求反切值的函数
// double atan(double a) // 参数为弧度值
// double atan2(double y, double x) // 参数为坐标值
getMatrix 函数 用于得到路径上某一长度的位置以及该位置的正切值的矩形
// boolean getMatrix(float distance, Matrix matrix, int flags)
// distance 距离path起始点的长度
// matrix 根据flags封装好的matrix会根据flags 的设置存入不同的内容
// flags 有两个值 PathMeasure.POSITION_MATRIX_FLAG获取位置信息 PathMeasure.TANGENT_MATRIX_FLAG获取切边信息
在上面圆形路径的基础上增加箭头
// 方法一
pathMeasure.getPosTan(stop , pos,tan)
var degress = (Math.atan2(tan[1].toDouble(), tan[0].toDouble())*180.0/Math.PI).toFloat()
var matrix = Matrix()
matrix.postRotate(degress , (bmp.width/2.0).toFloat(), (bmp.height/2.0).toFloat())
matrix.postTranslate(pos[0]- bmp.width/2, pos[1]-bmp.height/2)
canvas?.drawBitmap(bmp,matrix,paint)
// 方法二
// var matrix = Matrix()
// pathMeasure.getMatrix(stop , matrix , PathMeasure.POSITION_MATRIX_FLAG or PathMeasure.TANGENT_MATRIX_FLAG)
// matrix.preTranslate((-bmp.width/2).toFloat(), (-bmp.height/2).toFloat())
// canvas?.drawBitmap(bmp,matrix,paint)
支付宝支付成功动画
class View5Alipay : View {
var circle :Path
var dst :Path
var curx = 300f
var cury = 300f
var radius = 200f
var pathMeasure : PathMeasure
var animValue = 0f
var paint:Paint
constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
setLayerType(LAYER_TYPE_SOFTWARE,null)
paint = Paint()
paint.isAntiAlias = true
paint.strokeWidth = 10f
paint.color = Color.BLACK
paint.style = Paint.Style.STROKE
circle = Path()
dst = Path()
circle.addCircle(curx,cury ,radius ,Path.Direction.CW)
circle.moveTo(curx - radius/2 , cury)
circle.lineTo(curx , cury + radius/2)
circle.lineTo(curx + radius/2 , cury - radius/3)
pathMeasure = PathMeasure(circle , false)
var anim = ValueAnimator.ofFloat(0f,2f)
anim.addUpdateListener({
animValue = it.animatedValue as Float
invalidate()
Log.e("alipay","value= $animValue")
})
anim.duration = 2000
anim.repeatCount = ValueAnimator.INFINITE
anim.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
Log.e("alipay","onAnimationRepeat")
}
override fun onAnimationEnd(animation: Animator?) {
Log.e("alipay","onAnimationEnd")
}
override fun onAnimationCancel(animation: Animator?) {
Log.e("alipay","onAnimationCancel")
}
override fun onAnimationStart(animation: Animator?) {
Log.e("alipay","onAnimationStart")
}
})
anim.start()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawColor(Color.WHITE)
if(animValue < 1){
dst.reset()
pathMeasure = PathMeasure(circle , false)
var stop = pathMeasure.length * animValue
pathMeasure.getSegment(0f,stop ,dst , true)
}else if(animValue == 1f){
Log.e("alipay","nextContour")
pathMeasure.getSegment(0f,pathMeasure.length , dst ,true)
pathMeasure.nextContour()
}else{
var stop = pathMeasure.length *(animValue -1)
pathMeasure.getSegment(0f,stop,dst ,true)
}
canvas?.drawPath(dst,paint)
}
}