小米曰: 广告条有了,该告诉用户有几条广告了
作为一个米粉,这是小米被自己黑的最狠的一次。
言归正传,上次实现了一个简单的广告条,此次则实现一个简单的指示器,与其配套。
鉴于上次没有发布源码地址,这次先挂地址:https://github.com/zsh065400/KotlinFirst
这个App的所有功能更新都会同步,我会挑出一些功能写成博客,与大家共勉
自定义View(此次不包含ViewGroup)的方法有很多
- 继承现有的控件,实现附加功能:TextView、ImageView等
- 继承View,自主实现View的测量、绘制、状态更新等
指示器的实现采用了继承VIew的做法,自主实现View,需要注意以下几点
- onMeasure方法对wrap_content参数的支持
- onDraw方法的视图绘制,保证绘制正确与高效
- 自定义属性的定义与获取
- 具体的大小计算与视图绘制
OK,下面将实现拆分,一起来实现一个简单的Indicator指示器
Koltin在实现自定义View的时候要注意方式,不要使用主构造函数,将Java的方式变形成Kotlin即可
class Indicator : View {
constructor(context: Context) : this(context, null) {
}
constructor(context: Context, attributeSet: AttributeSet?)
: super(context, attributeSet) {
}
}
在这里有一些需要注意:
- Kotlin中一个类若存在主构造函数(跟随在类名后的括号),则其他的次构造函数需要直接或间接委托至主构造函数
- 若不存在主构造函数,且类继承自其他类,则所有的构造函数均要直接或间接调用父类构造函数
定义好了类,接着复写相应的方法处理View的宽高和绘制的内容
override fun onMeasure
(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
类准备好了,现在我们需要定义需要的属性,对于Indicator,此次我们需要4个属性:数量、半径、前景色和背景色
<declare-styleable name="Indicator">
<attr name="numbers"
format="integer"></attr>
<attr name="radius"
format="float"></attr>
<attr name="forceColor"
format="reference|color"></attr>
<attr name="backColor"
format="reference|color"></attr>
</declare-styleable>
reference为引用,即在布局文件中使用@引用资源
接下来开始我们的编码之旅,让代码说话
1.定义相应属性
val mForcePaint = Paint()//绘制前景的画笔
val mBackPaint = Paint()//绘制背景的画笔
var mOffset: Float = 0f//指示器滑动偏移量
var mNumbers = 3//指示器个数
var mRadius: Float = 20f//指示器半径
var mForceColor = Color.GRAY//前景色
var mBackColor = Color.GRAY//背景色
var mPaddingLeft = 2//默认左右内边距
var dpiX: Float = 0f//dpi缩放指数
2.获取在XML布局中设置的属性
constructor(context: Context, attributeSet: AttributeSet?)
: super(context, attributeSet) {
if (attributeSet != null) {
//通过typedArray与定义好的xml属性,获取声明在布局文件中的值
val typedArray =
context.obtainStyledAttributes
(attributeSet, R.styleable.Indicator)
//获取个数
mNumbers =
typedArray.getInteger
(R.styleable.Indicator_numbers, mNumbers)
//获取半径
mRadius =
typedArray.getFloat
(R.styleable.Indicator_radius, mRadius)
//颜色的获取包含引用和具体颜色值,故要分别处理
val forceColorResId =
typedArray.getResourceId
(R.styleable.Indicator_forceColor, 0)
mForceColor =
if (forceColorResId != 0) resources.getColor
(forceColorResId) else
typedArray.getColor
(R.styleable.Indicator_forceColor, mForceColor)
val backColorResId = typedArray.getResourceId
(R.styleable.Indicator_backColor, 0)
mBackColor =
if (backColorResId != 0) resources.getColor(backColorResId)
else typedArray
.getColor(R.styleable.Indicator_backColor, mBackColor)
typedArray.recycle()
}
//初始化画笔,后续介绍
initPaint()
}
Tips:对于值的获取要注意引用和具体值,在TypedArray使用完毕后注意释放
3.初始化画笔用于绘制
private fun initPaint(): Unit {
mForcePaint.strokeWidth = 2.0f//画笔粗细
mForcePaint.style = Paint.Style.FILL//画笔模式:填充
mForcePaint.color = mForceColor//画笔颜色
mForcePaint.isAntiAlias = true//开启抗锯齿
mBackPaint.strokeWidth = 2.0f
mBackPaint.style = Paint.Style.STROKE//画笔模式:边框
mBackPaint.color = mBackColor
mBackPaint.isAntiAlias = true
}
画笔的应用可根据需要自行定制
4.绘制指示器(此处使用圆点)
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
var i = 0
val y: Float = (mRadius * dpiX + 0.5).toFloat()//此处为适配wrap_content下的y坐标,后续会有讲解
while (i < mNumbers) {
//绘制背景空心圆,圆的间隔为一个半径
canvas?.drawCircle(mPaddingLeft + mRadius + i * mRadius * 3f, y, mRadius, mBackPaint)//Canvas绘制圆,参数为x,y,radius,paint
i++
}
//绘制前景实心圆,表示当前状态
canvas?.drawCircle(mPaddingLeft + mRadius + mOffset, y, mRadius, mForcePaint)
}
到此处,编译过后,即可在布局文件中使用我们的空间了,如下:
<!--注意命名空间的添加 xmlns:app="http://schemas.android.com/apk/res-auto"-->
<your.package.name.Indicator
android:id="@+id/id_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
app:backColor="@android:color/black"
app:forceColor="#ff0000"
app:numbers="3"
app:radius="25"/>
到此处,你已经能看到你写的指示器了,是不是很有成就感呢?接下来,让我们继续完善它,让她支持如下功能:
- 支持wrap_content属性设置
- 支持配合Banner的滑动状态改变
从难易程度上来说,第二个易于实现,还记得咱们声明的那些属性么,mOffset还没有用到哟,这就是突破点
fun setOffset(position: Int, positionOffset: Float): Unit {
val index = position % mNumbers//用来支持循环滑动,position对数量取模,即可循环取得下一页面的索引值
var offset = positionOffset//Kotlin的变量均为final类型,所以需要使用局部变量代替
val last = mNumbers - 1
//当滑动到最后一个点时,则清除偏移量,直接设置位置即可
if (index == last && positionOffset > 0) {
offset = 0f
}
//计算设置偏移量并刷新界面,从而达到圆点实时滑动的效果
//参数的来源为Banner的onPageChangeListener,position是当前起始滑动的点,positionOffset为滑动至下一个点的百分比
mOffset = (index * mRadius * 3) + (offset * mRadius * 3)
invalidate()
}
具体事项已经写到注释上,由于指示器是单独实现,我也没有整合到Banner中,所以想要实现滑动功能,需要配合Banner的滑动监听器,动态改变Indicator的位置,来达到同步滚动指示的效果。
最后,我说一下对wrap_content属性的支持,代码为先:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//获取宽高的设置模式和大小
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSpec = MeasureSpec.getSize(widthMeasureSpec)
val heightSpec = MeasureSpec.getSize(heightMeasureSpec)
var width = widthSpec
var height = heightSpec
//获取屏幕dpi并计算缩放指数
dpiX = (resources.displayMetrics.densityDpi / 160).toFloat()
//当宽度设置为wrap_content时,下同
if (widthMode == MeasureSpec.AT_MOST) {
//动态计算Indicator的宽度,具体值为两侧padding+所有圆的直径+间隔个数(圆个数-1)*半径
width = ((mPaddingLeft * 2) +
(mNumbers * mRadius * 2) + ((mNumbers - 1) * mRadius)).toInt()
}
//高度设置为:圆的直径*缩放指数(上下留白)+0.5:四舍五入
if (heightMode == MeasureSpec.AT_MOST) {
height = (mRadius * 2 * dpiX + 0.5).toInt()
}
//设置给View
setMeasuredDimension(width, height)
}
到此为止,Indicator的全部实现便全部完成了,让我们结合上一节的Banner做一次同步吧。
首先是布局文件:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<android.support.v4.view.ViewPager
android:id="@+id/id_vp_banner"
android:layout_width="match_parent"
android:layout_height="200dp">
</android.support.v4.view.ViewPager>
<cn.zhaoshuhao.kotlinfirst.view.Indicator
android:id="@+id/id_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
app:backColor="@android:color/black"
app:forceColor="#ff0000"
app:numbers="3"
app:radius="25"/>
</FrameLayout>
然后是代码:
id_vp_banner.addOnPageChangeListener
(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
id_indicator.setOffset(position, positionOffset)
}
override fun onPageSelected(position: Int) {}
override fun onPageScrollStateChanged(state: Int) {}
})
让我们看下效果吧
好了,本次就到这里,简单广告条的实现也到此为止,希望大家可以继续补充并留言。篇幅较长,以后会优化篇幅并提炼内容,写的我自己都看不下去了,不过从我个人的角度来说,还是比较喜欢这样的从头至尾,说的很清楚的文章,虽然可能耐不下性子看,哈
ok,拜拜,下次我会更好。写于:2017/07/24 17:38