Android贝塞尔曲线实现粘性 小圆点指示器

来源:互联网 发布:winsteel软件下载 编辑:程序博客网 时间:2024/06/06 17:52

自定义的一个粘性的指示器。

先看看效果。效果赞不赞因人而异,想更酷一些的话,可以花时间实现一下,这里就是想给大家分享一下实现的思路。

1.继承View。获取、计算 一些必要的数据。如:padding,view大小,圆点大小颜色,圆圈大小颜色,动画时间,圆点数量等。

2.获取、保存每个圆圈 的位置坐标。

3..放开选择index公共方法。调用此方法后,根据当前index和将要到达的index,获取到两个圆圈的位置坐标。启动圆圈粘性动画和圆点移动动画。

4.用ValueAnimator 从0到1,的百分百数据。根据两个点之间的总距离,算出当前位置坐标,形变程度等数据。draw出来(会画圆点和所有的圆圈)。

5.在圆点到位后,启动圆点形变复原动画。

6.动画结束后,即时改变当前index等数据。

注意点就是在动画执行完之前,用户再次选择。这个在用户选择之前判断动画是否完成,如果没完成,调用onCancel方法,onCancel中将数据即时改变。


下图可以更好的理解代码中的数据:


下面是自定义的view代码:

import java.util.ArrayList;import android.annotation.SuppressLint;import android.content.Context;import android.content.res.TypedArray;import android.graphics.BlurMaskFilter;import android.graphics.BlurMaskFilter.Blur;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.EmbossMaskFilter;import android.graphics.Paint;import android.graphics.Paint.Style;import android.graphics.Path;import android.os.Build;import android.util.AttributeSet;import android.view.View;import android.view.animation.AccelerateDecelerateInterpolator;import android.view.animation.OvershootInterpolator;import com.nineoldandroids.animation.Animator;import com.nineoldandroids.animation.Animator.AnimatorListener;import com.nineoldandroids.animation.ObjectAnimator;import com.nineoldandroids.animation.ValueAnimator;import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener;public class BezierLinearPointsView extends View {// 绘制圆形贝塞尔曲线控制点的位置private static final float C = 0.551915024494f;private int mViewWidth = 0, mViewHeight = 0;private int mViewLeftPadding = 0, mViewRightPadding = 0,mViewTopPadding = 0, mViewButtomPadding = 0;// 点数,默认为3private int mCount = 3;// 选中小点颜色private int mSelectedColor = Color.GREEN;// 未选中小点颜色private int mUnSelectedColor = Color.YELLOW;// 小点大小private int mPointSize = 50;// 画笔宽度private int mPaintStrokeWidth = 6;// 位移动画时间private long mDuration = 500;// 恢复动画时间private long mDurationRe = 200;// 大小抖动动画时间private long mDurationSize = 500;// 是否循环private boolean isLooper = false;// 外发光宽度 是 线条宽度的几倍private int mBlurRadio = 2;// 两个圆的paintprivate Paint mUnselectedPaint, mSelectedPaint;private Path mPath = new Path();// 存放未选中点的 位置数据private ArrayList<int[]> mUnselectedPoints = new ArrayList<int[]>();// 当前位置private int mCurrentIndex = 0, mToIndex = 0;// 两个圆之间圆心距离private int mDis = 20;// 当前选中点的位置private int[] mCurrentPointCenter;// 将要选中点的位置private int[] mToPointCenter;// 动画执行进度,用来控制圆的形变程度private float mPercent = 0f;// 反弹动画执行进度,用来控制圆的复原形变程度private float mRePercent = 0f;// 圆圈从未选中到选中抖动动画程度private float mSizePercent = 1f;// 圆形的控制点与数据点的差值private float mDifference;// 顺时针记录绘制圆形的四个数据点private float[] mData = new float[8];// 顺时针记录绘制圆形的八个控制点private float[] mCtrl = new float[16];// 来标识形变 恢复的时候是改动哪个点。0:两点相同,不变。1:index增加恢复,更改的是圆的左点。-1:index减少恢复,更改圆右点private int isIndexInCreate = 0;// 位移动画private ValueAnimator va;// 恢复动画private ValueAnimator vaRe;// 圆圈从 选中到不选中,粘性动画private ValueAnimator vaSize;public BezierLinearPointsView(Context context) {super(context);init(context, null);}public BezierLinearPointsView(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}public BezierLinearPointsView(Context context, AttributeSet attrs,int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs);}//public BezierLinearPointsView(Context context, AttributeSet attrs,//int defStyleAttr, int defStyleRes) {//super(context, attrs, defStyleAttr, defStyleRes);//init(context, attrs);//}/** * 初始化一些数据 */@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mViewHeight = h;mViewWidth = w;//paddingmViewButtomPadding = getPaddingBottom() + getPaintStrokeWidth()* getBlurRadio();mViewRightPadding = getPaddingRight() + getPaintStrokeWidth()* getBlurRadio();mViewLeftPadding = getPaddingLeft() + getPaintStrokeWidth()* getBlurRadio();mViewTopPadding = getPaddingTop() + getPaintStrokeWidth()* getBlurRadio();// 在宽度来说,点的最大半径int pointSizeWidth = (mViewWidth - mViewLeftPadding - mViewRightPadding)/ getCount();// 在高度来说,点的最大半径int pointSizeHeight = mViewHeight - mViewButtomPadding- mViewTopPadding;// 综合得到的最大半径int resultSize = Math.min(pointSizeWidth, pointSizeHeight);// 与规定半径相比,得出最小的半径.setPointSize(Math.min(getPointSize(), resultSize));// 得到圆控制点的距离mDifference = getPointSize() / 2 * C;// 两两圆点 圆心距离mDis = (mViewWidth - mViewLeftPadding - mViewRightPadding- getPointSize() - getPaintStrokeWidth())/ (getCount() - 1);}@SuppressLint("NewApi")private void init(Context context, AttributeSet attrs) {if(attrs!=null){TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.linearpointsview);setCount(ta.getInt(R.styleable.linearpointsview_count, getCount()));mSelectedColor = ta.getColor(R.styleable.linearpointsview_select_color,mSelectedColor);setUnSelectedColor(ta.getColor(R.styleable.linearpointsview_unselect_color,getUnSelectedColor()));setPointSize(ta.getDimensionPixelOffset(R.styleable.linearpointsview_pointsize, getPointSize()));setPaintStrokeWidth(ta.getDimensionPixelOffset(R.styleable.linearpointsview_pointstrokewidth,getPaintStrokeWidth()));setDuration(ta.getInt(R.styleable.linearpointsview_anim_move_duration,(int) getDuration()));setDurationRe(ta.getInt(R.styleable.linearpointsview_anim_move_duration,(int) getDurationRe()));setDurationSize(ta.getInt(R.styleable.linearpointsview_anim_move_duration,(int) geDurationSize()));setLooper(ta.getBoolean(R.styleable.linearpointsview_islooper,isLooper()));setBlurRadio(ta.getInt(R.styleable.linearpointsview_blur_radio,getBlurRadio()));ta.recycle();}mUnselectedPaint = new Paint();mUnselectedPaint.setAntiAlias(true);mUnselectedPaint.setDither(true);mUnselectedPaint.setStyle(Style.STROKE);mUnselectedPaint.setMaskFilter(new EmbossMaskFilter(new float[] { 1, 1,1 }, 0.4f, 6, 3.5f));mUnselectedPaint.setStrokeWidth(getPaintStrokeWidth());mUnselectedPaint.setColor(getUnSelectedColor());//不加这个maskFilter没效果if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {setLayerType(View.LAYER_TYPE_SOFTWARE, mUnselectedPaint);}mSelectedPaint = new Paint();mSelectedPaint.setAntiAlias(true);mSelectedPaint.setStyle(Style.FILL);mSelectedPaint.setMaskFilter(new BlurMaskFilter(getPaintStrokeWidth(),Blur.SOLID));mSelectedPaint.setColor(mSelectedColor);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {setLayerType(View.LAYER_TYPE_SOFTWARE, mSelectedPaint);}}/** * 选中此点 *  * @param index *            选中的索引 */public void selectIndex(int index) {if (index < 0 || index >= mUnselectedPoints.size()) {return;}//当上衣个动画没有结束,就不会进行mCurrent改变。主动调用cancel,结束动画,及时改变mCurrent的值if (va != null) {if (va.isRunning()) {va.cancel();}}if (vaRe != null) {if (vaRe.isRunning()) {vaRe.cancel();}}this.mToIndex = index;if (mCurrentIndex == mToIndex) {//相等则不变化,可以加一些抖动动画mPercent = 0;postInvalidate();} else {//启动选中的圆圈弹性动画(在圆点脱离圆圈时,会有一定的粘性的感觉)vaSize = ObjectAnimator.ofFloat(1f, 0.8f, 1f);vaSize.addUpdateListener(new AnimatorUpdateListener() {public void onAnimationUpdate(ValueAnimator arg0) {mSizePercent = arg0.getAnimatedFraction();postInvalidate();}});vaSize.setDuration(geDurationSize());vaSize.setInterpolator(new OvershootInterpolator());vaSize.start();//启动圆点的移动动画va = ObjectAnimator.ofFloat(0f, 1f);va.addUpdateListener(new AnimatorUpdateListener() {public void onAnimationUpdate(ValueAnimator arg0) {float f = arg0.getAnimatedFraction();mPercent = f;postInvalidate();}});va.setDuration((long) getDuration());va.setInterpolator(new AccelerateDecelerateInterpolator());va.addListener(new AnimatorListener() {public void onAnimationStart(Animator arg0) {}@Overridepublic void onAnimationRepeat(Animator arg0) {}/** * 移动动画结束后,启动圆点复原动画,并将选中数据及时赋值 */@Overridepublic void onAnimationEnd(Animator arg0) {//这里isIndexInCreate是用来标示  小圆点移动完成后,复原数据的控制。if (mCurrentIndex == mToIndex) {isIndexInCreate = 0;} else if (mCurrentIndex > mToIndex) {//左移isIndexInCreate = -1;} else {isIndexInCreate = 1;}//及时改变数据mCurrentIndex = mToIndex;//复原动画vaRe = ObjectAnimator.ofFloat(0f, 1f);vaRe.setInterpolator(new OvershootInterpolator());vaRe.addUpdateListener(new AnimatorUpdateListener() {public void onAnimationUpdate(ValueAnimator arg0) {float f = arg0.getAnimatedFraction();mRePercent = f;postInvalidate();}});vaRe.setDuration(getDurationRe());vaRe.start();}@Overridepublic void onAnimationCancel(Animator arg0) {//这里isIndexInCreate是用来标示 在快速切换时 小圆点移动完成后,复原数据的控制。if (mCurrentIndex == mToIndex) {isIndexInCreate = 0;} else if (mCurrentIndex > mToIndex) {//左移isIndexInCreate = -1;} else {isIndexInCreate = 1;}mCurrentIndex = mToIndex;//postInvalidate();}});va.start();}}/** <span style="white-space:pre"></span> * 选中下一个<span style="white-space:pre"></span> */<span style="white-space:pre"></span>public void selectNext() {<span style="white-space:pre"></span>//当上衣个动画没有结束,就不会进行mCurrent改变。主动调用cancel,结束动画,及时改变mCurrent的值<span style="white-space:pre"></span>if (va != null) {<span style="white-space:pre"></span>if (va.isRunning()) {<span style="white-space:pre"></span>va.cancel();<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>if (vaRe != null) {<span style="white-space:pre"></span>if (vaRe.isRunning()) {<span style="white-space:pre"></span>vaRe.cancel();<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>int toIndex = mCurrentIndex + 1;<span style="white-space:pre"></span>if (isLooper()) {<span style="white-space:pre"></span>if (toIndex >= getCount()) {<span style="white-space:pre"></span>toIndex = 0;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}else {<span style="white-space:pre"></span>if (toIndex >= getCount()) {<span style="white-space:pre"></span>toIndex = mCurrentIndex;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>selectIndex(toIndex);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>/**<span style="white-space:pre"></span> * 选中上一个<span style="white-space:pre"></span> */<span style="white-space:pre"></span>public void selectPre() {<span style="white-space:pre"></span>if (va != null) {<span style="white-space:pre"></span>if (va.isRunning()) {<span style="white-space:pre"></span>va.cancel();<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>if (vaRe != null) {<span style="white-space:pre"></span>if (vaRe.isRunning()) {<span style="white-space:pre"></span>vaRe.cancel();<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>int toIndex = mCurrentIndex - 1;<span style="white-space:pre"></span>if (isLooper()) {<span style="white-space:pre"></span>if (toIndex < 0) {<span style="white-space:pre"></span>toIndex = getCount() - 1;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}else {<span style="white-space:pre"></span>if (toIndex < 0) {<span style="white-space:pre"></span>toIndex = mCurrentIndex;<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>selectIndex(toIndex);<span style="white-space:pre"></span>}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawUnselectedPoints(canvas);drawSelectedPoint(canvas);}/** * 画圆点 * @param canvas */private void drawSelectedPoint(Canvas canvas) {//获取到当前选中的小圆圈 的位置数据mCurrentPointCenter = mUnselectedPoints.get(mCurrentIndex);//获取到 将要选中的小圆圈 的位置数据mToPointCenter = mUnselectedPoints.get(mToIndex);//获取小圆点将要移动的总距离float disX = mToPointCenter[0] - mCurrentPointCenter[0];//根据动画执行进度,获取当前移动了的距离float offsetX = disX * mPercent;//改变圆点正下方的定点X数据mData[0] = mCurrentPointCenter[0] + offsetX;//改变圆点正下方的定点Y数据mData[1] = mCurrentPointCenter[1] + getPointSize() / 2;if (mToIndex > mCurrentIndex) {//如果小圆点向右移动,右顶点只是改变移动数据mData[2] = mCurrentPointCenter[0] + offsetX + getPointSize() / 2;} else if (mToIndex < mCurrentIndex) {//如果小圆点向左移动,右顶点还需要算上拉伸的距离mData[2] = mCurrentPointCenter[0] + offsetX + getPointSize() / 2+ getPointSize() / 2 * mPercent;} else {//当小圆点移动到位时,开始复原if (isIndexInCreate == -1) {//如果左移了,则右顶点开始复原mData[2] = mCurrentPointCenter[0] + offsetX + getPointSize()-(float) getPointSize() / 2 * mRePercent;} else if (isIndexInCreate == 0) {//如果没变,则不用复原mData[2] = mCurrentPointCenter[0] + offsetX + getPointSize()- getPointSize() / 2;}}mData[3] = mCurrentPointCenter[1];mData[4] = mCurrentPointCenter[0] + offsetX;mData[5] = mCurrentPointCenter[1] - getPointSize() / 2;if (mToIndex > mCurrentIndex) {mData[6] = mCurrentPointCenter[0] + offsetX - getPointSize() / 2- getPointSize() / 2 * mPercent;} else if (mToIndex < mCurrentIndex) {mData[6] = mCurrentPointCenter[0] + offsetX - getPointSize() / 2;} else {if (isIndexInCreate == 1) {//如果右移了,则左顶点开始复原mData[6] = mCurrentPointCenter[0] + offsetX - getPointSize()+ getPointSize() / 2 * mRePercent;} else if (isIndexInCreate == 0) {mData[6] = mCurrentPointCenter[0] + offsetX - getPointSize()+ getPointSize() / 2;}}mData[7] = mCurrentPointCenter[1];//下面的控制点,则只需控制移动数据改变就行.mCtrl[0] = mData[0] + mDifference;mCtrl[1] = mData[1];mCtrl[2] = mData[2];mCtrl[3] = mData[3] + mDifference;mCtrl[4] = mData[2];mCtrl[5] = mData[3] - mDifference;mCtrl[6] = mData[4] + mDifference;mCtrl[7] = mData[5];mCtrl[8] = mData[4] - mDifference;mCtrl[9] = mData[5];mCtrl[10] = mData[6];mCtrl[11] = mData[7] - mDifference;mCtrl[12] = mData[6];mCtrl[13] = mData[7] + mDifference;mCtrl[14] = mData[0] - mDifference;mCtrl[15] = mData[1];//画圆点mPath.reset();mPath.moveTo(mData[0], mData[1]);mPath.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2],mData[3]);mPath.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4],mData[5]);mPath.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6],mData[7]);mPath.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0],mData[1]);canvas.drawPath(mPath, mSelectedPaint);}/** *  画圆圈 * @param canvas */private void drawUnselectedPoints(Canvas canvas) {int centerY = mViewHeight / 2;for (int i = 0; i < getCount(); i++) {// 设置要选中的圈 发光if (i == mToIndex) {mUnselectedPaint.setMaskFilter(new BlurMaskFilter(getPaintStrokeWidth() * getBlurRadio(), Blur.SOLID));} else {mUnselectedPaint.setMaskFilter(null);}//计算各个圆圈中心点的位置int centerX = mViewLeftPadding + getPointSize() / 2+ getPaintStrokeWidth() / 2 + mDis * i;if (i == mCurrentIndex) {//如果这是选中圈,则还要执行圆圈的抖动动画.canvas.drawCircle(centerX, centerY, getPointSize() / 2* mSizePercent, mUnselectedPaint);} else {canvas.drawCircle(centerX, centerY, getPointSize() / 2,mUnselectedPaint);}//把圆圈数据保存下来int[] ints = new int[] { centerX, centerY };mUnselectedPoints.add(ints);}}public int getCount() {return mCount;}public void setCount(int mCount) {this.mCount = mCount;}public int getUnSelectedColor() {return mUnSelectedColor;}public void setUnSelectedColor(int mUnSelectedColor) {this.mUnSelectedColor = mUnSelectedColor;}public int getPointSize() {return mPointSize;}public void setPointSize(int mPointSize) {this.mPointSize = mPointSize;}public int getPaintStrokeWidth() {return mPaintStrokeWidth;}public void setPaintStrokeWidth(int mPaintStrokeWidth) {this.mPaintStrokeWidth = mPaintStrokeWidth;}public long getDuration() {return mDuration;}public void setDuration(long mDuration) {this.mDuration = mDuration;}public long getDurationRe() {return mDurationRe;}public void setDurationRe(long mDurationRe) {this.mDurationRe = mDurationRe;}public long geDurationSize() {return mDurationSize;}public void setDurationSize(long mDurationSize) {this.mDurationSize = mDurationSize;}public boolean isLooper() {return isLooper;}public void setLooper(boolean isLooper) {this.isLooper = isLooper;}public int getBlurRadio() {return mBlurRadio;}public void setBlurRadio(int mBlurRadio) {this.mBlurRadio = mBlurRadio;}}
下面是自定义的属性:

<declare-styleable name="linearpointsview">        <attr name="count" format="integer" />        <attr name="select_color" format="color" />        <attr name="unselect_color" format="color" />        <attr name="pointsize" format="dimension" />        <attr name="pointstrokewidth" format="dimension" />        <attr name="anim_move_duration" format="integer" />        <attr name="anim_reset_duration" format="integer" />        <attr name="anim_shake_duration" format="integer" />        <attr name="islooper" format="boolean" />        <attr name="blur_radio" format="integer" /></declare-styleable>
布局中的使用:
 <com.dup.bezierdemo.BezierLinearPointsView        android:id="@+id/ttn"        android:layout_width="260dp"        android:layout_height="80dp"        android:layout_alignParentTop="true"        android:layout_centerHorizontal="true"        linearpointsview:anim_move_duration="400"        linearpointsview:anim_reset_duration="200"        linearpointsview:anim_shake_duration="500"        linearpointsview:blur_radio="2"        linearpointsview:count="4"        linearpointsview:islooper="false"        linearpointsview:pointsize="20dp"        linearpointsview:pointstrokewidth="3dp"        linearpointsview:select_color="#aaffff"        linearpointsview:unselect_color="#ffaaaa"  />

总结:贝塞尔曲线复杂的地方就是 数据点的计算控制,想要实现一些更酷炫的效果,那么数据的控制会更麻烦。其中肯定会用到动画,要考虑到动画提前结束的情况。想实现一些比较酷的动画,贝塞尔是很有用的东西,当然也不是想用就用,还是要和整个app的风格相统一咯。希望对大家有点作用吧,有不足问题还希望大家指正。


0 0
原创粉丝点击