自定义View和属性动画相结合实现支持动态修改指示点位置,拖拽或点击改变指示点位置,点击位置监听及切换动画自定义的圆点指示器。
最下面那个“吸干”,想不出用什么词形容更好>.<,后来改回”挤扁”
自定 View 代码写在 IndicatorView.java
中
指示器
的功能,为了实现“挤扁”的动画效果,绘制时用的是椭圆。线段
单位。循环绘制 线段
,绘制小圆点个数减一
次后连通所有小圆点,在布局文件或代码中可修改其可见性(lineVisible
) src/main/res/values/attrs.xml
文件中。
<resources>
<attr name="lineColor" format="color" />
<attr name="lineVisible" format="boolean" />
<attr name="lineWidth" format="dimension" />
<attr name="lineHeight" format="dimension" />
<attr name="dotSize" format="dimension" />
<attr name="dotNum" format="integer"/>
<attr name="dotColor" format="color" />
<attr name="indicatorColor" format="color" />
<attr name="indicatorSize" format="dimension" />
<attr name="indicatorPos" format="integer"/>
<attr name="duration" format="integer"/>
<attr name="dotClickEnable" format="boolean"/>
<attr name="indicatorDragEnable" format="boolean"/>
<attr name="touchEnable" format="boolean"/>
<attr name="IndicatorSwitchAnimation" format="integer">
<enum name="translation" value="1"/>
<enum name="squeeze" value="2"/>
<enum name="none" value="0"/>
attr>
<declare-styleable name="IndicatorView">
<attr name="dotColor" />
<attr name="dotSize" />
<attr name="dotNum" />
<attr name="lineColor" />
<attr name="lineVisible" />
<attr name="lineWidth" />
<attr name="lineHeight" />
<attr name="indicatorColor" />
<attr name="indicatorSize" />
<attr name="indicatorPos" />
<attr name="duration" />
<attr name="IndicatorSwitchAnimation"/>
<attr name="dotClickEnable"/>
<attr name="indicatorDragEnable"/>
<attr name="touchEnable"/>
declare-styleable>
resources>
可以在布局文件中直接使用:
<com.duan.indicatorviewdemo.IndicatorView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:IndicatorSwitchAnimation="squeeze"
app:dotColor="#2d2b2b"
app:dotNum="4"
app:dotSize="10dp"
app:duration="800"
app:indicatorColor="#ff9500"
app:indicatorPos="1"
app:indicatorSize="25dp"
app:lineColor="#b3b3b3"
app:lineHeight="4dp"
app:lineWidth="85dp" />
...........
/**
* 保存所有小圆点的圆点坐标,用于在touch事件中判断触摸了哪个点
*/
private int[][] clickableAreas;
/**
* 指示点,不断修改它的属性从而实现动画(属性动画)
*/
private IndicatorHolder indicatorHolder;
/**
* 指示点要移动到的目标位置
*/
private int switchTo = -1;
/**
* 手松开后根据该变量判断是否需要启动切换动画
*/
private boolean haveIndicatorAniming = false;
/**
* 指示点是否被拖拽过,当指示点被拖拽了但没有超过当前指示点位置范围时使之回到原位
*/
private boolean haveIndicatorDraged = false;
/**
* 保存转移动画开始时线的颜色
*/
private int tempLineColor;
.........
线段
加起来的总长度指示点
的高度。注意:使用默认的 指示点
触摸反馈动画时要加上高度差setPadding(getPaddingLeft() + mIndicatorSize / 3......
这一句是为了在使用默认的 指示点
触摸反馈动画,或是自定义动画中有使指示点放大的情况下要多留些空间给控件,否则 指示点
放大后超出控件高度的部分就不会被绘制(不会显示)。@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
setPadding(getPaddingLeft() + mIndicatorSize / 3,getPaddingTop(),getPaddingRight() + mIndicatorSize / 3,getPaddingBottom());
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//xml中宽度设为warp_content
width = getPaddingLeft() + ((mDotCount - 1) * mLineWidth + mIndicatorSize) + getPaddingRight();
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = getPaddingTop() + mIndicatorSize + getPaddingBottom();
}
//若使用默认的指示点触摸动画(放大+渐变颜色)需要加上放大后指示点与放大前指示点的高度差
//使用自定义时动画时则不加
setMeasuredDimension(width, mPressAnimator == null ? height + mIndicatorSize / 2 : height);
}
在onLayout方法中要对 indicatorHolder
变量进行初始化。
indicatorHolder
变量:private IndicatorHolder indicatorHolder;
IndicatorHolder
类的实例 indicatorHolder
即为属性对象的target
,控件默认定义好了三个属性动画: 指示点
的触摸反馈指示点
切换指示点
切换indicatorHolder
作为属性动画的target来实现动画上面三个动画都是通过不断修改
indicatorHolder
的属性(调用indicatorHolder
的 setXXX() 而 setXXX()方法中会调用 invalidate() 重绘 view)实现动画的。具体可参见:ValueAnimator和ObjectAnimator的高级用法
public class IndicatorHolder {
private int centerX;
private int centerY;
private int height;
private int color;
private int width;
private int alpha;
public void setAlpha(int alpha) {
this.alpha = alpha;
invalidate();
}
public int getAlpha() {
return alpha;
}
public void setHeight(int height) {
this.height = height;
invalidate();
}
...........
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//getHeight方法在onDraw方法中会取到错误的值
if (indicatorHolder != null) {
indicatorHolder.setColor(mIndicatorColor);
indicatorHolder.setCenterX(mIndicatorPos * mLineWidth + getPaddingLeft() + mIndicatorSize / 2);
indicatorHolder.setCenterY(getHeight() / 2);
indicatorHolder.setHeight(mIndicatorSize);
indicatorHolder.setWidth(mIndicatorSize);
indicatorHolder.setAlpha(255);
}
}
onDraw()
方法绘制控件的关键方法
1 控件中包含多个圆,使画笔支持抗锯齿使视觉效果更好 setAntiAlias(true)
2 画线的时候先判断是否设置了 线段不可见
属性
3 画 小圆点
,关键的一句 if (switchTo != -1 && i == switchTo)
:
小圆点
被点击或 指示点
被拖拽 同时被点击小圆点的位置与当前正在绘制的小圆点的位置相同
或指示点拖拽时指示点中心点(椭圆外切矩形对角线交点)所在区域(由 clickableAreas 规定的区域) 与 当前正在绘制的小圆点的位置相同
时该 if 才为 trueanimEnd()
方法中被重置为 -1clickableAreas
赋值,记录当前小圆点的圆心坐标4 画指示点:
indicatorHolder
的 getXXX()
方法获得指示点的当前形态进行绘制start()
之后会不断调用 indicatorHolder
的 setXXX()
方法,同时调用 invalidate
方法,onDraw(...)
方法就会被不断调用,视图不断刷新,指示点、小圆点以及线段的形态就会不断改变,动画就形成了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//去锯齿
mPaint.setAntiAlias(true);
//画线(如果可见)
if (mLineVisible) {
mPaint.setColor(mLineColor);
for (int i = 0; i < mDotCount - 1; i++) {
int left = getPaddingLeft() + mIndicatorSize / 2 + mLineWidth * i;
int top = (getHeight() - mLineHeight) / 2;
int right = getPaddingLeft() + mIndicatorSize / 2 + mLineWidth * (i + 1);
int bottom = (getHeight() + mLineHeight) / 2;
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
//画小圆点
for (int i = 0; i < clickableAreas.length; i++) {
int cx = i * mLineWidth + getPaddingLeft() + mIndicatorSize / 2;
int cy = getHeight() / 2;
if (switchTo != -1 && i == switchTo)
mPaint.setColor(mIndicatorColor);
else
mPaint.setColor(mDotColor);
canvas.drawCircle(cx, cy, mDotSize, mPaint);
clickableAreas[i][0] = cx;
clickableAreas[i][1] = cy;
}
//画指示点
mPaint.setColor(indicatorHolder.getColor());
mPaint.setAlpha(indicatorHolder.getAlpha());
canvas.drawOval(
indicatorHolder.getCenterX() - indicatorHolder.getWidth() / 2,
indicatorHolder.getCenterY() - indicatorHolder.getHeight() / 2,
indicatorHolder.getCenterX() + indicatorHolder.getWidth() / 2,
indicatorHolder.getCenterY() + indicatorHolder.getHeight() / 2,
mPaint
);
}
onTouchEvent
1 touchEnable
属性设为 false 则不需要响应触摸事件
2 动画正在进行时不再响应触摸事件,否则会乱套…..
3 if (switchTo != mIndicatorPos && !mDotClickEnable && !haveIndicatorDraged)
:
&& !haveIndicatorDraged
的条件?往下看…不可点击
,可拖拽
时,当你开始拖拽,而且拖拽位置超出当前指示点范围(clickableAreas
规定的范围),假设此时如果只判断 switchTo != mIndicatorPos && !mDotClickEnable
,那么此时这两条件都满足,返回。那此时效果就是指示点不能拖出clickableAreas
规定的范围,显然不满足 不可点击
,但可拖拽
的要求,所有还要加一个 是否被拖拽过的条件
。4 接下来依次判断手势状态
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTouchEnable)
return true;
//动画正在进行时不在响应点击事件
if (haveIndicatorAniming)
return true;
int ex = (int) event.getX();
int temp = mLineWidth / 2;
switchTo = 0;
//判断当前手指所在的小圆点是哪个
for (; switchTo < mDotCount; switchTo++) {
int[] xy = clickableAreas[switchTo];
//只对x坐标位置进行判断,这样即使用户手指在控件外面(先在控件内触摸后不抬起而是滑到控件外面)滑动也能判断
if (ex <= xy[0] + temp && ex >= xy[0] - temp) {
break;
}
}
if (switchTo != mIndicatorPos && !mDotClickEnable && !haveIndicatorDraged)
return true;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//按下且不是指示点所在的小圆点
if (mIndicatorPos != switchTo) {
startSwitchAnimation();
if (mListener != null)
mListener.onDotClickChange(this, switchTo);
} else {//按下且是指示点所在的小圆点
if (mIndicatorDragEnable)
startPressAnimation();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) { //手抬起
if (switchTo != mIndicatorPos || haveIndicatorDraged) {
haveIndicatorDraged = false;
if (mIndicatorDragEnable)
startSwitchAnimation();
}
} else { //按着+拖拽
if (mIndicatorDragEnable) {
haveIndicatorDraged = true;
indicatorHolder.setCenterX(ex);
}
}
return true;
}
代码有些多又繁琐就没贴完,上传到GitHub了,可以在这里下载到源码和示例:DuanJiaNing/IndicatorViewDemo
startPressAnimation()
startSwitchAnimation()
animEnd()
/**
* 指示点触摸(挤压)动画
*/
private void startPressAnimation() {
........
}
/**
* 指示点切换动画
*/
private void startSwitchAnimation() {
//平移
int startX = indicatorHolder.getCenterX();
int endX = switchTo * mLineWidth + getPaddingLeft() + mIndicatorSize / 2;
ValueAnimator trainsAnim = ObjectAnimator.ofInt(indicatorHolder, "centerX", startX, endX);
trainsAnim.setDuration(mDuration);
tempLineColor = mLineColor;
AnimatorSet defaultIndicatorSwitchAnim = new AnimatorSet();
defaultIndicatorSwitchAnim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mLineColor = indicatorHolder.getColor();
haveIndicatorAniming = true;
}
@Override
public void onAnimationEnd(Animator animation) {
animEnd();
}
@Override
public void onAnimationCancel(Animator animation) {
animEnd();
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
if (mSwitchAnimator == null) {
switch (mIndicatorSwitchAnim) {
case INDICATOR_SWITCH_ANIM_NONE:
indicatorHolder.setCenterX(endX);
animEnd();
break;
case INDICATOR_SWITCH_ANIM_SQUEEZE:
//“挤扁”
int centerH = mLineHeight * Math.abs(switchTo - mIndicatorPos);
int centerW = Math.abs(indicatorHolder.getCenterX() - clickableAreas[switchTo][0]);
ValueAnimator heightAnim = ObjectAnimator.ofInt(indicatorHolder, "height", mIndicatorSize, centerH, 0);
ValueAnimator widthAnim = ObjectAnimator.ofInt(indicatorHolder, "width", mIndicatorSize, centerW, 0);
heightAnim.setDuration(mDuration);
widthAnim.setDuration(mDuration);
//缩放
ValueAnimator scaleAnimH = ObjectAnimator.ofInt(indicatorHolder, "height", mDotSize, mIndicatorSize);
ValueAnimator scaleAnimW = ObjectAnimator.ofInt(indicatorHolder, "width", mDotSize, mIndicatorSize);
AnimatorSet scaleSet = new AnimatorSet();
scaleSet.play(scaleAnimH).with(scaleAnimW);
scaleSet.setDuration(500);
defaultIndicatorSwitchAnim.play(trainsAnim).with(heightAnim).with(widthAnim);
defaultIndicatorSwitchAnim.play(scaleSet).after(trainsAnim);
defaultIndicatorSwitchAnim.start();
break;
case INDICATOR_SWITCH_ANIM_TRANSLATION:
defaultIndicatorSwitchAnim.play(trainsAnim);
defaultIndicatorSwitchAnim.start();
break;
}
} else { //自定义
tempLineColor = mLineColor;
AnimatorSet customAnim = mSwitchAnimator.onIndicatorSwitch(this, indicatorHolder);
customAnim.play(trainsAnim);
customAnim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mLineColor = indicatorHolder.getColor();
haveIndicatorAniming = true;
}
@Override
public void onAnimationEnd(Animator animation) {
animEnd();
}
@Override
public void onAnimationCancel(Animator animation) {
animEnd();
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
customAnim.start();
}
}
/**
* 指示点切换动画结束或取消时重置和恢复一些变量的值
*/
private void animEnd() {
mLineColor = tempLineColor;
mIndicatorPos = switchTo;
switchTo = -1;
haveIndicatorAniming = false;
}
DuanJiaNing/IndicatorViewDemo 中示例Activity的位置:
\src\main\java\com\duan\indicatorviewdemo\MainActivity.java
public class MainActivity extends AppCompatActivity {
private IndicatorView indicator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
indicator = (IndicatorView) findViewById(R.id.indicator);
indicator.setOnDotClickListener((View v, int position) -> Toast.makeText(this, "点击了 " + position, Toast.LENGTH_SHORT).show());
indicator.setOnIndicatorSwitchAnimator((IndicatorView view, IndicatorView.IndicatorHolder target) -> {
int terminalColor = indicator.getIndicatorColor();
int centerColor = indicator.getDotColor();
ValueAnimator colorAnim = ObjectAnimator.ofArgb(target, "color", terminalColor, centerColor, terminalColor);
int terminalSize = indicator.getIndicatorPixeSize();
int centerSize = indicator.getIndicatorPixeSize() * 3 / 2;
ValueAnimator animatorH = ObjectAnimator.ofInt(target, "height", terminalSize, centerSize, terminalSize);
ValueAnimator animatorW = ObjectAnimator.ofInt(target, "width", terminalSize, centerSize, terminalSize);
AnimatorSet set = new AnimatorSet();
set.play(colorAnim).with(animatorH).with(animatorW);
set.setDuration(500);
return set;
});
//indicator1.setIndicatorSwitchAnim(random.nextInt(IndicatorView.INDICATOR_SWITCH_ANIM_TRANSLATION);
...
}
context.getTheme().obtainStyledAttributes(...)
方法获取onMeasure()
方法中对宽高的计算onDraw
绘制圆、矩形、椭圆时坐标的确定animEnd
:动画结束或取消时重置和恢复一些变量的值lineVisible
为 false ,也要为lineWidth
赋值,当然也可以使用默认的,因为lineWidth
是onMeasure
方法计算控件width
的重要变量。setOnIndicatorSwitchAnimator
或setOnIndicatorPressAnimator
自定义动画时,要将定义好的AnimatorSet
动画作为返回值返回,由控件控制动画在何时播放和添加监听事件。你可以为IndicatorHodler
添加更多属性并修改onDraw()
方法以实现更丰富的动画
源码和示例已上传GitHub,可以在这里下载到:DuanJiaNing/IndicatorViewDemo
觉得还不错的话就给颗star吧>.<