目录
一、前言
二、基础代码
(1)自定义View基础流程
(2)onDraw()方法
(3)onMeasure()方法
三、动画
(1)补间动画
(2)帧动画
(3)属性动画
四、SVG标签使用
五、练习Demo
六、Demo地址
七、内容推荐
自定义控件一直是Android很重要的一部分,但是大部分时间我们都在处理业务逻辑而很少自己去写控件。因为现在开源的控件比较多,自定义写起来也比较麻烦。但是当我们需要的时候可能就忘了,所以菜鸟作者就买了本书打算系统的学习一遍。这一篇也算是《Android 自定义控件开发入门与实战》读后感。边学边工作也花了一个多月写过几篇笔记,不过后面的隐藏掉了。感觉有点啰嗦所以就凑成这一篇总结。
不管感兴趣与不感兴趣可以浏览一遍,积累一下基础。如果要深入学习这里也推荐两位大神博客
https://www.jianshu.com/p/146e5cec4863
https://blog.csdn.net/harvic880925 启舰《Android 自定义控件开发入门与实战》
《 自定义控件开发入门与实战》这本书我这里主要分成两部分总结:1.自定义View 2.动画
这里主要根据练习时候写的Demo来进行基础的回顾。先从自定义View开始
先从简单的开始讲起:
为什么先说这方法呢 ? 因为前面两个方法不说。我们也可以通过这个方法来简单的实现自定义控件。
先上菜(代码),在介绍这道菜的好处
fun initPaint(){
//创建画笔
paint = Paint()
//设置画笔颜色
paint?.setColor(Color.WHITE)
//设置填充样式 1.Paint.Style.FILL仅填充内部 2.Paint.Style.FILL_AND_STROKE填充内部和描边 3.Paint.Style.STROKE仅描边
paint?.setStyle(Paint.Style.STROKE)
//设置画笔宽度 注:画笔样式为Paint.Style.FILL时不显示效果
paint?.setStrokeWidth(5f)
paint1 = Paint()
//设置画笔颜色
paint1?.setColor(Color.GREEN)
//设置填充样式 1.Paint.Style.FILL仅填充内部 2.Paint.Style.FILL_AND_STROKE填充内部和描边 3.Paint.Style.STROKE仅描边
paint1?.setStyle(Paint.Style.STROKE)
//设置画笔宽度 注:画笔样式为Paint.Style.FILL时不显示效果
paint1?.setStrokeWidth(5f)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//平移画布 起始点起始点从(X:0,Y:0)变成(X:100,Y:100)
canvas?.translate(0f, 100f)
//裁剪画布 构造方法很多 都是以clip开头
canvas?.clipRect(0, 0, 500, 500)
//设置画布颜色
canvas?.drawColor(Color.BLACK)
// canvas.drawARGB(0,0,0,255);
// canvas.drawRGB(0,0,255);
// 绘制直线 startX/startY:起始点X/Y坐标 stopX/stopY:终点X/Y坐标
canvas?.drawLine(100f, 50f, 450f, 50f, paint)
//绘制点
canvas?.drawPoint(50f, 50f, paint)
//绘制矩形====== >
val rect = Rect(50, 100, 450, 450)//创建矩形工具类
//绘制矩形方法
canvas?.drawRect(rect, paint)//第一种构造方法
// Rect/RectF用来保存int/float类型数值的矩形结构
// RectF rectf = new RectF(200,200,400,400);
// canvas.drawRect(rectf);第二种构造方法
// canvas.drawRect(200,200,400,400,paint);第三种构造方法
// <====== 绘制矩形方法
// 起始点变成(X:50,Y:100)
canvas?.translate(50f, 100f)
//绘制路径 ====== >
val path = Path()
//设置起始点
path.moveTo(100f, 50f)
//第一条直线的终点也是第二条直线的起点
path.lineTo(50f, 200f)
path.lineTo(150f, 100f)
path.lineTo(50f, 100f)
path.lineTo(150f, 200f)
// path.lineTo(100, 50);
//闭环
path.close()
//绘制路径方法
canvas?.drawPath(path, paint)
// < ======绘制路径
//绘制弧线 ===== >
val path1 = Path()
val rect1 = RectF(200f, 50f, 350f, 200f)
//弧线主要方法 oval 生成椭圆的矩形,startAngle 弧开始角度, sweepAngle 持续角度
path1.arcTo(rect1, 180f, 180f, false)
canvas?.drawPath(path1, paint)
// < ===== 绘制弧线
// 起始点变成(X:50,Y:100)
canvas?.translate(50f, 250f)
//绘制区域 ==== >
val region = Region(0, 0, 200, 50)
val region1 = Region(150, -50, 300, 50)
drawRegion(canvas, region, paint)
drawRegion(canvas, region1, paint1)
//区域操作
// (1)Op.DIFFERENCE:显示region与region1不同区域
// (2)Op.INTERSECT:显示region与region1相交区域
// (3)Op.UNION:显示region与region1组合在一起区域
// (4)Op.XOR:显示region与region1相交之外区域
// (5)Op.REVERSE_DIFFERENCE:显示region1与region不同区域
// (6)Op.REPLACE:显示region1区域
region.op(region1, Region.Op.INTERSECT)
paint1?.setColor(Color.GRAY)
drawRegion(canvas, region, paint1)
// < ==== 绘制区域
//保存当前画布状态
canvas?.save()
//恢复到上一层保存的状态
// canvas.restore();
}
fun drawRegion(canvas : Canvas?,region : Region,paint: Paint?){
var iter = RegionIterator(region)
var r = Rect()
while (iter.next(r)){
canvas?.drawRect(r,paint)
}
}
看完代码我们知道,绘图的前提是先准备好一只画笔(paint),要准备一只怎样的画笔呢。这时候我们可以根据Paint 类提供的API来定义画笔样式。
画笔准备好之后,我们就要考虑绘制什么样的图形呢,要绘制在什么地方。这时候我们需要一张画布,用来显示绘制的图形。
而onDraw(canvas: Canvas?)方法刚好提供了一个画布,并且通过Canvas类提供的API,我们可以绘制各式各样的图形。当然常用的绘图方法就看上面的代码。都用详细的注释。
总结:1.准备画笔 2.利用画布绘图
注意:通常要在自定义View的构造函数中初始化好画笔,不要在onDraw()方法中去初始画笔。因为每次刷新画布的时候都会不断调用ondraw()创建新的画笔,消耗内存....
如果要通过下面属性来控制自定义View的大小时,我们需要重载onMeasure()方法来控制画布(自定义View)的大小,
android:layout_width="200dp"
android:layout_height="250dp"
怎么控制呢。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//widthMeasureSpec/heightMeasureSpec 由mode+size两部分组成
//获取模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
//获取数值
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
when(widthMode){
//父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小 :match_parent 或者具体指 200dp
MeasureSpec.EXACTLY ->{
//根据需求计算宽度大小
testWidth = widthSize - 100
}
//子元素至多达到指定大小的值 : wrap_content
MeasureSpec.AT_MOST ->{}
//父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
MeasureSpec.UNSPECIFIED ->{}
}
when(heightMode){
MeasureSpec.EXACTLY ->{
//根据需求计算高度大小
testHeight = heightSize - 100
}
MeasureSpec.AT_MOST ->{}
MeasureSpec.UNSPECIFIED ->{}
}
//计算出的宽度和高度 可以通过setMeasuredDimension方法进行设置
setMeasuredDimension(testWidth!!, testHeight!!)
}
通过上面的代码我们可以简单的计算出所需要的宽高。
只通过上面简单的代码很难明白测量的意义,所以这里提供一个博客供大家参考,篇幅太长就不在这里多写 毕竟是总结..
https://www.cnblogs.com/yishujun/p/5560838.html
onLayout()方法在这里也不介绍,因为只用在自定义ViewGroup的时候才会使用。逻辑也会比较复杂。
这本书好几张都讲了动画,不过这里不打算详细介绍。自定义View才是重点。所以这边只贴一部分常用代码。供回忆用。
(代码使用这里就不贴了)
使用方式:
val loadAnimation = AnimationUtils.loadAnimation(this, R.anim.alpha)
view.startAnimation(TweenAnimation.getAlpha())
object FrameAnimation {
/**
* AnimationDrawable常用属性
* start():开始播放逐帧动画。
* stop():停止播放逐帧动画。
* getDuration(int index):得到指定index的帧的持续时间
* getFrame(int index):得到指定index的帧所对应的Drawable对象
* getNumberOfFrames():得到当前AnimationDrawable的所有帧数量
* isRunning():判断当前AnimationDrawable是否正在播放
* setOneShot(boolean oneShot):设置AnimationDrawable是否执行一次
* isOneShot():判断当前AnimationDrawable是否执行一次
* addFrame(Drawable frame,int duration):为AnimationDrawable添加1帧,并设置持续时间
*/
fun getAnimation(context: Context):AnimationDrawable{
val animationDrawable = AnimationDrawable()
for (x in 1..3){
val resources = context.resources.getIdentifier("frame"+x,"mipmap",context.packageName)
val drawable = context.resources.getDrawable(resources)
animationDrawable.addFrame(drawable,500)
}
animationDrawable.isOneShot = false
return animationDrawable
}
}
使用方式: val animation = FrameAnimation.getAnimation(this) iv_main.setBackgroundDrawable(animation) animation.start()
object ValueAnimation {
/**
* ValueAnimator常用函数
* ofInt(int... values)、ofFloat(float... values) 、ofObject:设置动画变化过程中的值
* setDuration(long duration):设置动画时长,单位是毫秒
* getAnimatedValue():获取ValueAnimator在运动时当前运动点的值
* start():开始动画
* setRepeatCount(int value):设置循环次数,设置为INFINITE表示无限循环
* setRepeatMode(int value):设置循环模式 ValueAnimation.REVERSE倒序重新开始 ValueAnimation.RESTART正序重新开始
* addUpdateListener(AnimatorUpdateListener listener) : 监听动画过程中值的实时变化
* addListener(AnimatorListener listener):监听动画变化时的4个状态
* removeUpdateListener(AnimatorUpdateListener listener):移除指定监听
* removeAllUpdateListeners():移除所有监听
* removeListener(AnimatorListener listener):移除AnimatorUpdateListener
* removeAllListeners():移除AnimatorListener
* setInterpolator():设置插值器
* cancel():取消动画
* setStartDelay(long startDelay):延时多久开始,单位是毫秒
* clone():克隆一个ValueAnimator实例
*/
fun init(view : View):ValueAnimator{
val animator = ValueAnimator.ofInt(0, 500)
animator.duration = 3000
animator.repeatCount = ValueAnimator.INFINITE
animator.repeatMode = ValueAnimator.REVERSE
animator.setEvaluator(MyEvaluator)
animator.addUpdateListener({animation ->
val value = animation.animatedValue as Int
view.layout(view.left,view.top,value+view.left,view.bottom)
})
return animator
}
/**
* 开始动画
*/
fun startAnimator(view : View){
val init = init(view)
init.start()
}
/**
* 停止动画
*/
fun stopAnimator(view : View){
val init = init(view)
init.removeAllUpdateListeners()
init.cancel()
}
}
使用方式:ValueAnimation.startAnimator(iv_main)
ObjectAnimation代码使用:
object ObjectAnimation {
/**
* 组合动画
*/
fun start(tv1 : View){
var background = ObjectAnimator.ofInt(tv1,"BackgroundColor", Color.BLACK,Color.BLUE,Color.RED)
background.setRepeatCount(ValueAnimator.INFINITE)
var alpha = ObjectAnimator.ofFloat(tv1,"alpha",0f,1f,0f,0.5f)
alpha.setRepeatCount(ValueAnimator.INFINITE)
var animatorSet = AnimatorSet()
animatorSet.playTogether(background,alpha)
animatorSet.setDuration(3000)
animatorSet.start()
}
/**
* PropertyValuesHolder 保存了动画过程中所需要操作的属性和对应的值。
* //设置动画的Evaluator
* public void setEvaluator(TypeEvaluator evaluator)
* //用于设置ofFloat()所对应的动画值列表
* public void setFloatValues(float... values)
* //用于设置ofInt()所对应的动画值列表
* public void setIntValues(int... values)
* //用于设置ofKeyframes()所对应的动画值列表
* public void setKeyframes(Keyframe... values)
* //用于设置ofObject()所对应的动画值列表
* public void setObjectValues(Object... values)
* //设置动画属性名
* public void setPropertyName(String propertyName)
*/
fun property(view : View){
val Background = PropertyValuesHolder.ofInt("BackgroundColor", Color.BLACK,Color.BLUE,Color.RED)
val alpha = PropertyValuesHolder.ofFloat("alpha",0f,1f,0f,0.5f)
val holder = ObjectAnimator.ofPropertyValuesHolder(view,Background, alpha)
holder.duration = 3000
holder.interpolator = AccelerateInterpolator()
holder.repeatCount = Animation.INFINITE
holder.repeatMode = REVERSE
holder.start()
}
/**
* KeyFrame 提供方便地控制动画速率问题。
*/
fun keyFrame(view : View){
val holder = PropertyValuesHolder.ofFloat("alpha", 0f,1f)
//fraction表示当前的显示进度 value:表示动画当前所在的数值位置。
val keyframe = Keyframe.ofFloat(0.1f, 0.1f)
PropertyValuesHolder.ofKeyframe("alpha",keyframe)
val valuesHolder = ObjectAnimator.ofPropertyValuesHolder(view, holder)
valuesHolder.duration = 3000
valuesHolder.repeatCount = Animation.INFINITE
valuesHolder.repeatMode = REVERSE
valuesHolder.start()
}
/**
* ViewPropertyAnimator Android3.1中新增ViewPropertyAnimator机制,给默认属性提供了一种更加便捷的用法。
*/
fun viewProperty(view : View){
view.animate().alpha(0.5f).translationX(500f).rotation(180f).setDuration(5000).scaleX(2f)
}
}
使用方式:
R.id.btn_propertyValuesHolder ->{
ObjectAnimation.property(iv_main)
}
R.id.btn_keyFrame ->{
ObjectAnimation.keyFrame(iv_main)
}
R.id.btn_animate ->{
ObjectAnimation.viewProperty(iv_main)
}
objectAnimator文件使用:
使用方式
//xml动画
val loadAnimator = AnimatorInflater.loadAnimator(this, R.animator.object_animation)
loadAnimator.setTarget(iv_main)
loadAnimator.start()
不解释光贴代码可能看起来很乱,但动画确实不是这篇的重点,只是书中有提到。所以把练习时写的代码给贴出来好复习。如果看起来很累大家可以跳过这一部分。
Google在Android 5.0中增加了对SVG图形的支持。因为SVG的占用空间比Bitmap小。所以这里也稍微提一下使用方式。
首先定义SVG标签:
svg_vector.xml
标签与动画绑定在一起
svg_animation.xml
使用方式:
val create = AnimatedVectorDrawableCompat.create(this, R.drawable.svg_animation) iv_main.setImageDrawable(create)
简单略过...,一切解释尽在代码中
看完这本书后发现了几个有趣的控件。例:放大镜 ,刮刮卡
这里就不展示代码了,都写在练习的项目当中,若有兴趣可以下载下来看看。
练习Demo地址Github:https://github.com/DayorNight/CustomView
另外介绍个人开发的一个Android工具类
Github:https://github.com/DayorNight/BLCS
apk下载体验地址:https://www.pgyer.com/BLCS
也可以扫码下载
简书:《Android 自定义控件基础》
《Android 下载安装应用APK封装(适配8.0)》
《Android Notification通知简单封装(适配8.0)》
《Android 仿RxDialog自定义DialogFragment》
《Android 获取App应用、缓存、数据等大小适配8.0(仿微信存储空间)》
如果你觉得写的不错或者对您有所帮助的话
不妨顶一个【微笑】,别忘了点赞、收藏、加关注哈
看在花了这么多时间整理写成文章分享给大家的份上,记得手下留情哈
您的每个举动都是对我莫大的支持