作者:Android小Y 链接:https://www.jianshu.com/p/19a559377ae5

前言

在现在的App设计中,轮播基本成为了每个应用的“标配”,有了轮播,就自然需要有对应的指示器,代表当前轮播的进度,现在市面上指示器的样式大部分都是基于小圆点的形式,实现这个基本的效果网上也有很多轮子,本文主要是在实现基本效果的基础上,在切换圆点之间添加一个粘性过渡的动画效果。
效果预览

实现思路

绘制圆点
圆点的话基于画笔绘制,将控件宽度平分为N等份,且选中的圆点半径稍大。
圆点之间的联动滚动
支持设置最多显示N个圆点,当圆点总数超过N个时,暂时不显示在控件可见范围内,直到左/右滚动到靠近边界时,自动平移所有圆点,从而让最新选中的圆点再次回到居中的位置。使用属性动画结合横坐标偏移实现。
圆点过渡动画
圆点与圆点之间,如果单纯切换选中,会显得有些生硬,所以要为这个过程添加一些过渡的动画效果,这里采用当下常见的一种“粘性”效果,类似于我们在QQ联系人列表长按拖动未读消息数的效果:
这里基于贝塞尔曲线来实现,通过计算准备过渡的两个圆点的位置,以及它们之间的中心点,可以绘制出上下两条贝塞尔曲线,再闭合起来即可。然后结合属性动画进行移动,完成最终的过渡效果。

实现步骤

1.计算控件宽高
按照设计的效果,控件的宽高取决于小圆点的排列:
控件宽度 = 屏幕中可见的所有小圆点的宽度 * 可见小圆点的数量 + 小圆点之间的间距 * (可见小圆点的数量 - 1)


控件高度 = 最大的小圆点的高度


@Override
protectedvoidonMeasure(int widthMeasureSpec, int heightMeasureSpec)
{

super
.onMeasure(widthMeasureSpec, heightMeasureSpec);

int
 count = Math.min(totalCount, showCount);

int
 width = (
int
) ((count - 
1
) * smallDotWidth + (count - 
1
) * dotPadding + bigDotWidth);

int
 height = (
int
) bigDotWidth;

finalint
 widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);

finalint
 widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

finalint
 heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

finalint
 heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);

if
 (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {

        setMeasuredDimension(width, height);

    } 
elseif
 (widthSpecMode == MeasureSpec.AT_MOST) {

        setMeasuredDimension(width, heightSpecSize);

    } 
elseif
 (heightSpecMode == MeasureSpec.AT_MOST) {

        setMeasuredDimension(widthSpecSize, height);

    } 
else
 {

        setMeasuredDimension(width, height);

    }

}

另外注意最大显示数量的控制,即 int count = Math.min(totalCount, showCount); 如果当前圆点总数超过屏幕可见数,则基于最大可见数来计算控件的宽度。
2.绘制小圆点
在知道小圆点的数量之后,只需要遍历依次绘制即可。考虑到选中的圆点与其他圆点样式上的区别,因此针对当前选中圆点单独设置宽度 bigDotWidth,单独设置颜色 selectColor,如下:
@Override
protectedvoidonDraw(Canvas canvas)
{

super
.onDraw(canvas);

float
 startX = curX;

float
 selectX = 
0
;

for
 (
int
 i = 
0
; i < totalCount; i++) {

if
 (curIndex == i) {

//绘制选中圆点
            paint.setColor(selectColor);

            paint.setStyle(Paint.Style.FILL);


            selectRectF.left = startX;

            selectRectF.top = getHeight() / 
2f
 - bigDotWidth / 
2
;

            selectRectF.right = startX + bigDotWidth;

            selectRectF.bottom = getHeight() / 
2f
 + bigDotWidth / 
2
;

            canvas.drawCircle(startX + (bigDotWidth) / 
2
, bigDotWidth / 
2
, (bigDotWidth) / 
2
, paint);

            selectX = startX + bigDotWidth / 
2
;

            startX += (bigDotWidth + dotPadding);

        } 
else
 {

//绘制其它圆点
            paint.setColor(defaultColor);

            paint.setStyle(Paint.Style.FILL);


            startX += smallDotWidth / 
2
;

            canvas.drawCircle(startX, bigDotWidth / 
2
, (smallDotWidth) / 
2
, paint);

            startX += (smallDotWidth / 
2
 + dotPadding);

        }

    }

}

3.左右平移动画
从效果图可以看出,指示器平移的触发时机在于每一次的左右切换,具体需要满足如下条件:
  • 1.当前圆点总数超过最大可见数
  • 2.当前准备切换的下一个圆点在屏幕非中间的位置
第一个条件,圆点总数超过最大可见数才可平移,这个很好理解。第二个是切换的下一个圆点在屏幕非中间位置,这个是平移的一个规则,比如下面的例子:
上图在切换之前,选中的是3,准备切换到4的过程中,由于当前总数为7个,超过最大可见数5个,满足第一个条件,同时由于在切换之前4是处在非屏幕中间的位置,因此满足第二个条件,需要整体向左平移一个单位,使得切换之后,4变成了屏幕中心的位置,逻辑如下:
publicvoidsetCurIndex(int index)
{

if
 (index == curIndex) {

return
;

    }

//当前圆点总数超过最大可见数
if
 (totalCount > showCount) {

if
 (index > curIndex) {

//往左边滑动
int
 start = showCount % 
2
 == 
0
 ? showCount/
2
 - 
1
 : showCount / 
2
;

int
 end = totalCount - showCount / 
2
;

//判断是否需要先滚动
if
 (index > start && index < end) {

                startScrollAnim(Duration.LEFT, () -> invalidateIndex(index));

            } 
else
 {

                invalidateIndex(index);

            }

        } 
else
 {

//往右边滑动
int
 start = showCount / 
2
;

int
 end = showCount % 
2
 == 
0
 ? totalCount - showCount / 
2
 + 
1
 : totalCount - showCount / 
2
;

//判断是否需要先滚动
if
 (index > start - 
1
 && index < end - 
1
) {

                startScrollAnim(Duration.RIGHT, () -> invalidateIndex(index));

            } 
else
 {

                invalidateIndex(index);

            }

        }

    } 
else
 {

        invalidateIndex(index);

    }

}

4.圆点过渡动画
圆点之间的粘性动画,本质上是以前一个圆点作为基准位置,然后平移另外一个圆点的水平位置,使得它们之间的闭合曲线逐渐变化,直到平移到与下一个圆点位置重合,如下:
由红色圆点切换到绿色圆点的过程中,以A点为起始点,连接A点与C点绘制一条贝塞尔曲线,同样,底部B点和D点之间也绘制一条贝塞尔曲线,然后再把AB和CD也连接起来,四条路径形成一条闭合的曲线绘制出来,形成基本的形状。
然后再结合属性动画,使得C点和D点不断向右移动,直到与绿色圆完全重合。 如下:
设置粘性属性动画的起始和结束值:
//当前选中的圆点的水平中心 作为粘性动画起始点
float
 startValues = getCurIndexX() + bigDotWidth / 
2
;

//根据方向设置动画的结束值
if
 (index > curIndex) {

    stickAnimator.setFloatValues(startValues, startValues + dotPadding + smallDotWidth);

else
 {

    stickAnimator.setFloatValues(startValues, startValues - dotPadding - smallDotWidth);

}

监听动画不断刷新粘性过渡的动画值:
ValueAnimator stickAnimator = 
new
 ValueAnimator();

stickAnimator.setDuration(animTime);

stickAnimator.addUpdateListener(animation -> {

    stickAnimX = (
float
) animation.getAnimatedValue();

    invalidate();

});

stickAnimator.removeAllListeners();

stickAnimator.addListener(
new
 AnimatorListenerAdapter() {

@Override
publicvoidonAnimationEnd(Animator animation)
{

        isSwitchFinish = 
true
;

        invalidate();

    }

});

stickAnimator.start();

绘制粘性曲线:
@Override
protectedvoidonDraw(Canvas canvas)
{

super
.onDraw(canvas);


if
 (isSwitchFinish) {

//切换完成记得重置路径
        stickPath.reset();

    } 
else
 {

        paint.setColor(selectColor);

//以当前选中的圆点为绘制起始点
float
 quadStartX = selectX;

float
 quadStartY = getHeight() / 
2f
 - bigDotWidth / 
2
;

        stickPath.reset();

//连接4个点
        stickPath.moveTo(quadStartX, quadStartY);

        stickPath.quadTo(quadStartX + (stickAnimX - quadStartX) / 
2
, bigDotWidth / 
2
, stickAnimX, quadStartY);

        stickPath.lineTo(stickAnimX, quadStartY + bigDotWidth);

        stickPath.quadTo(quadStartX + (stickAnimX - quadStartX) / 
2
, bigDotWidth / 
2
, quadStartX, quadStartY + bigDotWidth);

//形成闭合曲线
        stickPath.close();

//绘制过渡过程中的圆
        canvas.drawCircle(stickAnimX, bigDotWidth / 
2
, (bigDotWidth) / 
2
, paint);

        canvas.drawPath(stickPath, paint);

    }

}

结语

如果指定了最多显示多少个圆点,则当总数超过时会左右滚动,如果想要非滚动的形式也可以设置为最大圆点数。本控件主要还是通过贝塞尔曲线来制作粘性效果,让动画更为生动,支持设置是否开启粘性效果、粘性动画时长、小圆点选中与非选中时的样式等,后续会再根据需求扩充其它细节。
github地址:https://github.com/GitHubZJY/ZJYWidget
更文不易,点个“在看”支持一下👇
继续阅读
阅读原文