废话少说,先上图:
(请看底部的4个点)
忘记是在那个APP上看到ViewPager底部的圆点指示器可以随着滚动而滚动的效果,便开始思考要怎么实现,最终发现效果实现很简单,拿来练手自定义View挺不错的。
写码之前:
写代码之前必须至少先有大概的思路,而且不要想到一点就开始写,必须对整体都大概心里有数再开始写。比如在实现这个效果时,刚开始我是想着重写线性布局,然后动态添加圆点,通过margin控制间隔。但是我发现这种办法在滚动时的处理逻辑编写起来比较复杂,既然只是几个圆点而已,直接继承View用画的方式画出来更简单。最终写出来的类加上一大把自动生成的代码,也才一百多行。
写代码并不难,想到正确的思路才难。
高清源码:
(1)初始化
public AnimDian(Context context, AttributeSet attrs) { super(context, attrs); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AnimDian); dianCount = array.getInteger(R.styleable.AnimDian_dian_count, 5); dianColor = array.getColor(R.styleable.AnimDian_dian_color, 0XFFFF0000); dianBgColor = array.getColor(R.styleable.AnimDian_dian_bg_color, 0X88FFFFFF); margin = array.getInteger(R.styleable.AnimDian_dian_margin, 20); dianSize = array.getInteger(R.styleable.AnimDian_dian_size, 20); array.recycle(); init(); }
一些自定义属性可以在xml中设置,关于自定义属性的教学网上一大把,我就不赘述了。
private void init() { // 初始化两支画笔 dianPaint = new Paint(); dianPaint.setAntiAlias(true); dianPaint.setColor(dianColor); bgPaint = new Paint(); bgPaint.setAntiAlias(true); bgPaint.setColor(dianBgColor); }
初始化两支画笔,一支画显示选中的前景点,一支画背景点。
(2)测量大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 测量自身大小 // 宽=点的宽度(直径)*点的个数+点之间的距离*(点的个数-1) int width = dianSize * dianCount + margin * (dianCount - 1); // 高=点的高度(直径) int height = dianSize; int wMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); int hMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); // 设置计算结果 setMeasuredDimension(wMeasureSpec, hMeasureSpec); }
这个View的大小测量很简单,我也不浪费口水了。
(3)画
写一个自定义View是一个试验的过程,很少有一次性过把整个效果写完的,这里我们先考虑静态的,即没有滚动效果的该怎么写,然后再考虑怎么加上滚动效果。
先是静态的实现:
public void draw(Canvas canvas) { // 画背景点 for (int i = 0; i < dianCount; i++) { drawBgDian(canvas, i); } //画选中状态的点 if (selectPosition > -1) { drawDian(canvas); } }
静态的实现很简单,就是把几个背景的点画出来,再在选中位置的地方再画一个不同颜色的点遮盖住背景点。
注意:后画的东西会遮挡住先画的。
这里稍微复杂一点的就是位置的计算,不过只要认真思考现在已有的数据,以及是否有什么规律,画点草图,一般都不难解决。
画背景点:
private void drawBgDian(Canvas canvas, int i) { canvas.drawCircle((dianSize + margin) * i + dianSize / 2, dianSize / 2, dianSize / 2, bgPaint); }
圆心的Y坐标容易算,但X坐标需要画点草图想一下,让你的大脑动一下吧。
画选中状态的点跟背景点一样,只是只画一个,且用了不用颜色的画笔而已:
private void drawDian(Canvas canvas) { canvas.drawCircle((dianSize + margin) * selectPosition + dianSize / 2, dianSize / 2, dianSize / 2, dianPaint); }
就这样,静态效果已经完成了。现在开始思考怎么实现滚动效果:
首先,要让红点滚动,必须有滚动的数据,比如滚动的方向,滚动的距离。于是得先得到滚动的数据来源。
因为我们是用在ViewPager上的,所以很容易想到给ViewPager设置OnPageChangeListener,再把数据传给我们的AnimDian:
public void onPageScrolled(int arg0, float arg1, int arg2) { animDian.onPageScrolled(arg0, arg1, arg2); }
接下来就得搞清楚3个参数的含义,打印一下数值,得出结果是:
小结:
这里的返回参数并不受滑动方向的影响,一直以左边作为基准,在滑动时会有两个page显示在屏幕上:
arg0:左边page的index
arg1:左边page没有显示出来的部分的百分比,或者理解为右边page显示出来的部分的百分比
arg2:同arg1,不过是具体的像素值
我们这里只需要arg0和arg1,像素值我们用不着。
有了滚动的状态数据,就可以计算滚动点的位置了:
public void onPageScrolled(int arg0, float arg1, int arg2) { if (arg1 > 0 && arg1 < 1) { scrollState = STATE_SCROLLING; // 滚动时计算前景点距离左边的距离 scrollDianCX = (dianSize + margin) * (float) (arg0 + arg1) + dianSize / 2; invalidate(); } }
知道参数含义后怎么计算位置,还是需要我们再动下脑。
得到位置后就可以画出滚动点了:
private void drawScrollDian(Canvas canvas) { canvas.drawCircle(scrollDianCX, dianSize / 2, dianSize / 2, dianPaint); }
当然在滚动时,不应该画出选中位置的点,所以修改draw方法逻辑:
public void draw(Canvas canvas) { // 画背景点 for (int i = 0; i < dianCount; i++) { drawBgDian(canvas, i); } // 如果不是在滚动状态,画选中位置的前景点 if (selectPosition > -1 && scrollState != STATE_SCROLLING) { drawDian(canvas); } // 在滚动状态,画滚动点 if (scrollState == STATE_SCROLLING) { drawScrollDian(canvas); } }
然后在滚动结束和选中位置发生改变时,同样要通知我们的AnimDian:
public void onPageScrollStateChanged(int arg0) { //arg0==0表示滚动状态为结束 if (arg0 == 0) { animDian.onPageScrollEnd(); } } public void onPageSelected(int arg0) { animDian.setSelectPosition(arg0); }
在滚动结束时要修改AnimDian的滚动状态:
public void onPageScrollEnd() { scrollState = STATE_READY; invalidate(); }
同样,在选中位置改变时也要进行处理:
public void setSelectPosition(int selectPosition) { this.selectPosition = selectPosition; //外部可以设置监听选中位置的改变,一般用不着,直接监听ViewPager的就行了 if (mListener != null) mListener.onSelectChange(selectPosition); //如果当前正在滚动,就没必要请求重绘了,等到滚动结束后会去请求重绘的 if (scrollState == STATE_SCROLLING) return; invalidate(); }
至此,这个自定义View就完成了。虽然有点简单,但是作为新手刚开始还是不要挑战太复杂的自定义View,先写点简单的找点成就感和自信,多思考和理解下原理。
准备回家过年了!!!