├── .gitignore
├── Loadingbutton
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── flod
│ │ └── loadingbutton
│ │ ├── CircularProgressDrawable.java
│ │ └── LoadingButton.java
│ └── res
│ └── values
│ └── attrs.xml
├── README.md
├── README_CN.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── flod
│ │ └── loadingbutton
│ │ └── app
│ │ ├── Glide4Engine.java
│ │ └── MainActivity.java
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_fail.xml
│ ├── ic_launcher_background.xml
│ ├── ic_successful.xml
│ ├── selector_btn.xml
│ └── shape_btn.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── dialog_edit_text.xml
│ └── dialog_seek_bar.xml
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── provider_paths.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea
5 | !/.idea/codeStyles
6 | !/.idea/dictionaries
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 | .cxx
--------------------------------------------------------------------------------
/Loadingbutton/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Loadingbutton/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 29
5 |
6 |
7 | defaultConfig {
8 | minSdkVersion 17
9 | targetSdkVersion 29
10 | versionCode 110
11 | versionName "1.1.0"
12 | }
13 |
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 |
21 | }
22 |
23 | dependencies {
24 | implementation fileTree(dir: 'libs', include: ['*.jar'])
25 |
26 | compileOnly 'androidx.appcompat:appcompat:1.2.0'
27 | api 'com.github.FlodCoding:DrawableTextView:1.0.6'
28 |
29 |
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Loadingbutton/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/Loadingbutton/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Loadingbutton/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LoadingButton [](https://jitpack.io/#FlodCoding/LoadingButton)
2 |
3 | A small and flexible button control with loading function,Extends from [DrawableTextView](https://github.com/FlodCoding/DrawableTextView),Loading animation comes from [CircularProgressDrawable](https://developer.android.google.cn/reference/android/support/v4/widget/CircularProgressDrawable?hl=en)
4 |
5 | ## Feature
6 | * Support button shrink
7 | * Support loading completion and failure icon
8 | * Can custom loading drawable color, size, position and loading button shape
9 | * Custom radius
10 |
11 |
12 | ## How to install [中文说明](https://github.com/FlodCoding/LoadingButton/blob/master/README_CN.md)
13 |
14 | root directory build.gradle
15 | ```
16 | allprojects {
17 |
18 | repositories {
19 | ...
20 | maven { url 'https://jitpack.io' }
21 |
22 | }
23 | }
24 | ```
25 |
26 | App module build.gradle
27 |
28 | ```
29 | dependencies {
30 | //Androidx
31 | implementation 'com.github.FlodCoding:LoadingButton:1.1.0-alpha01'
32 |
33 | }
34 | ```
35 | Support-appcompat stop update
36 | ~~implementation 'com.github.FlodCoding:LoadingButton:1.0.5-support'~~
37 |
38 |
39 |
40 |
41 |
42 | ## Demo [Click me to download the apk](https://github.com/FlodCoding/LoadingButton/releases/download/1.1.0-alpha01/demo-1.1.0-alpha01.apk)
43 |
44 | 
45 |
46 | ### Demo screenshot
47 | 
48 | 
49 | 
50 | 
51 |
52 | ## Basic usage
53 |
54 | ### XML
55 | ```
56 |
76 | ```
77 | ### Code
78 | ```
79 |
80 | loadingBtn.start(); //Start loading
81 | loadingBtn.complete(true); //Success
82 | loadingBtn.complete(false); //failed
83 | loadingBtn.cancel(); //Cancel loading
84 |
85 | loadingBtn.setEnableShrink(true)
86 | .setEnableRestore(true)
87 | .setDisableClickOnLoading(true)
88 | .setShrinkDuration(450)
89 | .setLoadingPosition(DrawableTextView.POSITION.START)
90 | .setSuccessDrawable(R.drawable.ic_successful)
91 | .setFailDrawable(R.drawable.ic_fail)
92 | .setEndDrawableKeepDuration(900)
93 | .setLoadingEndDrawableSize((int) (loadingBtn.getTextSize() * 2));
94 |
95 | loadingBtn.getLoadingDrawable().setStrokeWidth(loadingBtn.getTextSize() * 0.14f);
96 | loadingBtn.getLoadingDrawable().setColorSchemeColors(loadingBtn.getTextColors().getDefaultColor());
97 |
98 | ```
99 |
100 | ### State callback
101 | start --> onShrinking --> onLoadingStart
102 | complete --> onLoadingStop --> onEndDrawableAppear --> onCompleted --> onRestored
103 |
104 | ```
105 | public static class OnStatusChangedListener {
106 |
107 | public void onShrinking() {}
108 |
109 | public void onLoadingStart() {}
110 |
111 | public void onLoadingStop() {}
112 |
113 | public void onEndDrawableAppear(boolean isSuccess, EndDrawable endDrawable) {}
114 |
115 | public void onRestoring() {}
116 |
117 | public void onRestored() {}
118 |
119 | public void onCompleted(boolean isSuccess) { }
120 |
121 | public void onCanceled() {}
122 | }
123 | ```
124 |
125 | ## Attribute
126 | ### XML
127 | Attribute name|type|Default value|Description
128 | ---|:--:|:---:|---:
129 | enableShrink |boolean |true |Shrink when begin loading
130 | disableClickOnLoading |boolean |true |Disable click on loading
131 | enableRestore |boolean |false |When finished, restore button(shape and text)
132 | radius |dimension |0dp |Set the rounded corners of the button,**(need SDK>=21)**
(from([DrawableTextView](https://github.com/FlodCoding/DrawableTextView))
133 | shrinkDuration |integer |450ms |Shrink animation duration
134 | shrinkShape |enum
(Default,Oval) |Oval |Shape after shrinking **(need SDK>=21)**
(Default:Keep the original shape,Oval:Round shape)
135 | loadingEndDrawableSize |dimension |TextSize \*2 |Set the size of LoadingDrawable and EndDrawable
136 | loadingDrawableColor |reference |TextColor |Set loading color
137 | loadingDrawablePosition |enum
(Start,Top,
End,Bottom) |Start |Set the loading drawable position
138 | endSuccessDrawable |reference | null |Successful drawable
139 | endFailDrawable |reference | null |failed drawable
140 | endDrawableAppearTime |integer | 300ms |Time for completion or failure icon to emerge from nothing
141 | endDrawableDuration |integer | 900ms |endDrawable keeping time
142 |
143 | ### Public Func
144 | Method name|Parameter description|default value|Description
145 | ---|:--:|:---:|---:
146 | start() |- |- |Start loading
147 | complete(boolean isSuccess) |whether succeed |- |Complete loading
148 | cancel()
cancel(boolean withRestoreAnim) |Whether to perform restore animation |true |Cancel loading
149 | setEnableShrink(boolean enable) |- |true |Shrink when begin loading
150 | setEnableRestore(boolean enable) |- |false |When finished, restore button(shape and text)
151 | setRadius(@Px int px)
setRadiusDP(int dp) |Px/Dp |0 |Set the rounded corners of the button,**(need SDK>=21)**
(from([DrawableTextView](https://github.com/FlodCoding/DrawableTextView))
152 | setShrinkShape(@ShrinkShape int shrinkShape) |Default:Keep the original shape,Oval:Round shape |Oval |Shape after shrinking **(need SDK>=21)**
153 | setShrinkDuration(long time) |milliseconds |450ms |Shrink animation duration
154 | setLoadingEndDrawableSize(@Px int px) |Px |TextSize \*2 |Set the size of LoadingDrawable and EndDrawable
155 | setLoadingPosition(@POSITION int position) |Start,Top,End,Bottom |Start |Set the loading drawable position
156 | setSuccessDrawable(@DrawableRes int drawableRes)
setSuccessDrawable(Drawable drawable) |- | null |Successful drawable
157 | setFailDrawable(@DrawableRes int drawableRes)
setFailDrawable(Drawable drawable) |- | null |failed drawable
158 | setEndDrawableAppearDuration(long time) |milliseconds | 300ms |Time for completion or failure icon to emerge from nothing
159 | setEndDrawableKeepDuration(long time) |milliseconds | 900ms |endDrawable keeping time
160 | setOnStatusChangedListener
(OnStatusChangedListener listener)|-|null|State callbacks of buttons
161 |
162 |
163 | ## Third-party libraries used by Demo
164 |
165 | ### [Matisse](https://github.com/zhihu/Matisse)
166 |
167 | ### [Glide](https://github.com/bumptech/glide)
168 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # LoadingButton [](https://jitpack.io/#FlodCoding/LoadingButton)
2 |
3 | 一个小巧灵活的带加载功能的按钮控件,继承自[DrawableTextView](https://github.com/FlodCoding/DrawableTextView),加载动画来自于[CircularProgressDrawable](https://developer.android.google.cn/reference/android/support/v4/widget/CircularProgressDrawable?hl=en)
4 |
5 | ## 特性
6 | * 支持按钮收缩
7 | * 支持加载完成和失败图标显示
8 | * 可设置加载动画颜色、大小、位置
9 | * 自定义圆角
10 |
11 |
12 | ## 如何导入
13 |
14 | 根目录下的build.gradle
15 | ```
16 | allprojects {
17 |
18 | repositories {
19 | ...
20 | maven { url 'https://jitpack.io' }
21 |
22 | }
23 | }
24 | ```
25 |
26 | App目录下的build.gradle
27 | #### 注意!从1.1.0开始与以前的版本有较大的变动,请谨慎升级
28 | ```
29 | dependencies {
30 | //Androidx
31 | implementation 'com.github.FlodCoding:LoadingButton:1.1.0-alpha01'
32 |
33 | }
34 | ```
35 | Support-appcompat 停止更新
36 | ~~implementation 'com.github.FlodCoding:LoadingButton:1.0.5-support'~~
37 |
38 |
39 |
40 |
41 |
42 | ## Demo [点我下载](https://github.com/FlodCoding/LoadingButton/releases/download/1.1.0-alpha01/demo-1.1.0-alpha01.apk)
43 |
44 | 
45 |
46 | ### Demo截图
47 | 
48 | 
49 | 
50 | 
51 |
52 | ## 基本用法
53 |
54 | ### XML
55 | ```
56 |
76 | ```
77 | ### Code
78 | ```
79 |
80 | loadingBtn.start(); //开始加载
81 | loadingBtn.complete(true); //加载成功
82 | loadingBtn.complete(false); //加载失败
83 | loadingBtn.cancel(); //加载取消
84 |
85 | loadingBtn.setEnableShrink(true)
86 | .setEnableRestore(true)
87 | .setDisableClickOnLoading(true)
88 | .setShrinkDuration(450)
89 | .setLoadingPosition(DrawableTextView.POSITION.START)
90 | .setSuccessDrawable(R.drawable.ic_successful)
91 | .setFailDrawable(R.drawable.ic_fail)
92 | .setEndDrawableKeepDuration(900)
93 | .setLoadingEndDrawableSize((int) (loadingBtn.getTextSize() * 2));
94 |
95 | loadingBtn.getLoadingDrawable().setStrokeWidth(loadingBtn.getTextSize() * 0.14f);
96 | loadingBtn.getLoadingDrawable().setColorSchemeColors(loadingBtn.getTextColors().getDefaultColor());
97 | ```
98 |
99 | ### 状态回调
100 | start --> onShrinking --> onLoadingStart
101 | complete --> onLoadingStop --> onEndDrawableAppear --> onCompleted --> onRestored
102 |
103 | ```
104 | public static class OnStatusChangedListener {
105 |
106 | public void onShrinking() {}
107 |
108 | public void onLoadingStart() {}
109 |
110 | public void onLoadingStop() {}
111 |
112 | public void onEndDrawableAppear(boolean isSuccess, EndDrawable endDrawable) {}
113 |
114 | public void onRestoring() {}
115 |
116 | public void onRestored() {}
117 |
118 | public void onCompleted(boolean isSuccess) { }
119 |
120 | public void onCanceled() {}
121 | }
122 | ```
123 |
124 | ## 属性说明
125 | ### XML
126 | 属性名|类型|默认值|说明
127 | ---|:--:|:---:|---:
128 | enableShrink |boolean |true |开始加载时收缩
129 | disableClickOnLoading |boolean |true |加载时禁用点击
130 | enableRestore |boolean |false |完成时,恢复按钮
131 | radius |dimension |0dp |设置按钮的圆角,**(需要SDK>=21)**
(来自([DrawableTextView](https://github.com/FlodCoding/DrawableTextView))
132 | shrinkDuration |integer |450ms |收缩动画时间
133 | shrinkShape |enum
(Default,Oval) |Oval |收缩后的形状 **(需要SDK>=21)**
(Default:保持原来的形状,Oval:圆形)
134 | loadingEndDrawableSize |dimension |TextSize \*2 |设置LoadingDrawable和EndDrawable大小
135 | loadingDrawableColor |reference |TextColor |设置Loading的颜色
136 | loadingDrawablePosition |enum
(Start,Top,
End,Bottom) |Start |设置Loading的位置
137 | endSuccessDrawable |reference | null |完成时显示的图标
138 | endFailDrawable |reference | null |失败时显示的图标
139 | endDrawableAppearTime |integer | 300ms |完成或失败图标从无到有的时间
140 | endDrawableDuration |integer | 900ms |完成或失败图标停留的时间
141 |
142 | ### Public Func
143 | 方法名|参数说明|默认值|说明
144 | ---|:--:|:---:|---:
145 | start() |- |- |开始加载
146 | complete(boolean isSuccess) |是否成功 |- |完成加载
147 | cancel()
cancel(boolean withRestoreAnim) |是否执行恢复动画 |true |取消
148 | setEnableShrink(boolean enable) |- |true |设置加载时按钮收缩
149 | setEnableRestore(boolean enable) |- |false |设置完成时按钮恢复(形状和文字)
150 | setRadius(@Px int px)
setRadiusDP(int dp) |Px/Dp |0 |设置按钮的圆角
**(需要SDK>=21)**
(来自([DrawableTextView](https://github.com/FlodCoding/DrawableTextView))
151 | setShrinkShape(@ShrinkShape int shrinkShape) |Default:保持原来的形状,
Oval:圆形 |Oval |收缩后的形状
**(需要SDK>=21)**
152 | setShrinkDuration(long time) |milliseconds |450ms |收缩动画时间
153 | setLoadingEndDrawableSize(@Px int px) |单位Px |TextSize \*2 |设置LoadingDrawable和EndDrawable大小
154 | setLoadingPosition(@POSITION int position) |Start,Top,End,Bottom |Start |设置Loading的位置
155 | setSuccessDrawable(@DrawableRes int drawableRes)
setSuccessDrawable(Drawable drawable) |- | null |成功时显示的图标
156 | setFailDrawable(@DrawableRes int drawableRes)
setFailDrawable(Drawable drawable) |- | null |失败时显示的图标
157 | setEndDrawableAppearDuration(long time) |milliseconds | 300ms |完成或失败图标从无到有的时间
158 | setEndDrawableKeepDuration(long time) |milliseconds | 900ms |完成或失败图标停留的时间
159 | setOnStatusChangedListener
(OnStatusChangedListener listener)|-|null|按钮的各种状态回调
160 |
161 | ### 常见问题
162 | #### 完成加载时,如何自动恢复到之前按钮的状态?
163 | 设置setEnableRestore(true)
164 |
165 | #### 当setEnableRestore(false)时,又想某个时机恢复原来的按钮的状态,要怎么做?
166 | 执行cancel()
167 |
168 |
169 | ## Demo使用的第三方库
170 |
171 | ### [Matisse](https://github.com/zhihu/Matisse)
172 |
173 | ### [Glide](https://github.com/bumptech/glide)
174 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | applicationId "com.flod.loadingbutton.app"
7 | minSdkVersion 17
8 | targetSdkVersion 28
9 | versionCode 100
10 | versionName "1.0.0"
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 | }
19 |
20 | dependencies {
21 | implementation fileTree(dir: 'libs', include: ['*.jar'])
22 | implementation project(':Loadingbutton')
23 | implementation 'androidx.appcompat:appcompat:1.1.0'
24 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
25 |
26 | implementation 'com.zhihu.android:matisse:0.5.2-beta4'
27 | implementation 'com.github.bumptech.glide:glide:4.9.0'
28 | annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
29 | }
30 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/flod/loadingbutton/app/Glide4Engine.java:
--------------------------------------------------------------------------------
1 | package com.flod.loadingbutton.app;
2 |
3 | import android.content.Context;
4 | import android.graphics.drawable.Drawable;
5 | import android.net.Uri;
6 | import android.widget.ImageView;
7 |
8 | import com.bumptech.glide.Glide;
9 | import com.bumptech.glide.Priority;
10 | import com.bumptech.glide.request.RequestOptions;
11 | import com.zhihu.matisse.engine.ImageEngine;
12 |
13 | public class Glide4Engine implements ImageEngine {
14 | @Override
15 | public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) {
16 | Glide.with(context)
17 | .asBitmap() // some .jpeg files are actually gif
18 | .load(uri)
19 | .apply(new RequestOptions()
20 | .override(resize, resize)
21 | .placeholder(placeholder)
22 | .centerCrop())
23 | .into(imageView);
24 | }
25 |
26 | @Override
27 | public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView,
28 | Uri uri) {
29 | Glide.with(context)
30 | .asBitmap() // some .jpeg files are actually gif
31 | .load(uri)
32 | .apply(new RequestOptions()
33 | .override(resize, resize)
34 | .placeholder(placeholder)
35 | .centerCrop())
36 | .into(imageView);
37 | }
38 |
39 | @Override
40 | public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
41 | Glide.with(context)
42 | .load(uri)
43 | .apply(new RequestOptions()
44 | .override(resizeX, resizeY)
45 | .priority(Priority.HIGH)
46 | .fitCenter())
47 | .into(imageView);
48 | }
49 |
50 | @Override
51 | public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
52 | Glide.with(context)
53 | .asGif()
54 | .load(uri)
55 | .apply(new RequestOptions()
56 | .override(resizeX, resizeY)
57 | .priority(Priority.HIGH)
58 | .fitCenter())
59 | .into(imageView);
60 | }
61 |
62 | @Override
63 | public boolean supportAnimatedGif() {
64 | return true;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/flod/loadingbutton/app/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.flod.loadingbutton.app;
2 |
3 | import android.Manifest;
4 | import android.annotation.SuppressLint;
5 | import android.app.Activity;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.content.pm.ActivityInfo;
9 | import android.content.pm.PackageManager;
10 | import android.graphics.drawable.Drawable;
11 | import android.net.Uri;
12 | import android.os.Build;
13 | import android.os.Bundle;
14 | import android.text.Editable;
15 | import android.text.TextWatcher;
16 | import android.util.Log;
17 | import android.view.LayoutInflater;
18 | import android.view.Menu;
19 | import android.view.MenuItem;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.widget.Button;
23 | import android.widget.CompoundButton;
24 | import android.widget.EditText;
25 | import android.widget.ImageView;
26 | import android.widget.RadioGroup;
27 | import android.widget.SeekBar;
28 | import android.widget.Switch;
29 | import android.widget.TextView;
30 | import android.widget.Toast;
31 |
32 | import androidx.annotation.NonNull;
33 | import androidx.annotation.Nullable;
34 | import androidx.appcompat.app.AlertDialog;
35 | import androidx.appcompat.app.AppCompatActivity;
36 | import androidx.core.content.ContextCompat;
37 |
38 | import com.bumptech.glide.Glide;
39 | import com.bumptech.glide.load.DataSource;
40 | import com.bumptech.glide.load.engine.GlideException;
41 | import com.bumptech.glide.request.RequestListener;
42 | import com.bumptech.glide.request.target.Target;
43 | import com.flod.drawabletextview.DrawableTextView;
44 | import com.flod.loadingbutton.LoadingButton;
45 | import com.zhihu.matisse.Matisse;
46 | import com.zhihu.matisse.MimeType;
47 | import com.zhihu.matisse.internal.entity.CaptureStrategy;
48 |
49 | import java.util.Arrays;
50 | import java.util.List;
51 |
52 | @SuppressWarnings({"FieldCanBeLocal", "SameParameterValue"})
53 | @SuppressLint("SetTextI18n")
54 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
55 |
56 | private static final int RQ_MULTIPLE_PERMISSIONS = 200;
57 | private static final int RQ_GET_PHOTO_COMPLETE = 10;
58 | private static final int RQ_GET_PHOTO_FAIL = 11;
59 | private LoadingButton loadingBtn;
60 | private Button btCancel;
61 | private Button btFail;
62 | private Button btComplete;
63 |
64 | private TextView tvLoadingPosition;
65 | private ImageView imEndCompleteDrawableIcon;
66 | private ImageView imEndFailDrawableIcon;
67 |
68 | private TextView tvLoadingText;
69 | private TextView tvCompleteText;
70 | private TextView tvFailText;
71 |
72 |
73 | private int itemIndexSelected;
74 | private String editTextString;
75 |
76 |
77 | private String loadingText = "Loading";
78 | private String completeText = "Success";
79 | private String failText = "Fail";
80 |
81 |
82 | @Override
83 | protected void onCreate(Bundle savedInstanceState) {
84 | super.onCreate(savedInstanceState);
85 | setContentView(R.layout.activity_main);
86 | initView();
87 | }
88 |
89 | @Override
90 | public boolean onCreateOptionsMenu(Menu menu) {
91 | menu.add("Reset")
92 | .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
93 | @Override
94 | public boolean onMenuItemClick(MenuItem item) {
95 | initView();
96 | Toast.makeText(getApplicationContext(), "Reset", Toast.LENGTH_SHORT).show();
97 | return false;
98 | }
99 | })
100 | .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
101 | return super.onCreateOptionsMenu(menu);
102 |
103 | }
104 |
105 | private void initView() {
106 | loadingBtn = findViewById(R.id.loadingBtn);
107 |
108 | initLoadingButton();
109 |
110 | Switch swEnableShrink = findViewById(R.id.swEnableShrink);
111 | Switch swEnableRestore = findViewById(R.id.swEnableRestore);
112 | Switch swDisableClickOnLoading = findViewById(R.id.swDisableOnLoading);
113 | final TextView tvRadiusValue = findViewById(R.id.tvRadiusValue);
114 | SeekBar sbRadius = findViewById(R.id.sbRadius);
115 | RadioGroup rgShrinkShape = findViewById(R.id.rgShrinkShape);
116 | final TextView tvLoadingDrawableColorValue = findViewById(R.id.tvLoadingDrawableColorValue);
117 | SeekBar sbLoadingDrawableColor = findViewById(R.id.sbLoadingDrawableColor);
118 | final TextView tvLoadingStrokeWidthValue = findViewById(R.id.tvLoadingStrokeWidthValue);
119 | final TextView tvShrinkDurationValue = findViewById(R.id.tvShrinkDurationValue);
120 | SeekBar sbShrinkDuration = findViewById(R.id.sbShrinkDuration);
121 | SeekBar sbLoadingStrokeWidth = findViewById(R.id.sbLoadingStrokeWidth);
122 | tvLoadingStrokeWidthValue.setText(loadingBtn.getLoadingDrawable().getStrokeWidth() + "");
123 | final TextView tvLoadingEndDrawableSizeValue = findViewById(R.id.tvLoadingEndDrawableSizeValue);
124 | SeekBar sbLoadingEndDrawableSizeValue = findViewById(R.id.sbLoadingEndDrawableSizeValue);
125 | final TextView tvEndDrawableDurationValue = findViewById(R.id.tvEndDrawableDurationValue);
126 | SeekBar sbEndDrawableDuration = findViewById(R.id.sbEndDrawableDuration);
127 |
128 | swEnableShrink.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
129 | @Override
130 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
131 | loadingBtn.cancel();
132 | loadingBtn.setEnableShrink(isChecked);
133 | int defaultStrokeWidth = (int) (loadingBtn.getTextSize() * 0.14f);
134 | tvLoadingStrokeWidthValue.setText(defaultStrokeWidth + "");
135 | int loadingSize;
136 | if (isChecked) {
137 | loadingSize = (int) loadingBtn.getTextSize() * 2;
138 | loadingBtn.setLoadingEndDrawableSize(loadingSize);
139 | loadingBtn.getLoadingDrawable().setStrokeWidth(defaultStrokeWidth);
140 |
141 |
142 | } else {
143 | loadingSize = (int) loadingBtn.getTextSize();
144 | loadingBtn.setLoadingEndDrawableSize(loadingSize);
145 | loadingBtn.getLoadingDrawable().setStrokeWidth(loadingSize * 0.14f);
146 | }
147 | tvLoadingEndDrawableSizeValue.setText(loadingSize + "");
148 | }
149 | });
150 |
151 | swEnableRestore.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
152 | @Override
153 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
154 | loadingBtn.cancel();
155 | loadingBtn.setEnableRestore(isChecked);
156 | }
157 | });
158 |
159 | swDisableClickOnLoading.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
160 | @Override
161 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
162 | loadingBtn.setDisableClickOnLoading(isChecked);
163 | }
164 | });
165 |
166 |
167 | tvRadiusValue.setText(35 + "");
168 | sbRadius.setMax(100);
169 | sbRadius.setProgress(35);
170 | sbRadius.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
171 | @Override
172 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
173 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
174 | loadingBtn.setRadius(progress);
175 | }
176 | tvRadiusValue.setText(String.valueOf(progress));
177 | }
178 | });
179 |
180 |
181 | rgShrinkShape.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
182 | @Override
183 | public void onCheckedChanged(RadioGroup group, int checkedId) {
184 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
185 | if (checkedId == R.id.rdDefault) {
186 | loadingBtn.setShrinkShape(LoadingButton.ShrinkShape.DEFAULT);
187 |
188 | } else if (checkedId == R.id.rdOval) {
189 | loadingBtn.setShrinkShape(LoadingButton.ShrinkShape.OVAL);
190 | }
191 | }
192 | }
193 | });
194 |
195 |
196 | tvShrinkDurationValue.setText(String.valueOf(500));
197 | sbShrinkDuration.setMax(3000);
198 | sbShrinkDuration.setProgress(500);
199 | sbShrinkDuration.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
200 | @Override
201 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
202 | loadingBtn.setShrinkDuration(progress);
203 | tvShrinkDurationValue.setText(String.valueOf(progress));
204 | }
205 | });
206 |
207 |
208 | int loadingDrawableColorValue = loadingBtn.getLoadingDrawable().getColorSchemeColors()[0];
209 | tvLoadingDrawableColorValue.setText(Integer.toHexString(loadingDrawableColorValue));
210 | tvLoadingDrawableColorValue.setBackgroundColor(loadingDrawableColorValue);
211 | sbLoadingDrawableColor.setMax(0xffffff);
212 | sbLoadingDrawableColor.setProgress(loadingDrawableColorValue - 0xff000000);
213 | sbLoadingDrawableColor.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
214 | @Override
215 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
216 | loadingBtn.getLoadingDrawable().setColorSchemeColors(progress);
217 | tvLoadingDrawableColorValue.setText(Integer.toHexString(progress + 0xff000000));
218 | tvLoadingDrawableColorValue.setBackgroundColor(progress + 0xff000000);
219 | }
220 | });
221 |
222 |
223 | sbLoadingStrokeWidth.setMax(30);
224 | sbLoadingStrokeWidth.setProgress((int) loadingBtn.getLoadingDrawable().getStrokeWidth());
225 | sbLoadingStrokeWidth.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
226 | @Override
227 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
228 | loadingBtn.getLoadingDrawable().setStrokeWidth(progress);
229 | tvLoadingStrokeWidthValue.setText(String.valueOf(progress));
230 | }
231 | });
232 |
233 |
234 | tvLoadingEndDrawableSizeValue.setText(loadingBtn.getLoadingEndDrawableSize() + "");
235 | sbLoadingEndDrawableSizeValue.setMax(250);
236 | sbLoadingEndDrawableSizeValue.setProgress(loadingBtn.getLoadingEndDrawableSize());
237 | sbLoadingEndDrawableSizeValue.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
238 | @Override
239 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
240 | loadingBtn.setLoadingEndDrawableSize(progress);
241 | tvLoadingEndDrawableSizeValue.setText(String.valueOf(progress));
242 | }
243 | });
244 |
245 |
246 | tvEndDrawableDurationValue.setText(loadingBtn.getEndDrawableDuration() + "");
247 | sbEndDrawableDuration.setMax(6500);
248 | sbEndDrawableDuration.setProgress((int) loadingBtn.getEndDrawableDuration());
249 | sbEndDrawableDuration.setOnSeekBarChangeListener(new EmptyOnSeekBarChangeListener() {
250 | @Override
251 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
252 | loadingBtn.setEndDrawableKeepDuration(progress);
253 | tvEndDrawableDurationValue.setText(String.valueOf(progress));
254 | }
255 | });
256 |
257 |
258 | tvLoadingPosition = findViewById(R.id.tvLoadingPosition);
259 | imEndCompleteDrawableIcon = findViewById(R.id.imEndCompleteDrawableIcon);
260 | imEndFailDrawableIcon = findViewById(R.id.imEndFailDrawableIcon);
261 | tvLoadingText = findViewById(R.id.tvLoadingText);
262 | tvCompleteText = findViewById(R.id.tvCompleteText);
263 | tvFailText = findViewById(R.id.tvFailText);
264 | btCancel = findViewById(R.id.btCancel);
265 | btFail = findViewById(R.id.btFail);
266 | btComplete = findViewById(R.id.btComplete);
267 |
268 | tvLoadingPosition.setOnClickListener(this);
269 | findViewById(R.id.layEndCompleteDrawableIcon).setOnClickListener(this);
270 | findViewById(R.id.layEndFailDrawableIcon).setOnClickListener(this);
271 |
272 | tvLoadingText.setOnClickListener(this);
273 | tvCompleteText.setOnClickListener(this);
274 | tvFailText.setOnClickListener(this);
275 | btCancel.setOnClickListener(this);
276 | btFail.setOnClickListener(this);
277 | btComplete.setOnClickListener(this);
278 |
279 | tvLoadingText.setText("Loading");
280 | tvCompleteText.setText("Success");
281 | tvFailText.setText("Fail");
282 | imEndCompleteDrawableIcon.setImageResource(R.drawable.ic_successful);
283 | imEndFailDrawableIcon.setImageResource(R.drawable.ic_fail);
284 |
285 | }
286 |
287 |
288 | private void initLoadingButton() {
289 |
290 | loadingBtn.setOnClickListener(this);
291 | loadingBtn.cancel();
292 | loadingBtn.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
293 | loadingBtn.getLoadingDrawable().setStrokeWidth(loadingBtn.getTextSize() * 0.14f);
294 | loadingBtn.getLoadingDrawable().setColorSchemeColors(loadingBtn.getTextColors().getDefaultColor());
295 | loadingBtn.setEnableShrink(true)
296 | .setDisableClickOnLoading(true)
297 | .setShrinkDuration(450)
298 | .setLoadingPosition(DrawableTextView.POSITION.START)
299 | .setSuccessDrawable(R.drawable.ic_successful)
300 | .setFailDrawable(R.drawable.ic_fail)
301 | .setEndDrawableKeepDuration(900)
302 | .setEnableRestore(true)
303 | .setLoadingEndDrawableSize((int) (loadingBtn.getTextSize() * 2))
304 | .setOnStatusChangedListener(new LoadingButton.OnStatusChangedListener() {
305 |
306 | @Override
307 | public void onShrinking() {
308 | Log.d("LoadingButton", "onShrinking");
309 | }
310 |
311 | @Override
312 | public void onLoadingStart() {
313 | Log.d("LoadingButton", "onLoadingStart");
314 | loadingBtn.setText(loadingText);
315 | }
316 |
317 | @Override
318 | public void onLoadingStop() {
319 | Log.d("LoadingButton","onLoadingStop");
320 | }
321 |
322 | @Override
323 | public void onEndDrawableAppear(boolean isSuccess, LoadingButton.EndDrawable endDrawable) {
324 | Log.d("LoadingButton", "onEndDrawableAppear");
325 | if (isSuccess) {
326 | loadingBtn.setText(completeText);
327 | } else {
328 | loadingBtn.setText(failText);
329 | }
330 | }
331 |
332 |
333 | @Override
334 | public void onCompleted(boolean isSuccess) {
335 | Log.d("LoadingButton", "onCompleted isSuccess: " + isSuccess);
336 | Toast.makeText(getApplicationContext(), isSuccess ? "Success" : "Fail", Toast.LENGTH_SHORT).show();
337 |
338 | }
339 |
340 |
341 | @Override
342 | public void onRestored() {
343 | Log.d("LoadingButton", "onRestored");
344 |
345 | }
346 |
347 | @Override
348 | public void onCanceled() {
349 | Log.d("LoadingButton", "onCanceled");
350 | Toast.makeText(getApplicationContext(), "onCanceled", Toast.LENGTH_SHORT).show();
351 | }
352 | });
353 |
354 |
355 |
356 |
357 | }
358 |
359 |
360 | @SuppressLint("NonConstantResourceId")
361 | @Override
362 | public void onClick(final View v) {
363 | int id = v.getId();
364 | switch (id) {
365 | case R.id.loadingBtn:
366 | loadingBtn.start();
367 | return;
368 | case R.id.btCancel: {
369 | loadingBtn.cancel();
370 | return;
371 | }
372 | case R.id.btFail: {
373 | loadingBtn.complete(false);
374 | return;
375 | }
376 | case R.id.btComplete: {
377 | loadingBtn.complete(true);
378 | return;
379 | }
380 | }
381 |
382 |
383 | loadingBtn.cancel();
384 | switch (id) {
385 |
386 | case R.id.tvLoadingPosition: {
387 | final List items = Arrays.asList("START", "TOP", "END", "BOTTOM");
388 | final int curIndex = items.indexOf(tvLoadingPosition.getText().toString());
389 |
390 | showSelectDialog("LoadingPosition", new DialogInterface.OnClickListener() {
391 | @Override
392 | public void onClick(DialogInterface dialog, int which) {
393 | loadingBtn.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
394 | loadingBtn.setLoadingPosition(itemIndexSelected);
395 | tvLoadingPosition.setText(items.get(itemIndexSelected));
396 | itemIndexSelected = 0;
397 | }
398 | }, curIndex, items.toArray(new String[0]));
399 |
400 |
401 | break;
402 | }
403 | case R.id.layEndCompleteDrawableIcon: {
404 | if (!requestPermissions(this, Manifest.permission.READ_EXTERNAL_STORAGE,
405 | Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
406 | Matisse.from(MainActivity.this)
407 | .choose(MimeType.ofImage())
408 | .countable(false)
409 | .capture(true)
410 | .theme(R.style.Matisse_Dracula)
411 | .captureStrategy(new CaptureStrategy(true, "com.flod.hardloadingbutton.provider"))
412 | .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
413 | .thumbnailScale(0.5f)
414 | .imageEngine(new Glide4Engine())
415 | .forResult(RQ_GET_PHOTO_COMPLETE);
416 | }
417 | break;
418 | }
419 | case R.id.layEndFailDrawableIcon: {
420 | if (!requestPermissions(this, Manifest.permission.READ_EXTERNAL_STORAGE,
421 | Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
422 | Matisse.from(MainActivity.this)
423 | .choose(MimeType.ofImage())
424 | .theme(R.style.Matisse_Dracula)
425 | .countable(false)
426 | .capture(true)
427 | .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
428 | .thumbnailScale(0.5f)
429 | .imageEngine(new Glide4Engine())
430 | .forResult(RQ_GET_PHOTO_FAIL);
431 | }
432 | break;
433 | }
434 | case R.id.tvLoadingText: {
435 | showEditDialog("SetLoadingText", new DialogInterface.OnClickListener() {
436 | @Override
437 | public void onClick(DialogInterface dialog, int which) {
438 | tvLoadingText.setText(editTextString);
439 | loadingText = editTextString;
440 | editTextString = "";
441 | }
442 | });
443 | break;
444 | }
445 | case R.id.tvCompleteText: {
446 | showEditDialog("SetCompleteText", new DialogInterface.OnClickListener() {
447 | @Override
448 | public void onClick(DialogInterface dialog, int which) {
449 | tvCompleteText.setText(editTextString);
450 | completeText = editTextString;
451 | editTextString = "";
452 | }
453 | });
454 | break;
455 | }
456 | case R.id.tvFailText: {
457 | showEditDialog("SetFailText", new DialogInterface.OnClickListener() {
458 | @Override
459 | public void onClick(DialogInterface dialog, int which) {
460 | tvFailText.setText(editTextString);
461 | failText = editTextString;
462 | editTextString = "";
463 | }
464 | });
465 | break;
466 | }
467 | }
468 | }
469 |
470 | private void showSelectDialog(String title, DialogInterface.OnClickListener onConfirmClickListener,
471 | int checkIndex, String... items) {
472 | final AlertDialog.Builder builder = new AlertDialog.Builder(this);
473 | builder.setTitle(title)
474 | .setSingleChoiceItems(items, checkIndex, new DialogInterface.OnClickListener() {
475 | @Override
476 | public void onClick(DialogInterface dialog, int which) {
477 | itemIndexSelected = which;
478 | }
479 | })
480 | .setNegativeButton("Confirm", onConfirmClickListener);
481 | builder.create().show();
482 | }
483 |
484 | private void showEditDialog(String title, DialogInterface.OnClickListener onConfirmClickListener) {
485 | @SuppressLint("InflateParams") View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
486 | final AlertDialog.Builder builder = new AlertDialog.Builder(this);
487 | final EditText editText = view.findViewById(R.id.et);
488 | editText.addTextChangedListener(new TextWatcher() {
489 | @Override
490 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
491 |
492 | }
493 |
494 | @Override
495 | public void onTextChanged(CharSequence s, int start, int before, int count) {
496 |
497 | }
498 |
499 | @Override
500 | public void afterTextChanged(Editable s) {
501 | editTextString = s.toString();
502 | }
503 | });
504 |
505 | builder.setTitle(title)
506 | .setView(view)
507 | .setNegativeButton("Confirm", onConfirmClickListener);
508 | builder.create().show();
509 | }
510 |
511 |
512 | @Override
513 | protected void onActivityResult(final int requestCode, int resultCode, @Nullable Intent data) {
514 | super.onActivityResult(requestCode, resultCode, data);
515 | if (resultCode == RESULT_OK && data != null) {
516 | if (requestCode == RQ_GET_PHOTO_COMPLETE || requestCode == RQ_GET_PHOTO_FAIL) {
517 | final ImageView targetImageView = requestCode == RQ_GET_PHOTO_COMPLETE ? imEndCompleteDrawableIcon : imEndFailDrawableIcon;
518 | List mSelected = Matisse.obtainResult(data);
519 | Log.d("Matisse", "mSelected: " + mSelected);
520 | Glide.with(this)
521 | .asDrawable()
522 | .listener(new RequestListener() {
523 | @Override
524 | public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) {
525 | return false;
526 | }
527 |
528 | @Override
529 | public boolean onResourceReady(Drawable resource, Object model, Target 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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_fail.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_successful.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | -
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
36 |
37 |
43 |
44 |
50 |
51 |
57 |
58 |
64 |
65 |
66 |
67 |
74 |
75 |
82 |
83 |
84 |
91 |
92 |
98 |
99 |
103 |
104 |
111 |
112 |
121 |
122 |
123 |
128 |
129 |
130 |
131 |
135 |
136 |
141 |
142 |
149 |
150 |
156 |
157 |
162 |
163 |
169 |
170 |
171 |
172 |
177 |
178 |
182 |
183 |
191 |
192 |
193 |
198 |
199 |
200 |
201 |
202 |
222 |
223 |
224 |
230 |
231 |
235 |
236 |
242 |
243 |
250 |
251 |
252 |
257 |
258 |
259 |
260 |
281 |
282 |
286 |
287 |
292 |
293 |
302 |
303 |
304 |
305 |
311 |
312 |
316 |
317 |
325 |
326 |
327 |
332 |
333 |
334 |
335 |
341 |
342 |
346 |
347 |
355 |
356 |
357 |
362 |
363 |
364 |
365 |
366 |
371 |
372 |
377 |
378 |
386 |
387 |
388 |
393 |
394 |
399 |
400 |
408 |
409 |
410 |
416 |
417 |
421 |
422 |
430 |
431 |
432 |
437 |
438 |
439 |
440 |
444 |
445 |
450 |
451 |
460 |
461 |
462 |
463 |
467 |
468 |
473 |
474 |
483 |
484 |
485 |
489 |
490 |
495 |
496 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_edit_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_seek_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlodCoding/LoadingButton/30cf165eb72e7151730321ae9626510bf1efe692/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlodCoding/LoadingButton/30cf165eb72e7151730321ae9626510bf1efe692/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1E8AE8
4 | #176EB9
5 | #ffff4444
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | LoadingButton
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.4.1'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | maven { url 'https://jitpack.io' }
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
17 |
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FlodCoding/LoadingButton/30cf165eb72e7151730321ae9626510bf1efe692/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 09 18:50:17 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':Loadingbutton'
2 |
--------------------------------------------------------------------------------