null.
524 | */
525 | public void setColors(@NonNull int[] colors) {
526 | mColors = colors;
527 | // if colors are reset, make sure to reset the color index as well
528 | setColorIndex(0);
529 | }
530 |
531 | /**
532 | * @param index Index into the color array of the color to display in
533 | * the progress spinner.
534 | */
535 | public void setColorIndex(int index) {
536 | mColorIndex = index;
537 | }
538 |
539 | /**
540 | * Proceed to the next available ring color. This will automatically
541 | * wrap back to the beginning of colors.
542 | */
543 | public void goToNextColor() {
544 | mColorIndex = (mColorIndex + 1) % (mColors.length);
545 | }
546 |
547 | public void setColorFilter(ColorFilter filter) {
548 | mPaint.setColorFilter(filter);
549 | invalidateSelf();
550 | }
551 |
552 | /**
553 | * @param alpha Set the alpha of the progress spinner and associated arrowhead.
554 | */
555 | public void setAlpha(int alpha) {
556 | mAlpha = alpha;
557 | }
558 |
559 | /**
560 | * @return Current alpha of the progress spinner and arrowhead.
561 | */
562 | public int getAlpha() {
563 | return mAlpha;
564 | }
565 |
566 | /**
567 | * @param strokeWidth Set the stroke width of the progress spinner in pixels.
568 | */
569 | public void setStrokeWidth(float strokeWidth) {
570 | mStrokeWidth = strokeWidth;
571 | mPaint.setStrokeWidth(strokeWidth);
572 | invalidateSelf();
573 | }
574 |
575 | @SuppressWarnings("unused")
576 | public float getStrokeWidth() {
577 | return mStrokeWidth;
578 | }
579 |
580 | @SuppressWarnings("unused")
581 | public void setStartTrim(float startTrim) {
582 | mStartTrim = startTrim;
583 | invalidateSelf();
584 | }
585 |
586 | @SuppressWarnings("unused")
587 | public float getStartTrim() {
588 | return mStartTrim;
589 | }
590 |
591 | public float getStartingStartTrim() {
592 | return mStartingStartTrim;
593 | }
594 |
595 | public float getStartingEndTrim() {
596 | return mStartingEndTrim;
597 | }
598 |
599 | @SuppressWarnings("unused")
600 | public void setEndTrim(float endTrim) {
601 | mEndTrim = endTrim;
602 | invalidateSelf();
603 | }
604 |
605 | @SuppressWarnings("unused")
606 | public float getEndTrim() {
607 | return mEndTrim;
608 | }
609 |
610 | @SuppressWarnings("unused")
611 | public void setRotation(float rotation) {
612 | mRotation = rotation;
613 | invalidateSelf();
614 | }
615 |
616 | @SuppressWarnings("unused")
617 | public float getRotation() {
618 | return mRotation;
619 | }
620 |
621 | public void setInsets(int width, int height) {
622 | final float minEdge = (float) Math.min(width, height);
623 | float insets;
624 | if (mRingCenterRadius <= 0 || minEdge < 0) {
625 | insets = (float) Math.ceil(mStrokeWidth / 2.0f);
626 | } else {
627 | insets = (float) (minEdge / 2.0f - mRingCenterRadius);
628 | }
629 | mStrokeInset = insets;
630 | }
631 |
632 | @SuppressWarnings("unused")
633 | public float getInsets() {
634 | return mStrokeInset;
635 | }
636 |
637 | /**
638 | * @param centerRadius Inner radius in px of the circle the progress
639 | * spinner arc traces.
640 | */
641 | public void setCenterRadius(double centerRadius) {
642 | mRingCenterRadius = centerRadius;
643 | }
644 |
645 | public double getCenterRadius() {
646 | return mRingCenterRadius;
647 | }
648 |
649 | /**
650 | * @param show Set to true to show the arrow head on the progress spinner.
651 | */
652 | public void setShowArrow(boolean show) {
653 | if (mShowArrow != show) {
654 | mShowArrow = show;
655 | invalidateSelf();
656 | }
657 | }
658 |
659 | /**
660 | * @param scale Set the scale of the arrowhead for the spinner.
661 | */
662 | public void setArrowScale(float scale) {
663 | if (scale != mArrowScale) {
664 | mArrowScale = scale;
665 | invalidateSelf();
666 | }
667 | }
668 |
669 | /**
670 | * @return The amount the progress spinner is currently rotated, between [0..1].
671 | */
672 | public float getStartingRotation() {
673 | return mStartingRotation;
674 | }
675 |
676 | /**
677 | * If the start / end trim are offset to begin with, store them so that
678 | * animation starts from that offset.
679 | */
680 | public void storeOriginals() {
681 | mStartingStartTrim = mStartTrim;
682 | mStartingEndTrim = mEndTrim;
683 | mStartingRotation = mRotation;
684 | }
685 |
686 | /**
687 | * Reset the progress spinner to default rotation, start and end angles.
688 | */
689 | public void resetOriginals() {
690 | mStartingStartTrim = 0;
691 | mStartingEndTrim = 0;
692 | mStartingRotation = 0;
693 | setStartTrim(0);
694 | setEndTrim(0);
695 | setRotation(0);
696 | }
697 |
698 | private void invalidateSelf() {
699 | mCallback.invalidateDrawable(null);
700 | }
701 | }
702 |
703 | /**
704 | * Squishes the interpolation curve into the second half of the animation.
705 | */
706 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
707 | @Override
708 | public float getInterpolation(float input) {
709 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
710 | }
711 | }
712 |
713 | /**
714 | * Squishes the interpolation curve into the first half of the animation.
715 | */
716 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
717 | @Override
718 | public float getInterpolation(float input) {
719 | return super.getInterpolation(Math.min(1, input * 2.0f));
720 | }
721 | }
722 | }
723 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/rxjava_retrofit_mvp_md/refresh/RefreshLayout.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.rxjava_retrofit_mvp_md.refresh;
18 |
19 |
20 | import android.content.Context;
21 | import android.content.res.Resources;
22 | import android.content.res.TypedArray;
23 | import android.support.v4.view.MotionEventCompat;
24 | import android.support.v4.view.ViewCompat;
25 | import android.util.AttributeSet;
26 | import android.util.DisplayMetrics;
27 | import android.view.MotionEvent;
28 | import android.view.View;
29 | import android.view.ViewConfiguration;
30 | import android.view.ViewGroup;
31 | import android.view.animation.Animation;
32 | import android.view.animation.Animation.AnimationListener;
33 | import android.view.animation.DecelerateInterpolator;
34 | import android.view.animation.Transformation;
35 | import android.widget.AbsListView;
36 |
37 | import com.example.rxjava_retrofit_mvp_md.R;
38 |
39 |
40 | /**
41 | * The SwipeRefreshLayout should be used whenever the user can refresh the
42 | * contents of a view via a vertical swipe gesture. The activity that
43 | * instantiates this view should add an OnRefreshListener to be notified
44 | * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout
45 | * will notify the listener each and every time the gesture is completed again;
46 | * the listener is responsible for correctly determining when to actually
47 | * initiate a refresh of its content. If the listener determines there should
48 | * not be a refresh, it must call setRefreshing(false) to cancel any visual
49 | * indication of a refresh. If an activity wishes to show just the progress
50 | * animation, it should call setRefreshing(true). To disable the gesture and
51 | * progress animation, call setEnabled(false) on the view.
52 | * 53 | * This layout should be made the parent of the view that will be refreshed as a 54 | * result of the gesture and can only support one direct child. This view will 55 | * also be made the target of the gesture and will be forced to match both the 56 | * width and the height supplied in this layout. The SwipeRefreshLayout does not 57 | * provide accessibility events; instead, a menu item must be provided to allow 58 | * refresh of the content wherever this gesture is used. 59 | *
60 | */ 61 | public class RefreshLayout extends ViewGroup { 62 | // Maps to ProgressBar.Large style 63 | public static final int LARGE = MaterialProgressDrawable.LARGE; 64 | // Maps to ProgressBar default style 65 | public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; 66 | 67 | private static final String LOG_TAG = RefreshLayout.class.getSimpleName(); 68 | 69 | private static final int MAX_ALPHA = 255; 70 | private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); 71 | 72 | private static final int CIRCLE_DIAMETER = 40; 73 | private static final int CIRCLE_DIAMETER_LARGE = 56; 74 | 75 | private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; 76 | private static final int INVALID_POINTER = -1; 77 | private static final float DRAG_RATE = .5f; 78 | 79 | // Max amount of circle that can be filled by progress during swipe gesture, 80 | // where 1.0 is a full circle 81 | private static final float MAX_PROGRESS_ANGLE = .8f; 82 | 83 | private static final int SCALE_DOWN_DURATION = 150; 84 | 85 | private static final int ALPHA_ANIMATION_DURATION = 300; 86 | 87 | private static final int ANIMATE_TO_TRIGGER_DURATION = 200; 88 | 89 | private static final int ANIMATE_TO_START_DURATION = 200; 90 | 91 | // Default background for the progress spinner 92 | private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; 93 | // Default offset in dips from the top of the view to where the progress spinner should stop 94 | private static final int DEFAULT_CIRCLE_TARGET = 64; 95 | 96 | private View mTarget; // the target of the gesture 97 | private SwipeRefreshLayoutDirection mDirection; 98 | private boolean mBothDirection; 99 | private OnRefreshListener mListener; 100 | private boolean mRefreshing = false; 101 | private int mTouchSlop; 102 | private float mTotalDragDistance = -1; 103 | private int mMediumAnimationDuration; 104 | private int mCurrentTargetOffsetTop; 105 | // Whether or not the starting offset has been determined. 106 | private boolean mOriginalOffsetCalculated = false; 107 | 108 | private float mInitialMotionY; 109 | private boolean mIsBeingDragged; 110 | private int mActivePointerId = INVALID_POINTER; 111 | // Whether this item is scaled up rather than clipped 112 | private boolean mScale; 113 | 114 | // Target is returning to its start offset because it was cancelled or a 115 | // refresh was triggered. 116 | private boolean mReturningToStart; 117 | private final DecelerateInterpolator mDecelerateInterpolator; 118 | private static final int[] LAYOUT_ATTRS = new int[]{ 119 | android.R.attr.enabled 120 | }; 121 | 122 | private CircleImageView mCircleView; 123 | private int mCircleViewIndex = -1; 124 | 125 | protected int mFrom; 126 | 127 | private float mStartingScale; 128 | 129 | protected int mOriginalOffsetTop; 130 | 131 | private MaterialProgressDrawable mProgress; 132 | 133 | private Animation mScaleAnimation; 134 | 135 | private Animation mScaleDownAnimation; 136 | 137 | private Animation mAlphaStartAnimation; 138 | 139 | private Animation mAlphaMaxAnimation; 140 | 141 | private Animation mScaleDownToStartAnimation; 142 | 143 | private float mSpinnerFinalOffset; 144 | 145 | private boolean mNotify; 146 | 147 | private int mCircleWidth; 148 | 149 | private int mCircleHeight; 150 | 151 | // Whether the client has set a custom starting position; 152 | private boolean mUsingCustomStart; 153 | 154 | private AnimationListener mRefreshListener = new AnimationListener() { 155 | @Override 156 | public void onAnimationStart(Animation animation) { 157 | } 158 | 159 | @Override 160 | public void onAnimationRepeat(Animation animation) { 161 | } 162 | 163 | @Override 164 | public void onAnimationEnd(Animation animation) { 165 | if (mRefreshing) { 166 | // Make sure the progress view is fully visible 167 | mProgress.setAlpha(MAX_ALPHA); 168 | mProgress.start(); 169 | if (mNotify) { 170 | if (mListener != null) { 171 | mListener.onRefresh(mDirection); 172 | } 173 | } 174 | } else { 175 | mProgress.stop(); 176 | mCircleView.setVisibility(View.GONE); 177 | setColorViewAlpha(MAX_ALPHA); 178 | // Return the circle to its start position 179 | if (mScale) { 180 | setAnimationProgress(0 /* animation complete and view is hidden */); 181 | } else { 182 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, 183 | true /* requires update */); 184 | } 185 | } 186 | mCurrentTargetOffsetTop = mCircleView.getTop(); 187 | } 188 | }; 189 | 190 | private void setColorViewAlpha(int targetAlpha) { 191 | mCircleView.getBackground().setAlpha(targetAlpha); 192 | mProgress.setAlpha(targetAlpha); 193 | } 194 | 195 | /** 196 | * The refresh indicator starting and resting position is always positioned 197 | * near the top of the refreshing content. This position is a consistent 198 | * location, but can be adjusted in either direction based on whether or not 199 | * there is a toolbar or actionbar present. 200 | * 201 | * @param scale Set to true if there is no view at a higher z-order than 202 | * where the progress spinner is set to appear. 203 | * @param start The offset in pixels from the top of this view at which the 204 | * progress spinner should appear. 205 | * @param end The offset in pixels from the top of this view at which the 206 | * progress spinner should come to rest after a successful swipe 207 | * gesture. 208 | */ 209 | /* 210 | public void setProgressViewOffset(boolean scale, int start, int end) { 211 | mScale = scale; 212 | mCircleView.setVisibility(View.GONE); 213 | mOriginalOffsetTop = mCurrentTargetOffsetTop = start; 214 | mSpinnerFinalOffset = end; 215 | mUsingCustomStart = true; 216 | mCircleView.invalidate(); 217 | }*/ 218 | 219 | /** 220 | * The refresh indicator resting position is always positioned near the top 221 | * of the refreshing content. This position is a consistent location, but 222 | * can be adjusted in either direction based on whether or not there is a 223 | * toolbar or actionbar present. 224 | * 225 | * @param scale Set to true if there is no view at a higher z-order than 226 | * where the progress spinner is set to appear. 227 | * @param end The offset in pixels from the top of this view at which the 228 | * progress spinner should come to rest after a successful swipe 229 | * gesture. 230 | */ 231 | /* 232 | public void setProgressViewEndTarget(boolean scale, int end) { 233 | mSpinnerFinalOffset = end; 234 | mScale = scale; 235 | mCircleView.invalidate(); 236 | }*/ 237 | 238 | /** 239 | * One of DEFAULT, or LARGE. 240 | */ 241 | public void setSize(int size) { 242 | if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { 243 | return; 244 | } 245 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 246 | if (size == MaterialProgressDrawable.LARGE) { 247 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); 248 | } else { 249 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 250 | } 251 | // force the bounds of the progress circle inside the circle view to 252 | // update by setting it to null before updating its size and then 253 | // re-setting it 254 | mCircleView.setImageDrawable(null); 255 | mProgress.updateSizes(size); 256 | mCircleView.setImageDrawable(mProgress); 257 | } 258 | 259 | /** 260 | * Simple constructor to use when creating a SwipeRefreshLayout from code. 261 | * 262 | * @param context 263 | */ 264 | public RefreshLayout(Context context) { 265 | this(context, null); 266 | } 267 | 268 | /** 269 | * Constructor that is called when inflating SwipeRefreshLayout from XML. 270 | * 271 | * @param context 272 | * @param attrs 273 | */ 274 | public RefreshLayout(Context context, AttributeSet attrs) { 275 | super(context, attrs); 276 | 277 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 278 | 279 | mMediumAnimationDuration = getResources().getInteger( 280 | android.R.integer.config_mediumAnimTime); 281 | 282 | setWillNotDraw(false); 283 | mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); 284 | 285 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 286 | setEnabled(a.getBoolean(0, true)); 287 | a.recycle(); 288 | 289 | final TypedArray a2 = context.obtainStyledAttributes(attrs, R.styleable.SwipyRefreshLayout); 290 | SwipeRefreshLayoutDirection direction 291 | = SwipeRefreshLayoutDirection.getFromInt(a2.getInt(R.styleable.SwipyRefreshLayout_direction, 0)); 292 | if (direction != SwipeRefreshLayoutDirection.BOTH) { 293 | mDirection = direction; 294 | mBothDirection = false; 295 | } else { 296 | mDirection = SwipeRefreshLayoutDirection.TOP; 297 | mBothDirection = true; 298 | } 299 | a2.recycle(); 300 | 301 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 302 | mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 303 | mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); 304 | 305 | createProgressView(); 306 | ViewCompat.setChildrenDrawingOrderEnabled(this, true); 307 | // the absolute offset has to take into account that the circle starts at an offset 308 | mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; 309 | mTotalDragDistance = mSpinnerFinalOffset; 310 | } 311 | 312 | protected int getChildDrawingOrder(int childCount, int i) { 313 | if (mCircleViewIndex < 0) { 314 | return i; 315 | } else if (i == childCount - 1) { 316 | // Draw the selected child last 317 | return mCircleViewIndex; 318 | } else if (i >= mCircleViewIndex) { 319 | // Move the children after the selected child earlier one 320 | return i + 1; 321 | } else { 322 | // Keep the children before the selected child the same 323 | return i; 324 | } 325 | } 326 | 327 | private void createProgressView() { 328 | mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER / 2); 329 | mProgress = new MaterialProgressDrawable(getContext(), this); 330 | mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); 331 | mCircleView.setImageDrawable(mProgress); 332 | mCircleView.setVisibility(View.GONE); 333 | addView(mCircleView); 334 | } 335 | 336 | /** 337 | * Set the listener to be notified when a refresh is triggered via the swipe 338 | * gesture. 339 | */ 340 | public void setOnRefreshListener(OnRefreshListener listener) { 341 | mListener = listener; 342 | } 343 | 344 | /** 345 | * Pre API 11, alpha is used to make the progress circle appear instead of scale. 346 | */ 347 | private boolean isAlphaUsedForScale() { 348 | return android.os.Build.VERSION.SDK_INT < 11; 349 | } 350 | 351 | /** 352 | * Notify the widget that refresh state has changed. Do not call this when 353 | * refresh is triggered by a swipe gesture. 354 | * 355 | * @param refreshing Whether or not the view should show refresh progress. 356 | */ 357 | public void setRefreshing(boolean refreshing) { 358 | if (refreshing && mRefreshing != refreshing) { 359 | // scale and show 360 | mRefreshing = refreshing; 361 | int endTarget = 0; 362 | if (!mUsingCustomStart) { 363 | endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); 364 | } else { 365 | endTarget = (int) mSpinnerFinalOffset; 366 | } 367 | setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, 368 | true /* requires update */); 369 | mNotify = false; 370 | startScaleUpAnimation(mRefreshListener); 371 | } else { 372 | setRefreshing(refreshing, false /* notify */); 373 | } 374 | } 375 | 376 | private void startScaleUpAnimation(AnimationListener listener) { 377 | mCircleView.setVisibility(View.VISIBLE); 378 | if (android.os.Build.VERSION.SDK_INT >= 11) { 379 | // Pre API 11, alpha is used in place of scale up to show the 380 | // progress circle appearing. 381 | // Don't adjust the alpha during appearance otherwise. 382 | mProgress.setAlpha(MAX_ALPHA); 383 | } 384 | mScaleAnimation = new Animation() { 385 | @Override 386 | public void applyTransformation(float interpolatedTime, Transformation t) { 387 | setAnimationProgress(interpolatedTime); 388 | } 389 | }; 390 | mScaleAnimation.setDuration(mMediumAnimationDuration); 391 | if (listener != null) { 392 | mCircleView.setAnimationListener(listener); 393 | } 394 | mCircleView.clearAnimation(); 395 | mCircleView.startAnimation(mScaleAnimation); 396 | } 397 | 398 | /** 399 | * Pre API 11, this does an alpha animation. 400 | * 401 | * @param progress 402 | */ 403 | private void setAnimationProgress(float progress) { 404 | if (isAlphaUsedForScale()) { 405 | setColorViewAlpha((int) (progress * MAX_ALPHA)); 406 | } else { 407 | ViewCompat.setScaleX(mCircleView, progress); 408 | ViewCompat.setScaleY(mCircleView, progress); 409 | } 410 | } 411 | 412 | private void setRefreshing(boolean refreshing, final boolean notify) { 413 | if (mRefreshing != refreshing) { 414 | mNotify = notify; 415 | ensureTarget(); 416 | mRefreshing = refreshing; 417 | if (mRefreshing) { 418 | animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); 419 | } else { 420 | startScaleDownAnimation(mRefreshListener); 421 | } 422 | } 423 | } 424 | 425 | private void startScaleDownAnimation(AnimationListener listener) { 426 | mScaleDownAnimation = new Animation() { 427 | @Override 428 | public void applyTransformation(float interpolatedTime, Transformation t) { 429 | setAnimationProgress(1 - interpolatedTime); 430 | } 431 | }; 432 | mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); 433 | mCircleView.setAnimationListener(listener); 434 | mCircleView.clearAnimation(); 435 | mCircleView.startAnimation(mScaleDownAnimation); 436 | } 437 | 438 | private void startProgressAlphaStartAnimation() { 439 | mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); 440 | } 441 | 442 | private void startProgressAlphaMaxAnimation() { 443 | mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); 444 | } 445 | 446 | private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { 447 | // Pre API 11, alpha is used in place of scale. Don't also use it to 448 | // show the trigger point. 449 | if (mScale && isAlphaUsedForScale()) { 450 | return null; 451 | } 452 | Animation alpha = new Animation() { 453 | @Override 454 | public void applyTransformation(float interpolatedTime, Transformation t) { 455 | mProgress 456 | .setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) 457 | * interpolatedTime))); 458 | } 459 | }; 460 | alpha.setDuration(ALPHA_ANIMATION_DURATION); 461 | // Clear out the previous animation listeners. 462 | mCircleView.setAnimationListener(null); 463 | mCircleView.clearAnimation(); 464 | mCircleView.startAnimation(alpha); 465 | return alpha; 466 | } 467 | 468 | /** 469 | * Set the background color of the progress spinner disc. 470 | * 471 | * @param colorRes Resource id of the color. 472 | */ 473 | public void setProgressBackgroundColor(int colorRes) { 474 | mCircleView.setBackgroundColor(colorRes); 475 | mProgress.setBackgroundColor(getResources().getColor(colorRes)); 476 | } 477 | 478 | /** 479 | * @deprecated Use {@link #setColorSchemeResources(int...)} 480 | */ 481 | @Deprecated 482 | public void setColorScheme(int... colors) { 483 | setColorSchemeResources(colors); 484 | } 485 | 486 | /** 487 | * Set the color resources used in the progress animation from color resources. 488 | * The first color will also be the color of the bar that grows in response 489 | * to a user swipe gesture. 490 | * 491 | * @param colorResIds 492 | */ 493 | public void setColorSchemeResources(int... colorResIds) { 494 | final Resources res = getResources(); 495 | int[] colorRes = new int[colorResIds.length]; 496 | for (int i = 0; i < colorResIds.length; i++) { 497 | colorRes[i] = res.getColor(colorResIds[i]); 498 | } 499 | setColorSchemeColors(colorRes); 500 | } 501 | 502 | /** 503 | * Set the colors used in the progress animation. The first 504 | * color will also be the color of the bar that grows in response to a user 505 | * swipe gesture. 506 | * 507 | * @param colors 508 | */ 509 | public void setColorSchemeColors(int... colors) { 510 | ensureTarget(); 511 | mProgress.setColorSchemeColors(colors); 512 | } 513 | 514 | /** 515 | * @return Whether the SwipeRefreshWidget is actively showing refresh 516 | * progress. 517 | */ 518 | public boolean isRefreshing() { 519 | return mRefreshing; 520 | } 521 | 522 | private void ensureTarget() { 523 | // Don't bother getting the parent height if the parent hasn't been laid 524 | // out yet. 525 | if (mTarget == null) { 526 | for (int i = 0; i < getChildCount(); i++) { 527 | View child = getChildAt(i); 528 | if (!child.equals(mCircleView)) { 529 | mTarget = child; 530 | break; 531 | } 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * Set the distance to trigger a sync in dips 538 | * 539 | * @param distance 540 | */ 541 | public void setDistanceToTriggerSync(int distance) { 542 | mTotalDragDistance = distance; 543 | } 544 | 545 | @Override 546 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 547 | final int width = getMeasuredWidth(); 548 | final int height = getMeasuredHeight(); 549 | if (getChildCount() == 0) { 550 | return; 551 | } 552 | if (mTarget == null) { 553 | ensureTarget(); 554 | } 555 | if (mTarget == null) { 556 | return; 557 | } 558 | final View child = mTarget; 559 | final int childLeft = getPaddingLeft(); 560 | final int childTop = getPaddingTop(); 561 | final int childWidth = width - getPaddingLeft() - getPaddingRight(); 562 | final int childHeight = height - getPaddingTop() - getPaddingBottom(); 563 | child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 564 | int circleWidth = mCircleView.getMeasuredWidth(); 565 | int circleHeight = mCircleView.getMeasuredHeight(); 566 | mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, 567 | (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); 568 | } 569 | 570 | @Override 571 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 572 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 573 | if (mTarget == null) { 574 | ensureTarget(); 575 | } 576 | if (mTarget == null) { 577 | return; 578 | } 579 | mTarget.measure(MeasureSpec.makeMeasureSpec( 580 | getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), 581 | MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( 582 | getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); 583 | mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), 584 | MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); 585 | if (!mUsingCustomStart && !mOriginalOffsetCalculated) { 586 | mOriginalOffsetCalculated = true; 587 | 588 | switch (mDirection) { 589 | case BOTTOM: 590 | mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight() - mCircleView.getMeasuredHeight(); 591 | break; 592 | case TOP: 593 | default: 594 | mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 595 | break; 596 | } 597 | } 598 | mCircleViewIndex = -1; 599 | // Get the index of the circleview. 600 | for (int index = 0; index < getChildCount(); index++) { 601 | if (getChildAt(index) == mCircleView) { 602 | mCircleViewIndex = index; 603 | break; 604 | } 605 | } 606 | } 607 | 608 | /** 609 | * @return Whether it is possible for the child view of this layout to 610 | * scroll up. Override this if the child view is a custom view. 611 | */ 612 | public boolean canChildScrollUp() { 613 | if (android.os.Build.VERSION.SDK_INT < 14) { 614 | if (mTarget instanceof AbsListView) { 615 | final AbsListView absListView = (AbsListView) mTarget; 616 | return absListView.getChildCount() > 0 617 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 618 | .getTop() < absListView.getPaddingTop()); 619 | } else { 620 | return mTarget.getScrollY() > 0; 621 | } 622 | } else { 623 | return ViewCompat.canScrollVertically(mTarget, -1); 624 | } 625 | } 626 | // public boolean canChildScrollUp() { 627 | // if (android.os.Build.VERSION.SDK_INT < 14) { 628 | // if (mTarget instanceof AbsListView) { 629 | // final AbsListView absListView = (AbsListView) mTarget; 630 | // if (absListView.getLastVisiblePosition() + 1 == absListView.getCount()) { 631 | // int lastIndex = absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition(); 632 | // 633 | // boolean res = absListView.getChildAt(lastIndex).getBottom() == absListView.getPaddingBottom(); 634 | // 635 | // return res; 636 | // } 637 | // return true; 638 | // } else { 639 | // return mTarget.getScrollY() > 0; 640 | // } 641 | // } else { 642 | // return ViewCompat.canScrollVertically(mTarget, 1); 643 | // } 644 | // } 645 | 646 | 647 | public boolean canChildScrollDown() { 648 | if (android.os.Build.VERSION.SDK_INT < 14) { 649 | if (mTarget instanceof AbsListView) { 650 | final AbsListView absListView = (AbsListView) mTarget; 651 | try { 652 | if (absListView.getCount() > 0) { 653 | if (absListView.getLastVisiblePosition() + 1 == absListView.getCount()) { 654 | int lastIndex = absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition(); 655 | return absListView.getChildAt(lastIndex).getBottom() == absListView.getPaddingBottom(); 656 | } 657 | } 658 | } catch (Exception e) { 659 | e.printStackTrace(); 660 | } 661 | return true; 662 | } else { 663 | return true; 664 | } 665 | } else { 666 | return ViewCompat.canScrollVertically(mTarget, 1); 667 | } 668 | } 669 | 670 | @Override 671 | public boolean onInterceptTouchEvent(MotionEvent ev) { 672 | ensureTarget(); 673 | 674 | final int action = MotionEventCompat.getActionMasked(ev); 675 | 676 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 677 | mReturningToStart = false; 678 | } 679 | 680 | switch (mDirection) { 681 | case BOTTOM: 682 | if (!isEnabled() || mReturningToStart || (!mBothDirection && canChildScrollDown()) || mRefreshing) { 683 | // Fail fast if we're not in a state where a swipe is possible 684 | return false; 685 | } 686 | break; 687 | case TOP: 688 | default: 689 | if (!isEnabled() || mReturningToStart || (!mBothDirection && canChildScrollUp()) || mRefreshing) { 690 | // Fail fast if we're not in a state where a swipe is possible 691 | return false; 692 | } 693 | break; 694 | } 695 | 696 | switch (action) { 697 | case MotionEvent.ACTION_DOWN: 698 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); 699 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 700 | mIsBeingDragged = false; 701 | final float initialMotionY = getMotionEventY(ev, mActivePointerId); 702 | if (initialMotionY == -1) { 703 | return false; 704 | } 705 | mInitialMotionY = initialMotionY; 706 | 707 | case MotionEvent.ACTION_MOVE: 708 | if (mActivePointerId == INVALID_POINTER) { 709 | return false; 710 | } 711 | 712 | final float y = getMotionEventY(ev, mActivePointerId); 713 | if (y == -1) { 714 | return false; 715 | } 716 | if (mBothDirection) { 717 | if (y > mInitialMotionY) { 718 | setRawDirection(SwipeRefreshLayoutDirection.TOP); 719 | } else if (y < mInitialMotionY) { 720 | setRawDirection(SwipeRefreshLayoutDirection.BOTTOM); 721 | } 722 | if ((mDirection == SwipeRefreshLayoutDirection.BOTTOM && canChildScrollDown()) 723 | || (mDirection == SwipeRefreshLayoutDirection.TOP && canChildScrollUp())) { 724 | return false; 725 | } 726 | } 727 | float yDiff; 728 | switch (mDirection) { 729 | case BOTTOM: 730 | yDiff = mInitialMotionY - y; 731 | break; 732 | case TOP: 733 | default: 734 | yDiff = y - mInitialMotionY; 735 | break; 736 | } 737 | if (yDiff > mTouchSlop && !mIsBeingDragged) { 738 | mIsBeingDragged = true; 739 | mProgress.setAlpha(STARTING_PROGRESS_ALPHA); 740 | } 741 | break; 742 | 743 | case MotionEventCompat.ACTION_POINTER_UP: 744 | onSecondaryPointerUp(ev); 745 | break; 746 | 747 | case MotionEvent.ACTION_UP: 748 | case MotionEvent.ACTION_CANCEL: 749 | mIsBeingDragged = false; 750 | mActivePointerId = INVALID_POINTER; 751 | break; 752 | } 753 | 754 | return mIsBeingDragged; 755 | } 756 | 757 | private float getMotionEventY(MotionEvent ev, int activePointerId) { 758 | final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); 759 | if (index < 0) { 760 | return -1; 761 | } 762 | return MotionEventCompat.getY(ev, index); 763 | } 764 | 765 | @Override 766 | public void requestDisallowInterceptTouchEvent(boolean b) { 767 | // Nope. 768 | } 769 | 770 | private boolean isAnimationRunning(Animation animation) { 771 | return animation != null && animation.hasStarted() && !animation.hasEnded(); 772 | } 773 | 774 | @Override 775 | public boolean onTouchEvent(MotionEvent ev) { 776 | final int action = MotionEventCompat.getActionMasked(ev); 777 | 778 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 779 | mReturningToStart = false; 780 | } 781 | 782 | switch (mDirection) { 783 | case BOTTOM: 784 | if (!isEnabled() || mReturningToStart || canChildScrollDown() || mRefreshing) { 785 | // Fail fast if we're not in a state where a swipe is possible 786 | return false; 787 | } 788 | break; 789 | case TOP: 790 | default: 791 | if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { 792 | // Fail fast if we're not in a state where a swipe is possible 793 | return false; 794 | } 795 | break; 796 | } 797 | 798 | switch (action) { 799 | case MotionEvent.ACTION_DOWN: 800 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 801 | mIsBeingDragged = false; 802 | break; 803 | 804 | case MotionEvent.ACTION_MOVE: { 805 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 806 | if (pointerIndex < 0) { 807 | return false; 808 | } 809 | 810 | final float y = MotionEventCompat.getY(ev, pointerIndex); 811 | 812 | float overscrollTop; 813 | switch (mDirection) { 814 | case BOTTOM: 815 | overscrollTop = (mInitialMotionY - y) * DRAG_RATE; 816 | break; 817 | case TOP: 818 | default: 819 | overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 820 | break; 821 | } 822 | if (mIsBeingDragged) { 823 | mProgress.showArrow(true); 824 | float originalDragPercent = overscrollTop / mTotalDragDistance; 825 | if (originalDragPercent < 0) { 826 | return false; 827 | } 828 | float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); 829 | float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; 830 | float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; 831 | float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset 832 | - mOriginalOffsetTop : mSpinnerFinalOffset; 833 | float tensionSlingshotPercent = Math.max(0, 834 | Math.min(extraOS, slingshotDist * 2) / slingshotDist); 835 | float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( 836 | (tensionSlingshotPercent / 4), 2)) * 2f; 837 | float extraMove = (slingshotDist) * tensionPercent * 2; 838 | 839 | // int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); 840 | int targetY; 841 | if (mDirection == SwipeRefreshLayoutDirection.TOP) { 842 | targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); 843 | } else { 844 | targetY = mOriginalOffsetTop - (int) ((slingshotDist * dragPercent) + extraMove); 845 | } 846 | // where 1.0f is a full circle 847 | if (mCircleView.getVisibility() != View.VISIBLE) { 848 | mCircleView.setVisibility(View.VISIBLE); 849 | } 850 | if (!mScale) { 851 | ViewCompat.setScaleX(mCircleView, 1f); 852 | ViewCompat.setScaleY(mCircleView, 1f); 853 | } 854 | if (overscrollTop < mTotalDragDistance) { 855 | if (mScale) { 856 | setAnimationProgress(overscrollTop / mTotalDragDistance); 857 | } 858 | if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA 859 | && !isAnimationRunning(mAlphaStartAnimation)) { 860 | // Animate the alpha 861 | startProgressAlphaStartAnimation(); 862 | } 863 | float strokeStart = (float) (adjustedPercent * .8f); 864 | mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); 865 | mProgress.setArrowScale(Math.min(1f, adjustedPercent)); 866 | } else { 867 | if (mProgress.getAlpha() < MAX_ALPHA 868 | && !isAnimationRunning(mAlphaMaxAnimation)) { 869 | // Animate the alpha 870 | startProgressAlphaMaxAnimation(); 871 | } 872 | } 873 | float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; 874 | mProgress.setProgressRotation(rotation); 875 | setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, 876 | true /* requires update */); 877 | } 878 | break; 879 | } 880 | case MotionEventCompat.ACTION_POINTER_DOWN: { 881 | final int index = MotionEventCompat.getActionIndex(ev); 882 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 883 | break; 884 | } 885 | 886 | case MotionEventCompat.ACTION_POINTER_UP: 887 | onSecondaryPointerUp(ev); 888 | break; 889 | 890 | case MotionEvent.ACTION_UP: 891 | case MotionEvent.ACTION_CANCEL: { 892 | if (mActivePointerId == INVALID_POINTER) { 893 | if (action == MotionEvent.ACTION_UP) { 894 | } 895 | return false; 896 | } 897 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 898 | final float y = MotionEventCompat.getY(ev, pointerIndex); 899 | 900 | float overscrollTop; 901 | switch (mDirection) { 902 | case BOTTOM: 903 | overscrollTop = (mInitialMotionY - y) * DRAG_RATE; 904 | break; 905 | case TOP: 906 | default: 907 | overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 908 | break; 909 | } 910 | mIsBeingDragged = false; 911 | if (overscrollTop > mTotalDragDistance) { 912 | setRefreshing(true, true /* notify */); 913 | } else { 914 | // cancel refresh 915 | mRefreshing = false; 916 | mProgress.setStartEndTrim(0f, 0f); 917 | AnimationListener listener = null; 918 | if (!mScale) { 919 | listener = new AnimationListener() { 920 | 921 | @Override 922 | public void onAnimationStart(Animation animation) { 923 | } 924 | 925 | @Override 926 | public void onAnimationEnd(Animation animation) { 927 | if (!mScale) { 928 | startScaleDownAnimation(null); 929 | } 930 | } 931 | 932 | @Override 933 | public void onAnimationRepeat(Animation animation) { 934 | } 935 | 936 | }; 937 | } 938 | animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); 939 | mProgress.showArrow(false); 940 | } 941 | mActivePointerId = INVALID_POINTER; 942 | return false; 943 | } 944 | } 945 | 946 | return true; 947 | } 948 | 949 | private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { 950 | mFrom = from; 951 | mAnimateToCorrectPosition.reset(); 952 | mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); 953 | mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); 954 | if (listener != null) { 955 | mCircleView.setAnimationListener(listener); 956 | } 957 | mCircleView.clearAnimation(); 958 | mCircleView.startAnimation(mAnimateToCorrectPosition); 959 | } 960 | 961 | private void animateOffsetToStartPosition(int from, AnimationListener listener) { 962 | if (mScale) { 963 | // Scale the item back down 964 | startScaleDownReturnToStartAnimation(from, listener); 965 | } else { 966 | mFrom = from; 967 | mAnimateToStartPosition.reset(); 968 | mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); 969 | mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); 970 | if (listener != null) { 971 | mCircleView.setAnimationListener(listener); 972 | } 973 | mCircleView.clearAnimation(); 974 | mCircleView.startAnimation(mAnimateToStartPosition); 975 | } 976 | } 977 | 978 | private final Animation mAnimateToCorrectPosition = new Animation() { 979 | @Override 980 | public void applyTransformation(float interpolatedTime, Transformation t) { 981 | int targetTop = 0; 982 | int endTarget = 0; 983 | if (!mUsingCustomStart) { 984 | switch (mDirection) { 985 | case BOTTOM: 986 | endTarget = getMeasuredHeight() - (int) (mSpinnerFinalOffset); 987 | break; 988 | case TOP: 989 | default: 990 | endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); 991 | break; 992 | } 993 | } else { 994 | endTarget = (int) mSpinnerFinalOffset; 995 | } 996 | targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); 997 | int offset = targetTop - mCircleView.getTop(); 998 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 999 | } 1000 | }; 1001 | 1002 | private void moveToStart(float interpolatedTime) { 1003 | int targetTop = 0; 1004 | targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); 1005 | int offset = targetTop - mCircleView.getTop(); 1006 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 1007 | } 1008 | 1009 | private final Animation mAnimateToStartPosition = new Animation() { 1010 | @Override 1011 | public void applyTransformation(float interpolatedTime, Transformation t) { 1012 | moveToStart(interpolatedTime); 1013 | } 1014 | }; 1015 | 1016 | private void startScaleDownReturnToStartAnimation(int from, 1017 | AnimationListener listener) { 1018 | mFrom = from; 1019 | if (isAlphaUsedForScale()) { 1020 | mStartingScale = mProgress.getAlpha(); 1021 | } else { 1022 | mStartingScale = ViewCompat.getScaleX(mCircleView); 1023 | } 1024 | mScaleDownToStartAnimation = new Animation() { 1025 | @Override 1026 | public void applyTransformation(float interpolatedTime, Transformation t) { 1027 | float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); 1028 | setAnimationProgress(targetScale); 1029 | moveToStart(interpolatedTime); 1030 | } 1031 | }; 1032 | mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); 1033 | if (listener != null) { 1034 | mCircleView.setAnimationListener(listener); 1035 | } 1036 | mCircleView.clearAnimation(); 1037 | mCircleView.startAnimation(mScaleDownToStartAnimation); 1038 | } 1039 | 1040 | private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { 1041 | mCircleView.bringToFront(); 1042 | mCircleView.offsetTopAndBottom(offset); 1043 | mCurrentTargetOffsetTop = mCircleView.getTop(); 1044 | if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { 1045 | invalidate(); 1046 | } 1047 | } 1048 | 1049 | private void onSecondaryPointerUp(MotionEvent ev) { 1050 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 1051 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 1052 | if (pointerId == mActivePointerId) { 1053 | // This was our active pointer going up. Choose a new 1054 | // active pointer and adjust accordingly. 1055 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1056 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 1057 | } 1058 | } 1059 | 1060 | /** 1061 | * Classes that wish to be notified when the swipe gesture correctly 1062 | * triggers a refresh should implement this interface. 1063 | */ 1064 | public interface OnRefreshListener { 1065 | public void onRefresh(SwipeRefreshLayoutDirection direction); 1066 | } 1067 | 1068 | public SwipeRefreshLayoutDirection getDirection() { 1069 | return mBothDirection ? SwipeRefreshLayoutDirection.BOTH : mDirection; 1070 | } 1071 | 1072 | public void setDirection(SwipeRefreshLayoutDirection direction) { 1073 | if (direction == SwipeRefreshLayoutDirection.BOTH) { 1074 | mBothDirection = true; 1075 | } else { 1076 | mBothDirection = false; 1077 | mDirection = direction; 1078 | } 1079 | 1080 | switch (mDirection) { 1081 | case BOTTOM: 1082 | mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight() - mCircleView.getMeasuredHeight(); 1083 | break; 1084 | case TOP: 1085 | default: 1086 | mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 1087 | break; 1088 | } 1089 | } 1090 | 1091 | // only TOP or Bottom 1092 | private void setRawDirection(SwipeRefreshLayoutDirection direction) { 1093 | if (mDirection == direction) { 1094 | return; 1095 | } 1096 | 1097 | mDirection = direction; 1098 | switch (mDirection) { 1099 | case BOTTOM: 1100 | mCurrentTargetOffsetTop = mOriginalOffsetTop = getMeasuredHeight() - mCircleView.getMeasuredHeight(); 1101 | break; 1102 | case TOP: 1103 | default: 1104 | mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 1105 | break; 1106 | } 1107 | } 1108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/rxjava_retrofit_mvp_md/refresh/SpaceItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.example.rxjava_retrofit_mvp_md.refresh; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | /** 7 | * Created by KomoriWu 8 | * on 2017-03-25. 9 | */ 10 | 11 | public class SpaceItemDecoration extends RecyclerView.ItemDecoration{ 12 | 13 | private int space; 14 | 15 | public SpaceItemDecoration(int space) { 16 | this.space = space; 17 | } 18 | 19 | @Override 20 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 21 | RecyclerView.State state) { 22 | 23 | if(parent.getChildPosition(view) != 0) 24 | outRect.top = space; 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/rxjava_retrofit_mvp_md/refresh/SwipeRefreshLayoutDirection.java: -------------------------------------------------------------------------------- 1 | package com.example.rxjava_retrofit_mvp_md.refresh; 2 | 3 | /** 4 | * Created by oliviergoutay on 1/23/15. 5 | */ 6 | public enum SwipeRefreshLayoutDirection { 7 | 8 | TOP(0), 9 | BOTTOM(1), 10 | BOTH(2); 11 | 12 | private int mValue; 13 | 14 | SwipeRefreshLayoutDirection(int value) { 15 | this.mValue = value; 16 | } 17 | 18 | public static SwipeRefreshLayoutDirection getFromInt(int value) { 19 | for (SwipeRefreshLayoutDirection direction : SwipeRefreshLayoutDirection.values()) { 20 | if (direction.mValue == value) { 21 | return direction; 22 | } 23 | } 24 | return BOTH; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/rxjava_retrofit_mvp_md/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.example.rxjava_retrofit_mvp_md.utils; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.res.TypedArray; 8 | import android.support.design.widget.Snackbar; 9 | import android.view.View; 10 | 11 | import com.example.rxjava_retrofit_mvp_md.R; 12 | import com.nostra13.universalimageloader.core.DisplayImageOptions; 13 | import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer; 14 | 15 | /** 16 | * Created by KomoriWu 17 | * on 2017-03-24. 18 | */ 19 | 20 | public class Utils { 21 | public static final String URL = "http://120.24.218.251:8080/geek_wz/"; 22 | public static final String GET_ALL_ARTICLES = "Make_ArticleJson"; 23 | 24 | 25 | public static int getToolbarHeight(Context context) { 26 | final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes( 27 | new int[]{R.attr.actionBarSize}); 28 | int toolbarHeight = (int) styledAttributes.getDimension(0, 0); 29 | styledAttributes.recycle(); 30 | 31 | return toolbarHeight; 32 | } 33 | 34 | public static DisplayImageOptions getImageOptions() { 35 | return getImageOptions(R.mipmap.jiazaishibai, 0); 36 | } 37 | 38 | public static DisplayImageOptions getImageOptions(boolean isCircular) { 39 | return getImageOptions(R.mipmap.jiazaishibai, 180); 40 | } 41 | 42 | public static DisplayImageOptions getImageOptions(int defaultIconId) { 43 | return getImageOptions(defaultIconId, 0); 44 | } 45 | 46 | public static DisplayImageOptions getImageOptions(int defaultIconId, int cornerRadiusPixels) { 47 | return new DisplayImageOptions.Builder() 48 | .displayer(new RoundedBitmapDisplayer(cornerRadiusPixels)) 49 | .showImageOnLoading(defaultIconId) 50 | .showImageOnFail(defaultIconId) 51 | .showImageForEmptyUri(defaultIconId) 52 | .cacheInMemory(true) 53 | .cacheOnDisc() 54 | .build(); 55 | } 56 | 57 | public static void showSnackBar(View view, String str) { 58 | Snackbar.make(view, str, Snackbar.LENGTH_SHORT).setAction(R.string.kown, 59 | new View.OnClickListener() { 60 | @Override 61 | public void onClick(View v) { 62 | 63 | } 64 | }).show(); 65 | } 66 | public static void showAlertDialog(Context context, String message) { 67 | if (!((Activity) context).isFinishing()) { 68 | AlertDialog dialog = new AlertDialog.Builder(context) 69 | .setMessage(message) 70 | .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 71 | @Override 72 | public void onClick(DialogInterface dialog, int which) { 73 | dialog.dismiss(); 74 | } 75 | }) 76 | .create(); 77 | dialog.setCancelable(true); 78 | dialog.show(); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 |