├── .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 [![LoadingButton](https://jitpack.io/v/FlodCoding/LoadingButton.svg)](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 | ![下载.png](https://upload-images.jianshu.io/upload_images/7565394-f96443d70435d4b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300) 45 | 46 | ### Demo screenshot 47 | ![1.gif](https://upload-images.jianshu.io/upload_images/7565394-b799c91e14a8f19a.gif?imageMogr2/auto-orient/strip) 48 | ![2.gif](https://upload-images.jianshu.io/upload_images/7565394-018bbbd27694d3b5.gif?imageMogr2/auto-orient/strip) 49 | ![3.gif](https://upload-images.jianshu.io/upload_images/7565394-88becf790d21d7fc.gif?imageMogr2/auto-orient/strip) 50 | ![4.gif](https://upload-images.jianshu.io/upload_images/7565394-f2ad03c89d715afa.gif?imageMogr2/auto-orient/strip) 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 [![LoadingButton](https://jitpack.io/v/FlodCoding/LoadingButton.svg)](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 | ![下载.png](https://upload-images.jianshu.io/upload_images/7565394-f96443d70435d4b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300) 45 | 46 | ### Demo截图 47 | ![1.gif](https://upload-images.jianshu.io/upload_images/7565394-b799c91e14a8f19a.gif?imageMogr2/auto-orient/strip) 48 | ![2.gif](https://upload-images.jianshu.io/upload_images/7565394-018bbbd27694d3b5.gif?imageMogr2/auto-orient/strip) 49 | ![3.gif](https://upload-images.jianshu.io/upload_images/7565394-88becf790d21d7fc.gif?imageMogr2/auto-orient/strip) 50 | ![4.gif](https://upload-images.jianshu.io/upload_images/7565394-f2ad03c89d715afa.gif?imageMogr2/auto-orient/strip) 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 |