88 | * N even: 89 | * [d][d + 1]...[d + 1][d] 90 | * 91 | * N odd: 92 | * [d][d + 1][d + 2][d + 1][d] 93 | *
94 | */ 95 | 96 | /** Walk forward with increasing duration */ 97 | for (int i = 0; i < half; ++i) { 98 | animators[i] = createAnimator(dotViews[i], d); 99 | d += DURATION_DIFF; 100 | } 101 | 102 | /** If dot count is odd, the middle dot has the longest duration */ 103 | if (dotCount % 2 == 1) { 104 | animators[half] = createAnimator(dotViews[half], d); 105 | half++; 106 | } 107 | 108 | /** Walk backward with decreasing duration */ 109 | for (int i = half; i < dotCount; ++i) { 110 | d -= DURATION_DIFF; 111 | animators[i] = createAnimator(dotViews[i], d); 112 | } 113 | 114 | final AnimatorSet animatorSet = new AnimatorSet(); 115 | animatorSet.playSequentially(animators); 116 | animatorSet.setInterpolator(new AccelerateInterpolator(2.0f)); 117 | animatorSet.addListener(new Animator.AnimatorListener() { 118 | @Override 119 | public void onAnimationStart(Animator animation) { 120 | 121 | } 122 | 123 | @Override 124 | public void onAnimationEnd(Animator animation) { 125 | if (!stop) { 126 | animatorSet.start(); 127 | } 128 | } 129 | 130 | @Override 131 | public void onAnimationCancel(Animator animation) { 132 | 133 | } 134 | 135 | @Override 136 | public void onAnimationRepeat(Animator animation) { 137 | 138 | } 139 | }); 140 | return animatorSet; 141 | } 142 | 143 | private void addCircleViews() { 144 | dotViews = new CircleView[dotCount]; 145 | final Context context = getContext(); 146 | for (int i = 0; i < dotCount; ++i) { 147 | Log.e(TAG, "add view: " + i); 148 | dotViews[i] = new CircleView(context); 149 | dotViews[i].setRadius(dotRadius); 150 | dotViews[i].setColor(neutralColor); 151 | addView( 152 | dotViews[i], 153 | 0, 154 | new LinearLayout.LayoutParams( 155 | ViewGroup.LayoutParams.WRAP_CONTENT, 156 | ViewGroup.LayoutParams.WRAP_CONTENT 157 | ) 158 | ); 159 | } 160 | animatorSet = prepareAnimators(); 161 | } 162 | 163 | public void startAnimation() { 164 | stop = false; 165 | animatorSet.start(); 166 | } 167 | 168 | public ObjectAnimator createAnimator(final CircleView v, long duration) { 169 | final ObjectAnimator animator = ObjectAnimator.ofObject( 170 | v, 171 | "color", 172 | new ArgbEvaluator(), 173 | neutralColor, 174 | blinkingColor 175 | ); 176 | animator.setDuration(duration); 177 | animator.setRepeatCount(1); 178 | animator.setInterpolator(DOT_INTERPOLATOR); 179 | animator.addListener(new Animator.AnimatorListener() { 180 | @Override 181 | public void onAnimationStart(Animator animation) { 182 | 183 | } 184 | 185 | @Override 186 | public void onAnimationEnd(Animator animation) { 187 | v.setColor(neutralColor); 188 | } 189 | 190 | @Override 191 | public void onAnimationCancel(Animator animation) { 192 | 193 | } 194 | 195 | @Override 196 | public void onAnimationRepeat(Animator animation) { 197 | 198 | } 199 | }); 200 | return animator; 201 | } 202 | 203 | public void stopAnimation() { 204 | stop = true; 205 | animatorSet.end(); 206 | } 207 | 208 | public int getDotCount() { 209 | return dotCount; 210 | } 211 | 212 | public int getDotRadius() { 213 | return dotRadius; 214 | } 215 | 216 | public int getNeutralColor() { 217 | return neutralColor; 218 | } 219 | 220 | public boolean isStop() { 221 | return stop; 222 | } 223 | 224 | public int getBlinkingColor() { 225 | return blinkingColor; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /library/src/main/java/com/github/channguyen/adv/CircleView.java: -------------------------------------------------------------------------------- 1 | package com.github.channguyen.adv; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | 11 | public class CircleView extends View { 12 | 13 | private static final String TAG = CircleView.class.getSimpleName(); 14 | 15 | private static final float DEFAULT_STROKE_WIDTH = 3.0f; 16 | 17 | private static final int DEFAULT_RADIUS = 20; 18 | 19 | private static final int DEFAULT_COLOR = Color.parseColor("#FF0000"); 20 | 21 | protected float radius; 22 | 23 | protected float strokeWidth = DEFAULT_STROKE_WIDTH; 24 | 25 | protected Paint paint; 26 | 27 | private int color = DEFAULT_COLOR; 28 | 29 | protected boolean filled = true; 30 | 31 | public CircleView(Context context) { 32 | this(context, null); 33 | } 34 | 35 | public CircleView(Context context, AttributeSet attrs) { 36 | this(context, attrs, -1); 37 | } 38 | 39 | public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | if (attrs != null) { 42 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); 43 | try { 44 | radius = a.getDimensionPixelSize( 45 | R.styleable.CircleView_cv___radius, DEFAULT_RADIUS); 46 | color = a.getInt( 47 | R.styleable.CircleView_cv___color, DEFAULT_COLOR); 48 | filled = a.getBoolean( 49 | R.styleable.CircleView_cv___filled, true); 50 | } finally { 51 | a.recycle(); 52 | } 53 | } 54 | 55 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 56 | paint.setStrokeWidth(strokeWidth); 57 | paint.setColor(color); 58 | if (filled) { 59 | paint.setStyle(Paint.Style.FILL); 60 | } else { 61 | paint.setStyle(Paint.Style.STROKE); 62 | } 63 | } 64 | 65 | public void setRadius(int radius) { 66 | this.radius = radius; 67 | invalidate(); 68 | } 69 | 70 | public void setColor(int color) { 71 | this.color = color; 72 | this.paint.setColor(color); 73 | invalidate(); 74 | } 75 | 76 | public void setFilled(boolean filled) { 77 | this.filled = filled; 78 | paint.setStyle(this.filled ? Paint.Style.FILL : Paint.Style.STROKE); 79 | } 80 | 81 | public boolean isFilled() { 82 | return filled; 83 | } 84 | 85 | public float getRadius() { 86 | return radius; 87 | } 88 | 89 | public float getStrokeWidth() { 90 | return strokeWidth; 91 | } 92 | 93 | public int getColor() { 94 | return color; 95 | } 96 | 97 | @Override 98 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 99 | setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); 100 | } 101 | 102 | /** 103 | * Measures height according to the passed measure spec 104 | * 105 | * @param measureSpec int measure spec to use 106 | * @return int pixel size 107 | */ 108 | protected int measureHeight(int measureSpec) { 109 | int specMode = MeasureSpec.getMode(measureSpec); 110 | int specSize = MeasureSpec.getSize(measureSpec); 111 | int result; 112 | if (specMode == MeasureSpec.EXACTLY) { 113 | result = specSize; 114 | } else { 115 | result = (int) (2 * radius) + getPaddingTop() + getPaddingBottom() + (int) (2 * strokeWidth); 116 | if (specMode == MeasureSpec.AT_MOST) { 117 | result = Math.min(result, specSize); 118 | } 119 | } 120 | return result; 121 | } 122 | 123 | /** 124 | * Measures width according to the passed measure spec 125 | * 126 | * @param measureSpec int measure spec to use 127 | * @return int pixel size 128 | */ 129 | protected int measureWidth(int measureSpec) { 130 | int specMode = MeasureSpec.getMode(measureSpec); 131 | int specSize = MeasureSpec.getSize(measureSpec); 132 | int result; 133 | if (specMode == MeasureSpec.EXACTLY) { 134 | result = specSize; 135 | } else { 136 | result = (int) (2 * radius) + getPaddingLeft() + getPaddingRight() + (int) (2 * strokeWidth); 137 | if (specMode == MeasureSpec.AT_MOST) { 138 | result = Math.min(result, specSize); 139 | } 140 | } 141 | return result; 142 | } 143 | 144 | @Override 145 | public void onDraw(Canvas canvas) { 146 | final int x = getWidth() >> 1; 147 | final int y = getHeight() >> 1; 148 | canvas.drawCircle(x, y, radius - strokeWidth, paint); 149 | super.onDraw(canvas); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /library/src/main/res/layout/v_animated_dots.xml: -------------------------------------------------------------------------------- 1 | 2 |