target, DataSource dataSource, boolean isFirstResource) {
530 | if (requestCode == RQ_GET_PHOTO_COMPLETE)
531 | loadingBtn.setSuccessDrawable(resource);
532 | else
533 | loadingBtn.setFailDrawable(resource);
534 | return false;
535 | }
536 | })
537 | .load(mSelected.get(0))
538 | .into(targetImageView);
539 |
540 | }
541 | }
542 |
543 | }
544 |
545 | @SuppressWarnings({"VariableArgumentMethod", "BooleanMethodIsAlwaysInverted"})
546 | public static boolean requestPermissions(Activity context, String... permissions) {
547 | boolean flag = false;
548 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
549 | for (String permission : permissions) {
550 | if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
551 | context.requestPermissions(permissions, RQ_MULTIPLE_PERMISSIONS);
552 | flag = true;
553 | }
554 | }
555 | }
556 | return flag;
557 | }
558 |
559 | @Override
560 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
561 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
562 | if (requestCode == RQ_MULTIPLE_PERMISSIONS) {
563 | if (grantResults.length > 0) {
564 | Toast.makeText(this, "Please allow Permissions", Toast.LENGTH_LONG).show();
565 | }
566 | }
567 | }
568 |
569 |
570 | static class EmptyOnSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
571 | @Override
572 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
573 |
574 | }
575 |
576 | @Override
577 | public void onStartTrackingTouch(SeekBar seekBar) {
578 |
579 | }
580 |
581 | @Override
582 | public void onStopTrackingTouch(SeekBar seekBar) {
583 |
584 | }
585 | }
586 | }
587 |
--------------------------------------------------------------------------------
/Loadingbutton/src/main/java/com/flod/loadingbutton/CircularProgressDrawable.java:
--------------------------------------------------------------------------------
1 | package com.flod.loadingbutton;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ValueAnimator;
5 | import android.content.Context;
6 | import android.content.res.Resources;
7 | import android.graphics.Canvas;
8 | import android.graphics.Color;
9 | import android.graphics.ColorFilter;
10 | import android.graphics.Paint;
11 | import android.graphics.Path;
12 | import android.graphics.PixelFormat;
13 | import android.graphics.Rect;
14 | import android.graphics.RectF;
15 | import android.graphics.drawable.Animatable;
16 | import android.graphics.drawable.Drawable;
17 | import android.util.DisplayMetrics;
18 | import android.view.animation.Interpolator;
19 | import android.view.animation.LinearInterpolator;
20 |
21 | import androidx.annotation.IntDef;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RestrictTo;
24 | import androidx.core.util.Preconditions;
25 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
26 |
27 | import java.lang.annotation.Retention;
28 | import java.lang.annotation.RetentionPolicy;
29 |
30 | import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
31 |
32 | /*
33 | * Copyright 2018 The Android Open Source Project
34 | *
35 | * Licensed under the Apache License, Version 2.0 (the "License");
36 | * you may not use this file except in compliance with the License.
37 | * You may obtain a copy of the License at
38 | *
39 | * http://www.apache.org/licenses/LICENSE-2.0
40 | *
41 | * Unless required by applicable law or agreed to in writing, software
42 | * distributed under the License is distributed on an "AS IS" BASIS,
43 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
44 | * See the License for the specific language governing permissions and
45 | * limitations under the License.
46 | */
47 |
48 |
49 | public class CircularProgressDrawable extends Drawable implements Animatable {
50 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
51 | private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
52 |
53 | /** @hide */
54 | @RestrictTo(LIBRARY_GROUP)
55 | @Retention(RetentionPolicy.SOURCE)
56 | @IntDef({LARGE, DEFAULT})
57 | public @interface ProgressDrawableSize {
58 | }
59 |
60 | /** Maps to ProgressBar.Large style. */
61 | public static final int LARGE = 0;
62 |
63 | private static final float CENTER_RADIUS_LARGE = 11f;
64 | private static final float STROKE_WIDTH_LARGE = 3f;
65 | private static final int ARROW_WIDTH_LARGE = 12;
66 | private static final int ARROW_HEIGHT_LARGE = 6;
67 |
68 | /** Maps to ProgressBar default style. */
69 | public static final int DEFAULT = 1;
70 |
71 | private static final float CENTER_RADIUS = 7.5f;
72 | private static final float STROKE_WIDTH = 2.5f;
73 | private static final int ARROW_WIDTH = 10;
74 | private static final int ARROW_HEIGHT = 5;
75 |
76 | /**
77 | * This is the default set of colors that's used in spinner. {@link
78 | * #setColorSchemeColors(int...)} allows modifying colors.
79 | */
80 | private static final int[] COLORS = new int[]{
81 | Color.BLACK
82 | };
83 |
84 | /**
85 | * The value in the linear interpolator for animating the drawable at which
86 | * the color transition should start
87 | */
88 | private static final float COLOR_CHANGE_OFFSET = 0.75f;
89 | private static final float SHRINK_OFFSET = 0.5f;
90 |
91 | /** The duration of a single progress spin in milliseconds. */
92 | private static final int ANIMATION_DURATION = 1332;
93 |
94 | /** Full rotation that's done for the animation duration in degrees. */
95 | private static final float GROUP_FULL_ROTATION = 1080f / 5f;
96 |
97 | /** The indicator ring, used to manage animation state. */
98 | private final Ring mRing;
99 |
100 | /** Canvas rotation in degrees. */
101 | private float mRotation;
102 |
103 | /** Maximum length of the progress arc during the animation. */
104 | private static final float MAX_PROGRESS_ARC = .8f;
105 | /** Minimum length of the progress arc during the animation. */
106 | private static final float MIN_PROGRESS_ARC = .01f;
107 |
108 | /** Rotation applied to ring during the animation, to complete it to a full circle. */
109 | private static final float RING_ROTATION = 1f - (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
110 |
111 | private Resources mResources;
112 | private Animator mAnimator;
113 | @SuppressWarnings("WeakerAccess") /* synthetic access */
114 | float mRotationCount;
115 | @SuppressWarnings("WeakerAccess") /* synthetic access */
116 | boolean mFinishing;
117 |
118 | /**
119 | * @param context application context
120 | */
121 | public CircularProgressDrawable(@NonNull Context context) {
122 | mResources = Preconditions.checkNotNull(context).getResources();
123 |
124 | mRing = new Ring();
125 | mRing.setColors(COLORS);
126 |
127 | setStrokeWidth(STROKE_WIDTH);
128 | setupAnimators();
129 | }
130 |
131 | /** Sets all parameters at once in dp. */
132 | private void setSizeParameters(float centerRadius, float strokeWidth, float arrowWidth,
133 | float arrowHeight) {
134 | final Ring ring = mRing;
135 | final DisplayMetrics metrics = mResources.getDisplayMetrics();
136 | final float screenDensity = metrics.density;
137 |
138 | ring.setStrokeWidth(strokeWidth * screenDensity);
139 | ring.setCenterRadius(centerRadius * screenDensity);
140 | ring.setColorIndex(0);
141 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
142 | }
143 |
144 | /**
145 | * Sets the overall size for the progress spinner. This updates the radius
146 | * and stroke width of the ring, and arrow dimensions.
147 | *
148 | * @param size one of {@link #LARGE} or {@link #DEFAULT}
149 | */
150 | public void setStyle(@ProgressDrawableSize int size) {
151 | if (size == LARGE) {
152 | setSizeParameters(CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE,
153 | ARROW_HEIGHT_LARGE);
154 | } else {
155 | setSizeParameters(CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT);
156 | }
157 | invalidateSelf();
158 | }
159 |
160 | /**
161 | * Returns the stroke width for the progress spinner in pixels.
162 | *
163 | * @return stroke width in pixels
164 | */
165 | public float getStrokeWidth() {
166 | return mRing.getStrokeWidth();
167 | }
168 |
169 | /**
170 | * Sets the stroke width for the progress spinner in pixels.
171 | *
172 | * @param strokeWidth stroke width in pixels
173 | */
174 | public void setStrokeWidth(float strokeWidth) {
175 | mRing.setStrokeWidth(strokeWidth);
176 | invalidateSelf();
177 | }
178 |
179 | /**
180 | * Returns the center radius for the progress spinner in pixels.
181 | *
182 | * @return center radius in pixels
183 | */
184 | public float getCenterRadius() {
185 | return mRing.getCenterRadius();
186 | }
187 |
188 | /**
189 | * Sets the center radius for the progress spinner in pixels. If set to 0, this drawable will
190 | * fill the bounds when drawn.
191 | *
192 | * @param centerRadius center radius in pixels
193 | */
194 | public void setCenterRadius(float centerRadius) {
195 | mRing.setCenterRadius(centerRadius);
196 | invalidateSelf();
197 | }
198 |
199 | /**
200 | * Sets the stroke cap of the progress spinner. Default stroke cap is {@link Paint.Cap#SQUARE}.
201 | *
202 | * @param strokeCap stroke cap
203 | */
204 | public void setStrokeCap(@NonNull Paint.Cap strokeCap) {
205 | mRing.setStrokeCap(strokeCap);
206 | invalidateSelf();
207 | }
208 |
209 | /**
210 | * Returns the stroke cap of the progress spinner.
211 | *
212 | * @return stroke cap
213 | */
214 | @NonNull
215 | public Paint.Cap getStrokeCap() {
216 | return mRing.getStrokeCap();
217 | }
218 |
219 | /**
220 | * Returns the arrow width in pixels.
221 | *
222 | * @return arrow width in pixels
223 | */
224 | public float getArrowWidth() {
225 | return mRing.getArrowWidth();
226 | }
227 |
228 | /**
229 | * Returns the arrow height in pixels.
230 | *
231 | * @return arrow height in pixels
232 | */
233 | public float getArrowHeight() {
234 | return mRing.getArrowHeight();
235 | }
236 |
237 | /**
238 | * Sets the dimensions of the arrow at the end of the spinner in pixels.
239 | *
240 | * @param width width of the baseline of the arrow in pixels
241 | * @param height distance from tip of the arrow to its baseline in pixels
242 | */
243 | public void setArrowDimensions(float width, float height) {
244 | mRing.setArrowDimensions(width, height);
245 | invalidateSelf();
246 | }
247 |
248 | /**
249 | * Returns {@code true} if the arrow at the end of the spinner is shown.
250 | *
251 | * @return {@code true} if the arrow is shown, {@code false} otherwise.
252 | */
253 | public boolean getArrowEnabled() {
254 | return mRing.getShowArrow();
255 | }
256 |
257 | /**
258 | * Sets if the arrow at the end of the spinner should be shown.
259 | *
260 | * @param show {@code true} if the arrow should be drawn, {@code false} otherwise
261 | */
262 | public void setArrowEnabled(boolean show) {
263 | mRing.setShowArrow(show);
264 | invalidateSelf();
265 | }
266 |
267 | /**
268 | * Returns the scale of the arrow at the end of the spinner.
269 | *
270 | * @return scale of the arrow
271 | */
272 | public float getArrowScale() {
273 | return mRing.getArrowScale();
274 | }
275 |
276 | /**
277 | * Sets the scale of the arrow at the end of the spinner.
278 | *
279 | * @param scale scaling that will be applied to the arrow's both width and height when drawing.
280 | */
281 | public void setArrowScale(float scale) {
282 | mRing.setArrowScale(scale);
283 | invalidateSelf();
284 | }
285 |
286 | /**
287 | * Returns the start trim for the progress spinner arc
288 | *
289 | * @return start trim from [0..1]
290 | */
291 | public float getStartTrim() {
292 | return mRing.getStartTrim();
293 | }
294 |
295 | /**
296 | * Returns the end trim for the progress spinner arc
297 | *
298 | * @return end trim from [0..1]
299 | */
300 | public float getEndTrim() {
301 | return mRing.getEndTrim();
302 | }
303 |
304 | /**
305 | * Sets the start and end trim for the progress spinner arc. 0 corresponds to the geometric
306 | * angle of 0 degrees (3 o'clock on a watch) and it increases clockwise, coming to a full circle
307 | * at 1.
308 | *
309 | * @param start starting position of the arc from [0..1]
310 | * @param end ending position of the arc from [0..1]
311 | */
312 | public void setStartEndTrim(float start, float end) {
313 | mRing.setStartTrim(start);
314 | mRing.setEndTrim(end);
315 | invalidateSelf();
316 | }
317 |
318 | /**
319 | * Returns the amount of rotation applied to the progress spinner.
320 | *
321 | * @return amount of rotation from [0..1]
322 | */
323 | public float getProgressRotation() {
324 | return mRing.getRotation();
325 | }
326 |
327 | /**
328 | * Sets the amount of rotation to apply to the progress spinner.
329 | *
330 | * @param rotation rotation from [0..1]
331 | */
332 | public void setProgressRotation(float rotation) {
333 | mRing.setRotation(rotation);
334 | invalidateSelf();
335 | }
336 |
337 | /**
338 | * Returns the background color of the circle drawn inside the drawable.
339 | *
340 | * @return an ARGB color
341 | */
342 | public int getBackgroundColor() {
343 | return mRing.getBackgroundColor();
344 | }
345 |
346 | /**
347 | * Sets the background color of the circle inside the drawable. Calling {@link
348 | * #setAlpha(int)} does not affect the visibility background color, so it should be set
349 | * separately if it needs to be hidden or visible.
350 | *
351 | * @param color an ARGB color
352 | */
353 | public void setBackgroundColor(int color) {
354 | mRing.setBackgroundColor(color);
355 | invalidateSelf();
356 | }
357 |
358 | /**
359 | * Returns the colors used in the progress animation
360 | *
361 | * @return list of ARGB colors
362 | */
363 | @NonNull
364 | public int[] getColorSchemeColors() {
365 | return mRing.getColors();
366 | }
367 |
368 | /**
369 | * Sets the colors used in the progress animation from a color list. The first color will also
370 | * be the color to be used if animation is not started yet.
371 | *
372 | * @param colors list of ARGB colors to be used in the spinner
373 | */
374 | public void setColorSchemeColors(@NonNull int... colors) {
375 | mRing.setColors(colors);
376 | mRing.setColorIndex(0);
377 | invalidateSelf();
378 | }
379 |
380 | @Override
381 | public void draw(Canvas canvas) {
382 | final Rect bounds = getBounds();
383 | canvas.save();
384 | canvas.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
385 | mRing.draw(canvas, bounds);
386 | canvas.restore();
387 | }
388 |
389 | @Override
390 | public void setAlpha(int alpha) {
391 | mRing.setAlpha(alpha);
392 | invalidateSelf();
393 | }
394 |
395 | @Override
396 | public int getAlpha() {
397 | return mRing.getAlpha();
398 | }
399 |
400 | @Override
401 | public void setColorFilter(ColorFilter colorFilter) {
402 | mRing.setColorFilter(colorFilter);
403 | invalidateSelf();
404 | }
405 |
406 | private void setRotation(float rotation) {
407 | mRotation = rotation;
408 | }
409 |
410 | private float getRotation() {
411 | return mRotation;
412 | }
413 |
414 | @Override
415 | public int getOpacity() {
416 | return PixelFormat.TRANSLUCENT;
417 | }
418 |
419 | @Override
420 | public boolean isRunning() {
421 | return mAnimator.isRunning();
422 | }
423 |
424 | /**
425 | * Starts the animation for the spinner.
426 | */
427 | @Override
428 | public void start() {
429 | mAnimator.cancel();
430 | mRing.storeOriginals();
431 | // Already showing some part of the ring
432 | if (mRing.getEndTrim() != mRing.getStartTrim()) {
433 | mFinishing = true;
434 | mAnimator.setDuration(ANIMATION_DURATION / 2);
435 | mAnimator.start();
436 | } else {
437 | mRing.setColorIndex(0);
438 | mRing.resetOriginals();
439 | mAnimator.setDuration(ANIMATION_DURATION);
440 | mAnimator.start();
441 | }
442 | }
443 |
444 | /**
445 | * Stops the animation for the spinner.
446 | */
447 | @Override
448 | public void stop() {
449 | mAnimator.cancel();
450 | setRotation(0);
451 | mRing.setShowArrow(false);
452 | mRing.setColorIndex(0);
453 | mRing.resetOriginals();
454 | invalidateSelf();
455 | }
456 |
457 | // Adapted from ArgbEvaluator.java
458 | private int evaluateColorChange(float fraction, int startValue, int endValue) {
459 | int startA = (startValue >> 24) & 0xff;
460 | int startR = (startValue >> 16) & 0xff;
461 | int startG = (startValue >> 8) & 0xff;
462 | int startB = startValue & 0xff;
463 |
464 | int endA = (endValue >> 24) & 0xff;
465 | int endR = (endValue >> 16) & 0xff;
466 | int endG = (endValue >> 8) & 0xff;
467 | int endB = endValue & 0xff;
468 |
469 | return (startA + (int) (fraction * (endA - startA))) << 24
470 | | (startR + (int) (fraction * (endR - startR))) << 16
471 | | (startG + (int) (fraction * (endG - startG))) << 8
472 | | (startB + (int) (fraction * (endB - startB)));
473 | }
474 |
475 | /**
476 | * Update the ring color if this is within the last 25% of the animation.
477 | * The new ring color will be a translation from the starting ring color to
478 | * the next color.
479 | */
480 | @SuppressWarnings("WeakerAccess") /* synthetic access */
481 | void updateRingColor(float interpolatedTime, Ring ring) {
482 | if (interpolatedTime > COLOR_CHANGE_OFFSET) {
483 | ring.setColor(evaluateColorChange((interpolatedTime - COLOR_CHANGE_OFFSET)
484 | / (1f - COLOR_CHANGE_OFFSET), ring.getStartingColor(),
485 | ring.getNextColor()));
486 | } else {
487 | ring.setColor(ring.getStartingColor());
488 | }
489 | }
490 |
491 | /**
492 | * Update the ring start and end trim if the animation is finishing (i.e. it started with
493 | * already visible progress, so needs to shrink back down before starting the spinner).
494 | */
495 | private void applyFinishTranslation(float interpolatedTime, Ring ring) {
496 | // shrink back down and complete a full rotation before
497 | // starting other circles
498 | // Rotation goes between [0..1].
499 | updateRingColor(interpolatedTime, ring);
500 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC)
501 | + 1f);
502 | final float startTrim = ring.getStartingStartTrim()
503 | + (ring.getStartingEndTrim() - MIN_PROGRESS_ARC - ring.getStartingStartTrim())
504 | * interpolatedTime;
505 | ring.setStartTrim(startTrim);
506 | ring.setEndTrim(ring.getStartingEndTrim());
507 | final float rotation = ring.getStartingRotation()
508 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
509 | ring.setRotation(rotation);
510 | }
511 |
512 | /**
513 | * Update the ring start and end trim according to current time of the animation.
514 | */
515 | @SuppressWarnings("WeakerAccess") /* synthetic access */
516 | void applyTransformation(float interpolatedTime, Ring ring, boolean lastFrame) {
517 | if (mFinishing) {
518 | applyFinishTranslation(interpolatedTime, ring);
519 | // Below condition is to work around a ValueAnimator issue where onAnimationRepeat is
520 | // called before last frame (1f).
521 | } else if (interpolatedTime != 1f || lastFrame) {
522 | final float startingRotation = ring.getStartingRotation();
523 | float startTrim, endTrim;
524 |
525 | if (interpolatedTime < SHRINK_OFFSET) { // Expansion occurs on first half of animation
526 | final float scaledTime = interpolatedTime / SHRINK_OFFSET;
527 | startTrim = ring.getStartingStartTrim();
528 | endTrim = startTrim + ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
529 | * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime) + MIN_PROGRESS_ARC);
530 | } else { // Shrinking occurs on second half of animation
531 | float scaledTime = (interpolatedTime - SHRINK_OFFSET) / (1f - SHRINK_OFFSET);
532 | endTrim = ring.getStartingStartTrim() + (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
533 | startTrim = endTrim - ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
534 | * (1f - MATERIAL_INTERPOLATOR.getInterpolation(scaledTime))
535 | + MIN_PROGRESS_ARC);
536 | }
537 |
538 | final float rotation = startingRotation + (RING_ROTATION * interpolatedTime);
539 | float groupRotation = GROUP_FULL_ROTATION * (interpolatedTime + mRotationCount);
540 |
541 | ring.setStartTrim(startTrim);
542 | ring.setEndTrim(endTrim);
543 | ring.setRotation(rotation);
544 | setRotation(groupRotation);
545 | }
546 | }
547 |
548 | private void setupAnimators() {
549 | final Ring ring = mRing;
550 | final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
551 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
552 | @Override
553 | public void onAnimationUpdate(ValueAnimator animation) {
554 | float interpolatedTime = (float) animation.getAnimatedValue();
555 | updateRingColor(interpolatedTime, ring);
556 | applyTransformation(interpolatedTime, ring, false);
557 | invalidateSelf();
558 | }
559 | });
560 | animator.setRepeatCount(ValueAnimator.INFINITE);
561 | animator.setRepeatMode(ValueAnimator.RESTART);
562 | animator.setInterpolator(LINEAR_INTERPOLATOR);
563 | animator.addListener(new Animator.AnimatorListener() {
564 |
565 | @Override
566 | public void onAnimationStart(Animator animator) {
567 | mRotationCount = 0;
568 | }
569 |
570 | @Override
571 | public void onAnimationEnd(Animator animator) {
572 | // do nothing
573 | }
574 |
575 | @Override
576 | public void onAnimationCancel(Animator animation) {
577 | // do nothing
578 | }
579 |
580 | @Override
581 | public void onAnimationRepeat(Animator animator) {
582 | applyTransformation(1f, ring, true);
583 | ring.storeOriginals();
584 | ring.goToNextColor();
585 | if (mFinishing) {
586 | // finished closing the last ring from the swipe gesture; go
587 | // into progress mode
588 | mFinishing = false;
589 | animator.cancel();
590 | animator.setDuration(ANIMATION_DURATION);
591 | animator.start();
592 | ring.setShowArrow(false);
593 | } else {
594 | mRotationCount = mRotationCount + 1;
595 | }
596 | }
597 | });
598 | mAnimator = animator;
599 | }
600 |
601 | /**
602 | * A private class to do all the drawing of CircularProgressDrawable, which includes background,
603 | * progress spinner and the arrow. This class is to separate drawing from animation.
604 | */
605 | private static class Ring {
606 | final RectF mTempBounds = new RectF();
607 | final Paint mPaint = new Paint();
608 | final Paint mArrowPaint = new Paint();
609 | final Paint mCirclePaint = new Paint();
610 |
611 | float mStartTrim = 0f;
612 | float mEndTrim = 0f;
613 | float mRotation = 0f;
614 | float mStrokeWidth = 5f;
615 |
616 | int[] mColors;
617 | // mColorIndex represents the offset into the available mColors that the
618 | // progress circle should currently display. As the progress circle is
619 | // animating, the mColorIndex moves by one to the next available color.
620 | int mColorIndex;
621 | float mStartingStartTrim;
622 | float mStartingEndTrim;
623 | float mStartingRotation;
624 | boolean mShowArrow;
625 | Path mArrow;
626 | float mArrowScale = 1;
627 | float mRingCenterRadius;
628 | int mArrowWidth;
629 | int mArrowHeight;
630 | int mAlpha = 255;
631 | int mCurrentColor;
632 |
633 | Ring() {
634 | mPaint.setStrokeCap(Paint.Cap.SQUARE);
635 | mPaint.setAntiAlias(true);
636 | mPaint.setStyle(Paint.Style.STROKE);
637 |
638 | mArrowPaint.setStyle(Paint.Style.FILL);
639 | mArrowPaint.setAntiAlias(true);
640 |
641 | mCirclePaint.setColor(Color.TRANSPARENT);
642 | }
643 |
644 | /**
645 | * Sets the dimensions of the arrowhead.
646 | *
647 | * @param width width of the hypotenuse of the arrow head
648 | * @param height height of the arrow point
649 | */
650 | void setArrowDimensions(float width, float height) {
651 | mArrowWidth = (int) width;
652 | mArrowHeight = (int) height;
653 | }
654 |
655 | void setStrokeCap(Paint.Cap strokeCap) {
656 | mPaint.setStrokeCap(strokeCap);
657 | }
658 |
659 | Paint.Cap getStrokeCap() {
660 | return mPaint.getStrokeCap();
661 | }
662 |
663 | float getArrowWidth() {
664 | return mArrowWidth;
665 | }
666 |
667 | float getArrowHeight() {
668 | return mArrowHeight;
669 | }
670 |
671 | /**
672 | * Draw the progress spinner
673 | */
674 | void draw(Canvas c, Rect bounds) {
675 | final RectF arcBounds = mTempBounds;
676 | float arcRadius = mRingCenterRadius + mStrokeWidth / 2f;
677 | if (mRingCenterRadius <= 0) {
678 | // If center radius is not set, fill the bounds
679 | arcRadius = Math.min(bounds.width(), bounds.height()) / 2f - Math.max(
680 | (mArrowWidth * mArrowScale) / 2f, mStrokeWidth / 2f);
681 | }
682 | arcBounds.set(bounds.centerX() - arcRadius,
683 | bounds.centerY() - arcRadius,
684 | bounds.centerX() + arcRadius,
685 | bounds.centerY() + arcRadius);
686 |
687 | final float startAngle = (mStartTrim + mRotation) * 360;
688 | final float endAngle = (mEndTrim + mRotation) * 360;
689 | float sweepAngle = endAngle - startAngle;
690 |
691 | mPaint.setColor(mCurrentColor);
692 | mPaint.setAlpha(mAlpha);
693 |
694 | // Draw the background first
695 | float inset = mStrokeWidth / 2f; // Calculate inset to draw inside the arc
696 | arcBounds.inset(inset, inset); // Apply inset
697 | c.drawCircle(arcBounds.centerX(), arcBounds.centerY(), arcBounds.width() / 2f,
698 | mCirclePaint);
699 | arcBounds.inset(-inset, -inset); // Revert the inset
700 |
701 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
702 |
703 | drawTriangle(c, startAngle, sweepAngle, arcBounds);
704 | }
705 |
706 | void drawTriangle(Canvas c, float startAngle, float sweepAngle, RectF bounds) {
707 | if (mShowArrow) {
708 | if (mArrow == null) {
709 | mArrow = new android.graphics.Path();
710 | mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD);
711 | } else {
712 | mArrow.reset();
713 | }
714 | float centerRadius = Math.min(bounds.width(), bounds.height()) / 2f;
715 | float inset = mArrowWidth * mArrowScale / 2f;
716 | // Update the path each time. This works around an issue in SKIA
717 | // where concatenating a rotation matrix to a scale matrix
718 | // ignored a starting negative rotation. This appears to have
719 | // been fixed as of API 21.
720 | mArrow.moveTo(0, 0);
721 | mArrow.lineTo(mArrowWidth * mArrowScale, 0);
722 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
723 | * mArrowScale));
724 | mArrow.offset(centerRadius + bounds.centerX() - inset,
725 | bounds.centerY() + mStrokeWidth / 2f);
726 | mArrow.close();
727 | // draw a triangle
728 | mArrowPaint.setColor(mCurrentColor);
729 | mArrowPaint.setAlpha(mAlpha);
730 | c.save();
731 | c.rotate(startAngle + sweepAngle, bounds.centerX(),
732 | bounds.centerY());
733 | c.drawPath(mArrow, mArrowPaint);
734 | c.restore();
735 | }
736 | }
737 |
738 | /**
739 | * Sets the colors the progress spinner alternates between.
740 | *
741 | * @param colors array of ARGB colors. Must be non-{@code null}.
742 | */
743 | void setColors(@NonNull int[] colors) {
744 | mColors = colors;
745 | // if colors are reset, make sure to reset the color index as well
746 | setColorIndex(0);
747 | }
748 |
749 | int[] getColors() {
750 | return mColors;
751 | }
752 |
753 | /**
754 | * Sets the absolute color of the progress spinner. This is should only
755 | * be used when animating between current and next color when the
756 | * spinner is rotating.
757 | *
758 | * @param color an ARGB color
759 | */
760 | void setColor(int color) {
761 | mCurrentColor = color;
762 | }
763 |
764 | /**
765 | * Sets the background color of the circle inside the spinner.
766 | */
767 | void setBackgroundColor(int color) {
768 | mCirclePaint.setColor(color);
769 | }
770 |
771 | int getBackgroundColor() {
772 | return mCirclePaint.getColor();
773 | }
774 |
775 | /**
776 | * @param index index into the color array of the color to display in
777 | * the progress spinner.
778 | */
779 | void setColorIndex(int index) {
780 | mColorIndex = index;
781 | mCurrentColor = mColors[mColorIndex];
782 | }
783 |
784 | /**
785 | * @return int describing the next color the progress spinner should use when drawing.
786 | */
787 | int getNextColor() {
788 | return mColors[getNextColorIndex()];
789 | }
790 |
791 | int getNextColorIndex() {
792 | return (mColorIndex + 1) % (mColors.length);
793 | }
794 |
795 | /**
796 | * Proceed to the next available ring color. This will automatically
797 | * wrap back to the beginning of colors.
798 | */
799 | void goToNextColor() {
800 | setColorIndex(getNextColorIndex());
801 | }
802 |
803 | void setColorFilter(ColorFilter filter) {
804 | mPaint.setColorFilter(filter);
805 | }
806 |
807 | /**
808 | * @param alpha alpha of the progress spinner and associated arrowhead.
809 | */
810 | void setAlpha(int alpha) {
811 | mAlpha = alpha;
812 | }
813 |
814 | /**
815 | * @return current alpha of the progress spinner and arrowhead
816 | */
817 | int getAlpha() {
818 | return mAlpha;
819 | }
820 |
821 | /**
822 | * @param strokeWidth set the stroke width of the progress spinner in pixels.
823 | */
824 | void setStrokeWidth(float strokeWidth) {
825 | mStrokeWidth = strokeWidth;
826 | mPaint.setStrokeWidth(strokeWidth);
827 | }
828 |
829 | float getStrokeWidth() {
830 | return mStrokeWidth;
831 | }
832 |
833 | void setStartTrim(float startTrim) {
834 | mStartTrim = startTrim;
835 | }
836 |
837 | float getStartTrim() {
838 | return mStartTrim;
839 | }
840 |
841 | float getStartingStartTrim() {
842 | return mStartingStartTrim;
843 | }
844 |
845 | float getStartingEndTrim() {
846 | return mStartingEndTrim;
847 | }
848 |
849 | int getStartingColor() {
850 | return mColors[mColorIndex];
851 | }
852 |
853 | void setEndTrim(float endTrim) {
854 | mEndTrim = endTrim;
855 | }
856 |
857 | float getEndTrim() {
858 | return mEndTrim;
859 | }
860 |
861 | void setRotation(float rotation) {
862 | mRotation = rotation;
863 | }
864 |
865 | float getRotation() {
866 | return mRotation;
867 | }
868 |
869 | /**
870 | * @param centerRadius inner radius in px of the circle the progress spinner arc traces
871 | */
872 | void setCenterRadius(float centerRadius) {
873 | mRingCenterRadius = centerRadius;
874 | }
875 |
876 | float getCenterRadius() {
877 | return mRingCenterRadius;
878 | }
879 |
880 | /**
881 | * @param show {@code true} if should show the arrow head on the progress spinner
882 | */
883 | void setShowArrow(boolean show) {
884 | if (mShowArrow != show) {
885 | mShowArrow = show;
886 | }
887 | }
888 |
889 | boolean getShowArrow() {
890 | return mShowArrow;
891 | }
892 |
893 | /**
894 | * @param scale scale of the arrowhead for the spinner
895 | */
896 | void setArrowScale(float scale) {
897 | if (scale != mArrowScale) {
898 | mArrowScale = scale;
899 | }
900 | }
901 |
902 | float getArrowScale() {
903 | return mArrowScale;
904 | }
905 |
906 | /**
907 | * @return The amount the progress spinner is currently rotated, between [0..1].
908 | */
909 | float getStartingRotation() {
910 | return mStartingRotation;
911 | }
912 |
913 | /**
914 | * If the start / end trim are offset to begin with, store them so that animation starts
915 | * from that offset.
916 | */
917 | void storeOriginals() {
918 | mStartingStartTrim = mStartTrim;
919 | mStartingEndTrim = mEndTrim;
920 | mStartingRotation = mRotation;
921 | }
922 |
923 | /**
924 | * Reset the progress spinner to default rotation, start and end angles.
925 | */
926 | void resetOriginals() {
927 | mStartingStartTrim = 0;
928 | mStartingEndTrim = 0;
929 | mStartingRotation = 0;
930 | setStartTrim(0);
931 | setEndTrim(0);
932 | setRotation(0);
933 | }
934 | }
935 | }
--------------------------------------------------------------------------------
/Loadingbutton/src/main/java/com/flod/loadingbutton/LoadingButton.java:
--------------------------------------------------------------------------------
1 | package com.flod.loadingbutton;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ObjectAnimator;
6 | import android.animation.ValueAnimator;
7 | import android.annotation.SuppressLint;
8 | import android.annotation.TargetApi;
9 | import android.content.Context;
10 | import android.content.res.TypedArray;
11 | import android.graphics.Bitmap;
12 | import android.graphics.Canvas;
13 | import android.graphics.Outline;
14 | import android.graphics.Paint;
15 | import android.graphics.Path;
16 | import android.graphics.Rect;
17 | import android.graphics.drawable.Drawable;
18 | import android.os.Build;
19 | import android.text.TextUtils;
20 | import android.util.AttributeSet;
21 | import android.view.MotionEvent;
22 | import android.view.View;
23 |
24 | import androidx.annotation.DrawableRes;
25 | import androidx.annotation.IntDef;
26 | import androidx.annotation.Nullable;
27 | import androidx.annotation.Px;
28 | import androidx.annotation.RequiresApi;
29 | import androidx.core.content.ContextCompat;
30 |
31 | import com.flod.drawabletextview.DrawableTextView;
32 |
33 | import java.lang.annotation.Retention;
34 | import java.lang.annotation.RetentionPolicy;
35 |
36 | /**
37 | * SimpleDes:
38 | * Creator: Flod
39 | * Date: 2019-06-13
40 | * UseDes:
41 | *
42 | * 1、改变loading的大小 √
43 | * 2、收缩动画后不居中 √
44 | * 3、收缩后的大小随loading的大小决定 √
45 | * 4、设置loading 可以设置为上下左右 √
46 | * 5、重复start的动画处理 √
47 | * 6、恢复动画还没结束时,点击收缩会变成恢复的状态 √
48 | * 7、正在显示EndDrawable时,再次点击start会变成恢复状态的加载
49 | * 先执行 beginChangeSize(true) 后执行 beginChangeSize(false); √
50 | *
51 | * 8、多次start和end 会出错 √
52 | * 9、设置完Drawable大小后,start后再次设置rootView大小失控,是因为原来是wrap_content √
53 | * 10、start和compete同时按Loading没有关 √
54 | * 11、偶尔由onTouchEvent导致出现selector 状态异常,√
55 | * 12、收缩状态下,先点击Fail 再点击cancel,会再跑一遍收缩恢复 √
56 | * 13、收缩后定义形状 √
57 | * 14、设置按钮圆角 √
58 | */
59 |
60 | @SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
61 | public class LoadingButton extends DrawableTextView {
62 |
63 | private int curStatus = STATUS.IDE; //当前的状态
64 |
65 | interface STATUS {
66 | int IDE = 0;
67 | int SHRINKING = 1;
68 | int LOADING = 2;
69 | int END_DRAWABLE_SHOWING = 3;
70 | int RESTORING = 4;
71 | }
72 |
73 |
74 | //Arr
75 | private boolean enableShrink; //是否开启收缩动画, 默认开启
76 | private boolean enableRestore; //完成时是否恢复按钮 默认关闭
77 | private boolean disableClickOnLoading; //Loading中禁用点击, 默认开启
78 |
79 | private Drawable[] mDrawablesSaved;
80 | private int mDrawablePaddingSaved;
81 | private CharSequence mTextSaved;
82 | private boolean mEnableTextInCenterSaved;
83 | private final int[] mRootViewSizeSaved = new int[]{0, 0};
84 |
85 | private ValueAnimator mShrinkAnimator;
86 | private int mShrinkDuration; //收缩和恢复的时间 默认450ms
87 | private int mShrinkShape; //收缩后的形状
88 |
89 | @IntDef({ShrinkShape.DEFAULT, ShrinkShape.OVAL})
90 | @Retention(RetentionPolicy.SOURCE)
91 | public @interface ShrinkShape {
92 | int DEFAULT = 0; //默认形状
93 | int OVAL = 1; //圆形
94 | }
95 |
96 |
97 | private CircularProgressDrawable mLoadingDrawable;
98 | private OnStatusChangedListener mListener;
99 | private EndDrawable mEndDrawable;
100 | private int mLoadingSize;
101 | private int mLoadingPosition;
102 |
103 | private boolean isSizeChanging; //当前布局尺寸正发生改变
104 | private boolean nextReverse; //下一步是否是恢复动画
105 | private boolean isFail; //是否失败
106 |
107 |
108 | public LoadingButton(Context context) {
109 | super(context);
110 | init(context, null);
111 | }
112 |
113 | public LoadingButton(Context context, AttributeSet attrs) {
114 | super(context, attrs);
115 | init(context, attrs);
116 | }
117 |
118 | public LoadingButton(Context context, AttributeSet attrs, int defStyleAttr) {
119 | super(context, attrs, defStyleAttr);
120 | init(context, attrs);
121 | }
122 |
123 | private void init(Context context, AttributeSet attrs) {
124 |
125 | //getConfig
126 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoadingButton);
127 |
128 | disableClickOnLoading = array.getBoolean(R.styleable.LoadingButton_disableClickOnLoading, true);
129 |
130 | enableShrink = array.getBoolean(R.styleable.LoadingButton_enableShrink, true);
131 | enableRestore = array.getBoolean(R.styleable.LoadingButton_enableRestore, false);
132 | mShrinkDuration = array.getInt(R.styleable.LoadingButton_shrinkDuration, 450);
133 | mShrinkShape = array.getInt(R.styleable.LoadingButton_shrinkShape, ShrinkShape.DEFAULT);
134 |
135 | int loadingDrawableSize = array.getDimensionPixelSize(R.styleable.LoadingButton_loadingEndDrawableSize, (int) (enableShrink ? getTextSize() * 2 : getTextSize()));
136 | int loadingDrawableColor = array.getColor(R.styleable.LoadingButton_loadingDrawableColor, getTextColors().getDefaultColor());
137 | int loadingDrawablePosition = array.getInt(R.styleable.LoadingButton_loadingDrawablePosition, POSITION.START);
138 | int endSuccessDrawable = array.getResourceId(R.styleable.LoadingButton_endSuccessDrawable, -1);
139 | int endFailDrawableResId = array.getResourceId(R.styleable.LoadingButton_endFailDrawable, -1);
140 | int endDrawableAppearTime = array.getInt(R.styleable.LoadingButton_endDrawableAppearTime, EndDrawable.DEFAULT_APPEAR_DURATION);
141 | int endDrawableDuration = array.getInt(R.styleable.LoadingButton_endDrawableDuration, 900);
142 |
143 | array.recycle();
144 |
145 | //initLoadingDrawable
146 | mLoadingDrawable = new CircularProgressDrawable(context);
147 | mLoadingDrawable.setColorSchemeColors(loadingDrawableColor);
148 | mLoadingDrawable.setStrokeWidth(loadingDrawableSize * 0.14f);
149 |
150 | mLoadingSize = loadingDrawableSize;
151 | mLoadingPosition = loadingDrawablePosition;
152 | setDrawable(mLoadingPosition, mLoadingDrawable, loadingDrawableSize, loadingDrawableSize);
153 | setEnableCenterDrawables(true);
154 |
155 | //initLoadingDrawable
156 | if (endSuccessDrawable != -1 || endFailDrawableResId != -1) {
157 | mEndDrawable = new EndDrawable(endSuccessDrawable, endFailDrawableResId);
158 | mEndDrawable.mAppearAnimator.setDuration(endDrawableAppearTime);
159 | mEndDrawable.setKeepDuration(endDrawableDuration);
160 | }
161 |
162 | //initShrinkAnimator
163 | setUpShrinkAnimator();
164 |
165 | //initShrinkShape
166 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
167 | if (mShrinkShape > 0) {
168 | setClipToOutline(true);
169 | setOutlineProvider(new ShrinkShapeOutlineProvider());
170 | }
171 |
172 | }
173 |
174 |
175 | //Start|End -> true Top|Bottom ->false
176 | setEnableTextInCenter(mLoadingPosition % 2 == 0);
177 |
178 |
179 | if (isInEditMode()) {
180 | mLoadingDrawable.setStartEndTrim(0, 0.8f);
181 | }
182 |
183 | }
184 |
185 |
186 | /**
187 | * 设置收缩动画,主要用来收缩和恢复布局的宽度,动画开始前会保存一些收缩前的参数(文字,其他Drawable等)
188 | */
189 | private void setUpShrinkAnimator() {
190 | mShrinkAnimator = ValueAnimator.ofFloat(0, 1f);
191 | mShrinkAnimator.setDuration(mShrinkDuration);
192 | mShrinkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
193 | @Override
194 | public void onAnimationUpdate(ValueAnimator animation) {
195 | // y = kx + b
196 | // b = getRootViewSize()
197 | // k = getRootViewSize() - getLoadingSize
198 | getLayoutParams().width = (int) ((getShrinkSize() - mRootViewSizeSaved[0]) * (float) animation.getAnimatedValue() + mRootViewSizeSaved[0]);
199 | getLayoutParams().height = (int) ((getShrinkSize() - mRootViewSizeSaved[1]) * (float) animation.getAnimatedValue() + mRootViewSizeSaved[1]);
200 | requestLayout();
201 | }
202 | });
203 |
204 |
205 | mShrinkAnimator.addListener(new AnimatorListenerAdapter() {
206 |
207 | //onAnimationStart(Animator animation, boolean isReverse) 在7.0测试没有调用fuck
208 | @Override
209 | public void onAnimationStart(Animator animation) {
210 | if (!nextReverse) {
211 | //begin shrink
212 | curStatus = STATUS.SHRINKING;
213 | isSizeChanging = true;
214 | if (mListener != null) {
215 | mListener.onShrinking();
216 | }
217 |
218 | LoadingButton.super.setText("", BufferType.NORMAL);
219 | setCompoundDrawablePadding(0);
220 | setCompoundDrawablesRelative(mLoadingDrawable, null, null, null);
221 | setEnableTextInCenter(false);
222 |
223 | } else {
224 | //begin restore
225 | stopLoading();
226 | curStatus = STATUS.RESTORING;
227 | if (mListener != null) {
228 | mListener.onRestoring();
229 | }
230 |
231 | }
232 | }
233 |
234 | @Override
235 | public void onAnimationEnd(Animator animation) {
236 | if (!nextReverse) {
237 | //shrink over
238 | curStatus = STATUS.LOADING;
239 | startLoading();
240 | nextReverse = true;
241 |
242 | } else {
243 | //restore over
244 | isSizeChanging = false;
245 | nextReverse = false;
246 | toIde();
247 | if (mListener != null) {
248 | mListener.onRestored();
249 | }
250 | }
251 | }
252 |
253 | });
254 |
255 | }
256 |
257 |
258 | /**
259 | * 开始收缩或恢复
260 | *
261 | * @param isReverse true:恢复,且开始时停止Loading false:收缩,且结束时开始Loading
262 | * @param lastFrame 是否只显示最后一帧
263 | */
264 | private void beginShrink(boolean isReverse, boolean lastFrame) {
265 | if (mShrinkAnimator.isRunning()) {
266 | //如果上一个动画还在执行,就结束到最后一帧
267 | mShrinkAnimator.end();
268 | }
269 | this.nextReverse = isReverse;
270 | if (!isReverse) {
271 | mShrinkAnimator.start();
272 |
273 | } else {
274 | mShrinkAnimator.reverse();
275 |
276 | }
277 | if (lastFrame) {
278 | mShrinkAnimator.end();
279 | }
280 |
281 | }
282 |
283 |
284 |
285 | /**
286 | * 保存一些即将被改变的数据或状态
287 | */
288 | private void saveStatus() {
289 | mTextSaved = getText();
290 | mDrawablesSaved = copyDrawables(true);
291 | mDrawablePaddingSaved = getCompoundDrawablePadding();
292 | mEnableTextInCenterSaved = isEnableTextInCenter();
293 | }
294 |
295 | /**
296 | * 恢复保存的状态
297 | */
298 | private void restoreStatus() {
299 | setText(mTextSaved);
300 | setCompoundDrawablePadding(mDrawablePaddingSaved);
301 | setCompoundDrawablesRelative(mDrawablesSaved[POSITION.START], mDrawablesSaved[POSITION.TOP], mDrawablesSaved[POSITION.END], mDrawablesSaved[POSITION.BOTTOM]);
302 | setEnableTextInCenter(mEnableTextInCenterSaved);
303 | getLayoutParams().width = mRootViewSizeSaved[0];
304 | getLayoutParams().height = mRootViewSizeSaved[1];
305 | requestLayout();
306 |
307 | addOnLayoutChangeListener(new OnLayoutChangeListener() {
308 | @Override
309 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
310 | measureTextHeight();
311 | measureTextWidth();
312 | removeOnLayoutChangeListener(this);
313 | }
314 | });
315 |
316 | }
317 |
318 | private void toIde() {
319 | curStatus = STATUS.IDE;
320 | restoreStatus();
321 | isFail = false;
322 |
323 | }
324 |
325 | /**
326 | * 如果disableClickOnLoading==true,且不是闲置状态,点击会无效
327 | */
328 | @SuppressLint("ClickableViewAccessibility")
329 | @Override
330 | public boolean onTouchEvent(MotionEvent event) {
331 | //disable click
332 | if (event.getAction() == MotionEvent.ACTION_DOWN
333 | && disableClickOnLoading && curStatus != STATUS.IDE)
334 | return true;
335 | return super.onTouchEvent(event);
336 | }
337 |
338 |
339 | /**
340 | * 开始加载
341 | */
342 | private void startLoading() {
343 | curStatus = STATUS.LOADING;
344 |
345 | if (!mLoadingDrawable.isRunning()) {
346 | mLoadingDrawable.start();
347 | }
348 |
349 | if (mListener != null) {
350 | mListener.onLoadingStart();
351 | }
352 | }
353 |
354 | /**
355 | * 停止加载
356 | */
357 | private void stopLoading() {
358 | if (mLoadingDrawable.isRunning()) {
359 | mLoadingDrawable.stop();
360 | if (mListener != null) {
361 | mListener.onLoadingStop();
362 | }
363 | }
364 |
365 | }
366 |
367 | /**
368 | * 取消当前所有的动画进程
369 | *
370 | * @param withRestoreAnim 是否显示恢复动画
371 | */
372 | private void cancelAllRunning(boolean withRestoreAnim) {
373 |
374 | switch (curStatus) {
375 | case STATUS.SHRINKING:
376 | beginShrink(true, !withRestoreAnim);
377 | break;
378 | case STATUS.LOADING: {
379 | stopLoading();
380 | if (enableShrink) {
381 | beginShrink(true, !withRestoreAnim);
382 | } else {
383 | toIde();
384 | }
385 | break;
386 | }
387 | case STATUS.END_DRAWABLE_SHOWING:
388 | if (mEndDrawable != null) {
389 | mEndDrawable.cancel(withRestoreAnim);
390 | } else {
391 | beginShrink(true, !withRestoreAnim);
392 | }
393 | break;
394 | case STATUS.RESTORING:
395 | if (!withRestoreAnim)
396 | mShrinkAnimator.end();
397 | else {
398 | nextReverse = true;
399 | mShrinkAnimator.reverse();
400 | }
401 | break;
402 | }
403 |
404 | }
405 |
406 |
407 | /**
408 | * 开始加载,默认禁用加载时的点击
409 | */
410 | public void start() {
411 | //cancel last loading
412 | cancelAllRunning(false);
413 |
414 | saveStatus();
415 | if (enableShrink) {
416 | beginShrink(false, false);
417 | } else {
418 | if (TextUtils.isEmpty(getText())) {
419 | setCompoundDrawablePadding(0);
420 | }
421 | startLoading();
422 | }
423 | }
424 |
425 | /**
426 | * 完成加载,显示对应的EndDrawable
427 | *
428 | *
429 | * @param isSuccess 是否加载成功,将参数传递给回调{@link OnStatusChangedListener#onCompleted(boolean)} ()},
430 | */
431 | public void complete(boolean isSuccess) {
432 | stopLoading();
433 | if (mEndDrawable != null) {
434 | if (mShrinkAnimator.isRunning())
435 | mShrinkAnimator.end();
436 |
437 | mEndDrawable.show(isSuccess);
438 | } else {
439 | //No EndDrawable,enableShrink
440 | this.isFail = !isSuccess;
441 | if (enableShrink) {
442 | if (enableRestore)
443 | beginShrink(true, curStatus != STATUS.LOADING);
444 |
445 | else {
446 | if (mListener != null) {
447 | mListener.onCompleted(isSuccess);
448 | }
449 | }
450 | } else {
451 | //No EndDrawable,disableShrink
452 | if (mListener != null) {
453 | mListener.onCompleted(isSuccess);
454 | }
455 |
456 | if (enableRestore)
457 | toIde();
458 | }
459 |
460 |
461 | }
462 | }
463 |
464 |
465 | /**
466 | * 取消加载,默认显示恢复动画
467 | */
468 | public void cancel() {
469 | cancel(true);
470 | }
471 |
472 | /**
473 | * 取消加载
474 | *
475 | * @param withRestoreAnim 是否显示收缩动画
476 | */
477 | public void cancel(boolean withRestoreAnim) {
478 | if (curStatus != STATUS.IDE) {
479 | cancelAllRunning(withRestoreAnim);
480 |
481 | if (mListener != null)
482 | mListener.onCanceled();
483 | }
484 | }
485 |
486 | /**
487 | * 设置加载中不可点击
488 | */
489 | public LoadingButton setDisableClickOnLoading(boolean disable) {
490 | this.disableClickOnLoading = disable;
491 | return this;
492 | }
493 |
494 | /**
495 | * 设置是否显示收缩动画
496 | */
497 | public LoadingButton setEnableShrink(boolean enable) {
498 | this.enableShrink = enable;
499 | return this;
500 | }
501 |
502 | /**
503 | * 完成后是否恢复
504 | */
505 | public LoadingButton setEnableRestore(boolean enable) {
506 | this.enableRestore = enable;
507 | return this;
508 | }
509 |
510 |
511 | /**
512 | * 设置收缩后的形状
513 | *
514 | * @see ShrinkShape
515 | */
516 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
517 | public LoadingButton setShrinkShape(@ShrinkShape int shrinkShape) {
518 | this.mShrinkShape = shrinkShape;
519 | if (!(getOutlineProvider() instanceof ShrinkShapeOutlineProvider)) {
520 | setOutlineProvider(new ShrinkShapeOutlineProvider());
521 | setClipToOutline(true);
522 | } else
523 | invalidateOutline();
524 |
525 | return this;
526 | }
527 |
528 | /**
529 | * 获取收缩后的形状
530 | *
531 | * @see ShrinkShape
532 | */
533 | public int getShrinkShape() {
534 | return mShrinkShape;
535 | }
536 |
537 | /**
538 | * 收缩后的尺寸
539 | */
540 | public int getShrinkSize() {
541 | return Math.max(Math.min(mRootViewSizeSaved[0], mRootViewSizeSaved[1]), getLoadingEndDrawableSize());
542 | }
543 |
544 | /**
545 | * 收缩的Animator,可自行设置参数
546 | */
547 | public ValueAnimator getShrinkAnimator() {
548 | return mShrinkAnimator;
549 | }
550 |
551 | /**
552 | * 设置收缩的总时间
553 | */
554 | public LoadingButton setShrinkDuration(long milliseconds) {
555 | mShrinkAnimator.setDuration(milliseconds);
556 | return this;
557 | }
558 |
559 | /**
560 | * 收缩的总时间
561 | */
562 | public int getShrinkDuration() {
563 | return mShrinkDuration;
564 | }
565 |
566 | /**
567 | * 拿到CircularProgressDrawable 可自行设置想要的参数
568 | *
569 | * @return CircularProgressDrawable
570 | */
571 | public CircularProgressDrawable getLoadingDrawable() {
572 | return mLoadingDrawable;
573 | }
574 |
575 |
576 | /**
577 | * 设置LoadingDrawable的位置,如果开启收缩动画,则建议放Start或End
578 | *
579 | * @param position {@link DrawableTextView.POSITION}
580 | */
581 | public LoadingButton setLoadingPosition(@POSITION int position) {
582 | boolean enableTextInCenter = position % 2 == 0;
583 | setEnableTextInCenter(enableTextInCenter);
584 | mEnableTextInCenterSaved = enableTextInCenter;
585 | setDrawable(mLoadingPosition, null, 0, 0);
586 | mLoadingPosition = position;
587 | setDrawable(position, getLoadingDrawable(), getLoadingEndDrawableSize(), getLoadingEndDrawableSize());
588 | return this;
589 | }
590 |
591 |
592 | /**
593 | * 设置LoadingDrawable和EnaDrawable的尺寸
594 | */
595 | public LoadingButton setLoadingEndDrawableSize(@Px int px) {
596 | mLoadingSize = px;
597 | setDrawable(mLoadingPosition, mLoadingDrawable, px, px);
598 | return this;
599 | }
600 |
601 |
602 | public int getLoadingEndDrawableSize() {
603 | return mLoadingSize;
604 | }
605 |
606 |
607 | public LoadingButton setSuccessDrawable(@DrawableRes int drawableRes) {
608 | if (mEndDrawable == null)
609 | mEndDrawable = new EndDrawable(drawableRes, -1);
610 | else {
611 | mEndDrawable.setSuccessDrawable(drawableRes);
612 | }
613 | return this;
614 | }
615 |
616 | public LoadingButton setSuccessDrawable(Drawable drawable) {
617 | if (mEndDrawable == null)
618 | mEndDrawable = new EndDrawable(drawable, null);
619 | else {
620 | mEndDrawable.setSuccessDrawable(drawable);
621 | }
622 | return this;
623 | }
624 |
625 | public LoadingButton setFailDrawable(@DrawableRes int drawableRes) {
626 | if (mEndDrawable == null)
627 | mEndDrawable = new EndDrawable(-1, drawableRes);
628 | else {
629 | mEndDrawable.setFailDrawable(drawableRes);
630 | }
631 | return this;
632 | }
633 |
634 | public LoadingButton setFailDrawable(Drawable drawable) {
635 | if (mEndDrawable == null)
636 | mEndDrawable = new EndDrawable(null, drawable);
637 | else {
638 | mEndDrawable.setFailDrawable(drawable);
639 | }
640 | return this;
641 | }
642 |
643 | /**
644 | * EndDrawable 停留显示的时间
645 | */
646 | public LoadingButton setEndDrawableKeepDuration(long milliseconds) {
647 | if (mEndDrawable != null)
648 | mEndDrawable.setKeepDuration(milliseconds);
649 | return this;
650 | }
651 |
652 | public long getEndDrawableDuration() {
653 | if (mEndDrawable != null)
654 | return mEndDrawable.mKeepDuration;
655 | return EndDrawable.DEFAULT_APPEAR_DURATION;
656 | }
657 |
658 |
659 | /**
660 | * CompleteDrawable或FailDrawable 变大出现的时间
661 | */
662 | public LoadingButton setEndDrawableAppearDuration(long milliseconds) {
663 | if (mEndDrawable != null)
664 | mEndDrawable.getAppearAnimator().setDuration(milliseconds);
665 | return this;
666 | }
667 |
668 | @Nullable
669 | public ObjectAnimator getEndDrawableAnimator() {
670 | if (mEndDrawable != null) {
671 | return mEndDrawable.getAppearAnimator();
672 | }
673 | return null;
674 | }
675 |
676 |
677 | @Override
678 | public void setCompoundDrawablePadding(int pad) {
679 | super.setCompoundDrawablePadding(pad);
680 | if (curStatus == STATUS.IDE)
681 | mDrawablePaddingSaved = pad;
682 | }
683 |
684 |
685 | @Override
686 | public void setText(CharSequence text, BufferType type) {
687 | if (TextUtils.isEmpty(text) && curStatus != STATUS.IDE) {
688 | setCompoundDrawablePadding(0);
689 | }
690 |
691 | if (enableShrink && isSizeChanging) {
692 | return;
693 | }
694 | super.setText(text, type);
695 | }
696 |
697 |
698 | @Override
699 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
700 | super.onLayout(changed, left, top, right, bottom);
701 | if (curStatus == STATUS.IDE) {
702 | mRootViewSizeSaved[0] = getWidth();
703 | mRootViewSizeSaved[1] = getHeight();
704 | }
705 | }
706 |
707 | /**
708 | * 第一次Layout
709 | */
710 | @Override
711 | protected void onFirstLayout(int left, int top, int right, int bottom) {
712 | super.onFirstLayout(left, top, right, bottom);
713 | saveStatus();
714 |
715 |
716 | }
717 |
718 | @Override
719 | protected void onDraw(Canvas canvas) {
720 | if (mEndDrawable != null)
721 | mEndDrawable.draw(canvas);
722 | super.onDraw(canvas);
723 | }
724 |
725 |
726 | @SuppressWarnings("SameParameterValue")
727 | public class EndDrawable {
728 | private static final int DEFAULT_APPEAR_DURATION = 300;
729 | private Bitmap mSuccessBitmap;
730 | private Bitmap mFailBitmap;
731 | private Paint mPaint;
732 | private final Rect mBounds = new Rect();
733 | private Path mCirclePath; //圆形裁剪路径
734 | private ObjectAnimator mAppearAnimator;
735 | private long mKeepDuration;
736 | private float animValue;
737 | int[] offsetTemp = new int[]{0, 0};
738 | private boolean isShowing;
739 | private Runnable mRunnable;
740 |
741 | private EndDrawable(@Nullable Drawable successDrawable, @Nullable Drawable failDrawable) {
742 | setSuccessDrawable(successDrawable);
743 | setFailDrawable(failDrawable);
744 | init();
745 | }
746 |
747 | private EndDrawable(@DrawableRes int successResId, @DrawableRes int failResId) {
748 | setSuccessDrawable(successResId);
749 | setFailDrawable(failResId);
750 | init();
751 | }
752 |
753 | private void init() {
754 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
755 | mCirclePath = new Path();
756 | mAppearAnimator = ObjectAnimator.ofFloat(this, "animValue", 1.0f);
757 | mRunnable = new Runnable() {
758 | @Override
759 | public void run() {
760 |
761 | if (enableShrink) {
762 | if (enableRestore) {
763 | setAnimValue(0);
764 | beginShrink(true, !nextReverse);
765 | }
766 | } else {
767 | if (enableRestore) {
768 | setAnimValue(0);
769 | toIde();
770 | }
771 | }
772 |
773 | isShowing = false;
774 | }
775 | };
776 | mAppearAnimator.addListener(new AnimatorListenerAdapter() {
777 | @Override
778 | public void onAnimationStart(Animator animation) {
779 | super.onAnimationStart(animation);
780 | curStatus = STATUS.END_DRAWABLE_SHOWING;
781 | if (mListener != null) {
782 | mListener.onEndDrawableAppear(!isFail, mEndDrawable);
783 | }
784 | }
785 |
786 | @Override
787 | public void onAnimationEnd(Animator animation) {
788 | if (isShowing) {
789 | postDelayed(mRunnable, mKeepDuration);
790 | }
791 | if (mListener != null) {
792 | mListener.onCompleted(!isFail);
793 | }
794 | }
795 | });
796 | }
797 |
798 |
799 | /**
800 | * 显示EndDrawable
801 | */
802 | private void show(boolean isSuccess) {
803 |
804 | //end showing endDrawable
805 | if (isShowing) {
806 | cancel(false);
807 | }
808 |
809 | LoadingButton.this.isFail = !isSuccess;
810 | mAppearAnimator.start();
811 | isShowing = true;
812 |
813 | }
814 |
815 | /**
816 | * 取消出现动画
817 | *
818 | * @param withAnim 是否显示恢复动画
819 | */
820 | private void cancel(boolean withAnim) {
821 | isShowing = false;
822 |
823 | getHandler().removeCallbacks(mRunnable);
824 | if (mAppearAnimator.isRunning()) {
825 | mAppearAnimator.end();
826 | }
827 |
828 | if (enableShrink)
829 | beginShrink(true, !(withAnim && nextReverse));
830 | else {
831 | toIde();
832 | }
833 | setAnimValue(0);
834 | }
835 |
836 | /**
837 | * 消失动画,暂不使用
838 | */
839 | private void hide() {
840 | if (isShowing) {
841 | cancel(false);
842 | }
843 | mAppearAnimator.reverse();
844 | isShowing = true;
845 | }
846 |
847 | /**
848 | * 测量EndDrawable需要位移的offsetX和offsetY,(因为EnaDrawable一开始是在左上角开始显示的)
849 | *
850 | * @param canvas 当前画布
851 | * @param bounds LoadingDrawable的显示范围
852 | * @param pos EndDrawable的显示位置
853 | * @return int[0] = offsetX,int[1] = offsetY
854 | */
855 | private int[] calcOffset(Canvas canvas, Rect bounds, @POSITION int pos) {
856 | final int[] offset = offsetTemp;
857 | offset[0] = canvas.getWidth() / 2;
858 | offset[1] = canvas.getHeight() / 2;
859 |
860 | switch (pos) {
861 | case POSITION.START: {
862 | offset[0] -= (int) getTextWidth() / 2 + bounds.width() + getCompoundDrawablePadding();
863 | if (enableShrink && nextReverse) {
864 | offset[0] += bounds.width() / 2;
865 | } else if (!isEnableTextInCenter()) {
866 | offset[0] += (bounds.width() + getCompoundDrawablePadding()) / 2;
867 | }
868 |
869 | offset[1] -= bounds.height() / 2;
870 | break;
871 | }
872 | case POSITION.TOP: {
873 | offset[0] -= bounds.width() / 2;
874 | offset[1] -= (int) getTextHeight() / 2 + bounds.height() + getCompoundDrawablePadding();
875 | if (enableShrink && nextReverse) {
876 | offset[1] += bounds.height() / 2;
877 | } else if (!isEnableTextInCenter()) {
878 | offset[1] += (bounds.height() + getCompoundDrawablePadding()) / 2;
879 | }
880 | break;
881 | }
882 | case POSITION.END: {
883 | offset[0] += (int) getTextWidth() / 2 + getCompoundDrawablePadding();
884 | if (enableShrink && nextReverse) {
885 | offset[0] -= bounds.width() / 2;
886 | } else if (!isEnableTextInCenter()) {
887 | offset[0] -= (bounds.width() + getCompoundDrawablePadding()) / 2;
888 | }
889 | offset[1] -= bounds.height() / 2;
890 | break;
891 | }
892 | case POSITION.BOTTOM: {
893 | offset[0] -= bounds.width() / 2;
894 | offset[1] += (int) getTextHeight() / 2 + getCompoundDrawablePadding();
895 | if (enableShrink && nextReverse) {
896 | offset[1] -= bounds.height() / 2;
897 | } else if (!isEnableTextInCenter()) {
898 | offset[1] -= (bounds.height() + getCompoundDrawablePadding()) / 2;
899 | }
900 | break;
901 | }
902 | }
903 | return offset;
904 | }
905 |
906 | /**
907 | * 绘制
908 | *
909 | * 步骤:
910 | * 将画布平移到LoadingDrawable的位置 -> 裁剪出一个圆形画布(由小到大)-> 在裁剪后的绘制图形
911 | * ->随animValue值画布逐渐变大,实现出现的效果
912 | */
913 | private void draw(Canvas canvas) {
914 | if (getAnimValue() > 0 && mLoadingDrawable != null) {
915 | final Bitmap targetBitMap = isFail ? mFailBitmap : mSuccessBitmap;
916 | if (targetBitMap != null) {
917 | final Rect bounds = mLoadingDrawable.getBounds();
918 | mBounds.right = bounds.width();
919 | mBounds.bottom = bounds.height();
920 |
921 | final int[] offsets = calcOffset(canvas, mBounds, mLoadingPosition);
922 | canvas.save();
923 | canvas.translate(offsets[0], offsets[1]);
924 | mCirclePath.reset();
925 | mCirclePath.addCircle(mBounds.centerX(), mBounds.centerY(),
926 | ((getLoadingEndDrawableSize() >> 1) * 1.5f) * animValue, Path.Direction.CW);
927 | canvas.clipPath(mCirclePath);
928 | canvas.drawBitmap(targetBitMap, null, mBounds, mPaint);
929 | canvas.restore();
930 | }
931 | }
932 | }
933 |
934 | private void setAnimValue(float animValue) {
935 | this.animValue = animValue;
936 | invalidate();
937 | }
938 |
939 | public float getAnimValue() {
940 | return animValue;
941 | }
942 |
943 | /**
944 | * EndDrawable的Animator
945 | *
946 | * @return ObjectAnimator
947 | */
948 | public ObjectAnimator getAppearAnimator() {
949 | return mAppearAnimator;
950 | }
951 |
952 | /**
953 | * EndDrawable的停留时间
954 | *
955 | * @param keepDuration millionMs
956 | */
957 | public void setKeepDuration(long keepDuration) {
958 | this.mKeepDuration = keepDuration;
959 | }
960 |
961 |
962 | public void setSuccessDrawable(Drawable drawable) {
963 | mSuccessBitmap = getBitmap(drawable);
964 | }
965 |
966 | public void setSuccessDrawable(@DrawableRes int id) {
967 | if (id != -1) {
968 | Drawable drawable = ContextCompat.getDrawable(getContext(), id);
969 | mSuccessBitmap = getBitmap(drawable);
970 | }
971 | }
972 |
973 | public void setFailDrawable(@DrawableRes int id) {
974 | if (id != -1) {
975 | Drawable failDrawable = ContextCompat.getDrawable(getContext(), id);
976 | mFailBitmap = getBitmap(failDrawable);
977 | }
978 | }
979 |
980 | public void setFailDrawable(Drawable drawable) {
981 | mSuccessBitmap = getBitmap(drawable);
982 | }
983 |
984 | }
985 |
986 | @Nullable
987 | private Bitmap getBitmap(Drawable drawable) {
988 | if (drawable != null) {
989 | Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
990 | Canvas canvas = new Canvas(bitmap);
991 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
992 | drawable.draw(canvas);
993 | return bitmap;
994 | }
995 | return null;
996 | }
997 |
998 |
999 | @Override
1000 | protected void onDetachedFromWindow() {
1001 | //release
1002 | mShrinkAnimator.cancel();
1003 | mLoadingDrawable.stop();
1004 | if (mEndDrawable != null)
1005 | mEndDrawable.mAppearAnimator.cancel();
1006 |
1007 | super.onDetachedFromWindow();
1008 |
1009 | }
1010 |
1011 |
1012 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
1013 | public class ShrinkShapeOutlineProvider extends RadiusViewOutlineProvider {
1014 | @Override
1015 | public void getOutline(View view, Outline outline) {
1016 | if (enableShrink && mShrinkShape == ShrinkShape.OVAL
1017 | && (curStatus == STATUS.LOADING || curStatus == STATUS.END_DRAWABLE_SHOWING)) {
1018 | outline.setOval(0, 0, getShrinkSize(), getShrinkSize());
1019 | } else {
1020 | super.getOutline(view, outline);
1021 | }
1022 |
1023 | }
1024 | }
1025 |
1026 |
1027 | /**
1028 | * 状态回调
1029 | * start -> onShrinking -> onLoadingStart -> complete -> onCompleted
1030 | * -> onLoadingStop -> onEndDrawableAppear -> onRestoring -> onRestored
1031 | */
1032 | public static class OnStatusChangedListener {
1033 |
1034 | public void onShrinking() {
1035 |
1036 | }
1037 |
1038 | public void onLoadingStart() {
1039 |
1040 | }
1041 |
1042 | public void onLoadingStop() {
1043 |
1044 | }
1045 |
1046 | public void onEndDrawableAppear(boolean isSuccess, EndDrawable endDrawable) {
1047 |
1048 | }
1049 |
1050 | public void onRestoring() {
1051 |
1052 | }
1053 |
1054 | public void onRestored() {
1055 |
1056 | }
1057 | public void onCompleted(boolean isSuccess) {
1058 |
1059 | }
1060 |
1061 | public void onCanceled() {
1062 |
1063 | }
1064 | }
1065 |
1066 | public LoadingButton setOnStatusChangedListener(OnStatusChangedListener listener) {
1067 | mListener = listener;
1068 | return this;
1069 | }
1070 |
1071 |
1072 | }
1073 |
--------------------------------------------------------------------------------