【FirstKotlinApp】使用Kotlin实现简单的Banner广告条(二):Indicator指示器实现

小米曰: 广告条有了,该告诉用户有几条广告了

作为一个米粉,这是小米被自己黑的最狠的一次。

言归正传,上次实现了一个简单的广告条,此次则实现一个简单的指示器,与其配套。

鉴于上次没有发布源码地址,这次先挂地址:https://github.com/zsh065400/KotlinFirst
这个App的所有功能更新都会同步,我会挑出一些功能写成博客,与大家共勉

自定义View(此次不包含ViewGroup)的方法有很多

  1. 继承现有的控件,实现附加功能:TextView、ImageView等
  2. 继承View,自主实现View的测量、绘制、状态更新等

指示器的实现采用了继承VIew的做法,自主实现View,需要注意以下几点

  1. onMeasure方法对wrap_content参数的支持
  2. onDraw方法的视图绘制,保证绘制正确与高效
  3. 自定义属性的定义与获取
  4. 具体的大小计算与视图绘制

OK,下面将实现拆分,一起来实现一个简单的Indicator指示器

Koltin在实现自定义View的时候要注意方式,不要使用主构造函数,将Java的方式变形成Kotlin即可

class Indicator : View {
    constructor(context: Context) : this(context, null) {

    }

    constructor(context: Context, attributeSet: AttributeSet?)
     : super(context, attributeSet) {

    }
}   

在这里有一些需要注意:

  1. Kotlin中一个类若存在主构造函数(跟随在类名后的括号),则其他的次构造函数需要直接或间接委托至主构造函数
  2. 若不存在主构造函数,且类继承自其他类,则所有的构造函数均要直接或间接调用父类构造函数

定义好了类,接着复写相应的方法处理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"/>

到此处,你已经能看到你写的指示器了,是不是很有成就感呢?接下来,让我们继续完善它,让她支持如下功能:

  1. 支持wrap_content属性设置
  2. 支持配合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) {}
    })

让我们看下效果吧
GIF没整出来,下次补上

好了,本次就到这里,简单广告条的实现也到此为止,希望大家可以继续补充并留言。篇幅较长,以后会优化篇幅并提炼内容,写的我自己都看不下去了,不过从我个人的角度来说,还是比较喜欢这样的从头至尾,说的很清楚的文章,虽然可能耐不下性子看,哈

ok,拜拜,下次我会更好。写于:2017/07/24 17:38

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值