null
.
516 | */
517 | public void setColors(@NonNull int[] colors) {
518 | mColors = colors;
519 | // if colors are reset, make sure to reset the color index as well
520 | setColorIndex(0);
521 | }
522 |
523 | /**
524 | * @param index Index into the color array of the color to display in
525 | * the progress spinner.
526 | */
527 | public void setColorIndex(int index) {
528 | mColorIndex = index;
529 | }
530 |
531 | /**
532 | * Proceed to the next available ring color. This will automatically
533 | * wrap back to the beginning of colors.
534 | */
535 | public void goToNextColor() {
536 | mColorIndex = (mColorIndex + 1) % (mColors.length);
537 | }
538 |
539 | public void setColorFilter(ColorFilter filter) {
540 | mPaint.setColorFilter(filter);
541 | invalidateSelf();
542 | }
543 |
544 | /**
545 | * @param alpha Set the alpha of the progress spinner and associated arrowhead.
546 | */
547 | public void setAlpha(int alpha) {
548 | mAlpha = alpha;
549 | }
550 |
551 | /**
552 | * @return Current alpha of the progress spinner and arrowhead.
553 | */
554 | public int getAlpha() {
555 | return mAlpha;
556 | }
557 |
558 | /**
559 | * @param strokeWidth Set the stroke width of the progress spinner in pixels.
560 | */
561 | public void setStrokeWidth(float strokeWidth) {
562 | mStrokeWidth = strokeWidth;
563 | mPaint.setStrokeWidth(strokeWidth);
564 | invalidateSelf();
565 | }
566 |
567 | @SuppressWarnings("unused")
568 | public float getStrokeWidth() {
569 | return mStrokeWidth;
570 | }
571 |
572 | @SuppressWarnings("unused")
573 | public void setStartTrim(float startTrim) {
574 | mStartTrim = startTrim;
575 | invalidateSelf();
576 | }
577 |
578 | @SuppressWarnings("unused")
579 | public float getStartTrim() {
580 | return mStartTrim;
581 | }
582 |
583 | public float getStartingStartTrim() {
584 | return mStartingStartTrim;
585 | }
586 |
587 | public float getStartingEndTrim() {
588 | return mStartingEndTrim;
589 | }
590 |
591 | @SuppressWarnings("unused")
592 | public void setEndTrim(float endTrim) {
593 | mEndTrim = endTrim;
594 | invalidateSelf();
595 | }
596 |
597 | @SuppressWarnings("unused")
598 | public float getEndTrim() {
599 | return mEndTrim;
600 | }
601 |
602 | @SuppressWarnings("unused")
603 | public void setRotation(float rotation) {
604 | mRotation = rotation;
605 | invalidateSelf();
606 | }
607 |
608 | @SuppressWarnings("unused")
609 | public float getRotation() {
610 | return mRotation;
611 | }
612 |
613 | public void setInsets(int width, int height) {
614 | final float minEdge = (float) Math.min(width, height);
615 | float insets;
616 | if (mRingCenterRadius <= 0 || minEdge < 0) {
617 | insets = (float) Math.ceil(mStrokeWidth / 2.0f);
618 | } else {
619 | insets = (float) (minEdge / 2.0f - mRingCenterRadius);
620 | }
621 | mStrokeInset = insets;
622 | }
623 |
624 | @SuppressWarnings("unused")
625 | public float getInsets() {
626 | return mStrokeInset;
627 | }
628 |
629 | /**
630 | * @param centerRadius Inner radius in px of the circle the progress
631 | * spinner arc traces.
632 | */
633 | public void setCenterRadius(double centerRadius) {
634 | mRingCenterRadius = centerRadius;
635 | }
636 |
637 | public double getCenterRadius() {
638 | return mRingCenterRadius;
639 | }
640 |
641 | /**
642 | * @param show Set to true to show the arrow head on the progress spinner.
643 | */
644 | public void setShowArrow(boolean show) {
645 | if (mShowArrow != show) {
646 | mShowArrow = show;
647 | invalidateSelf();
648 | }
649 | }
650 |
651 | /**
652 | * @param scale Set the scale of the arrowhead for the spinner.
653 | */
654 | public void setArrowScale(float scale) {
655 | if (scale != mArrowScale) {
656 | mArrowScale = scale;
657 | invalidateSelf();
658 | }
659 | }
660 |
661 | /**
662 | * @return The amount the progress spinner is currently rotated, between [0..1].
663 | */
664 | public float getStartingRotation() {
665 | return mStartingRotation;
666 | }
667 |
668 | /**
669 | * If the start / end trim are offset to begin with, store them so that
670 | * animation starts from that offset.
671 | */
672 | public void storeOriginals() {
673 | mStartingStartTrim = mStartTrim;
674 | mStartingEndTrim = mEndTrim;
675 | mStartingRotation = mRotation;
676 | }
677 |
678 | /**
679 | * Reset the progress spinner to default rotation, start and end angles.
680 | */
681 | public void resetOriginals() {
682 | mStartingStartTrim = 0;
683 | mStartingEndTrim = 0;
684 | mStartingRotation = 0;
685 | setStartTrim(0);
686 | setEndTrim(0);
687 | setRotation(0);
688 | }
689 |
690 | private void invalidateSelf() {
691 | mCallback.invalidateDrawable(null);
692 | }
693 | }
694 |
695 | /**
696 | * Squishes the interpolation curve into the second half of the animation.
697 | */
698 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
699 | @Override
700 | public float getInterpolation(float input) {
701 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
702 | }
703 | }
704 |
705 | /**
706 | * Squishes the interpolation curve into the first half of the animation.
707 | */
708 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
709 | @Override
710 | public float getInterpolation(float input) {
711 | return super.getInterpolation(Math.min(1, input * 2.0f));
712 | }
713 | }
714 | }
--------------------------------------------------------------------------------
/android/src/com/rkam/swiperefreshlayout/MySwipeRefreshLayout.java:
--------------------------------------------------------------------------------
1 | package com.rkam.swiperefreshlayout;
2 |
3 | import android.content.Context;
4 | import android.support.v4.view.ViewCompat;
5 | import android.util.AttributeSet;
6 | import android.view.View;
7 | import android.widget.AbsListView;
8 | import android.widget.FrameLayout;
9 | import android.widget.ScrollView;
10 | import android.widget.RelativeLayout;
11 |
12 | /**
13 | * MySwipeRefreshLayout is a modified SwipeRefreshLayout so that Titanium views
14 | * are supported. It overrides the canChildScrollUp method used by Android to
15 | * determine whether the gesture is for refresh or if the user is just scrolling up
16 | * the scrollable view.
17 | */
18 | public class MySwipeRefreshLayout extends SwipeRefreshLayout {
19 |
20 | private View nativeView; // usually the layout wrapping the listview
21 | private View nativeChildView; // the native android listview
22 |
23 | public MySwipeRefreshLayout(Context context) {
24 | super(context);
25 | }
26 |
27 | public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
28 | super(context, attrs);
29 | }
30 |
31 | public View getNativeView() {
32 | return nativeView;
33 | }
34 |
35 | public void setNativeView(View view) {
36 | this.nativeView = view;
37 | }
38 |
39 | @Override
40 | public boolean canChildScrollUp() {
41 | // ScrollViews are also an instance of FrameLayouts and we do not want to get
42 | // the ScrollView's child view as it will not work.
43 | if (nativeView instanceof FrameLayout && !(nativeView instanceof ScrollView)) {
44 | // Try to get the native Android ListView inside the FrameLayout
45 | nativeChildView = ((FrameLayout) nativeView).getChildAt(0);
46 | } else if(nativeView instanceof RelativeLayout){
47 | //get the ListView inside the tableView
48 | nativeChildView = ((RelativeLayout) nativeView).getChildAt(1);
49 | nativeChildView = ((FrameLayout) nativeChildView).getChildAt(0);
50 | } else {
51 | nativeChildView = nativeView;
52 | }
53 | if (android.os.Build.VERSION.SDK_INT < 14) {
54 | if (nativeChildView instanceof AbsListView) {
55 | final AbsListView absListView = (AbsListView) nativeChildView;
56 | return absListView.getChildCount() > 0
57 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
58 | .getTop() < absListView.getPaddingTop());
59 | } else {
60 | return nativeChildView.getScrollY() > 0;
61 | }
62 | } else {
63 | return ViewCompat.canScrollVertically(nativeChildView, -1);
64 | }
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/android/src/com/rkam/swiperefreshlayout/ScrollingView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2014 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.rkam.swiperefreshlayout;
18 |
19 | /**
20 | * An interface that can be implemented by Views to provide scroll related APIs.
21 | */
22 | public interface ScrollingView {
23 | /**
24 | * Compute the horizontal range that the horizontal scrollbar 25 | * represents.
26 | * 27 | *The range is expressed in arbitrary units that must be the same as the 28 | * units used by {@link #computeHorizontalScrollExtent()} and 29 | * {@link #computeHorizontalScrollOffset()}.
30 | * 31 | *The default range is the drawing width of this view.
32 | * 33 | * @return the total horizontal range represented by the horizontal 34 | * scrollbar 35 | * 36 | * @see #computeHorizontalScrollExtent() 37 | * @see #computeHorizontalScrollOffset() 38 | * @see android.widget.ScrollBarDrawable 39 | */ 40 | int computeHorizontalScrollRange(); 41 | 42 | /** 43 | *Compute the horizontal offset of the horizontal scrollbar's thumb 44 | * within the horizontal range. This value is used to compute the position 45 | * of the thumb within the scrollbar's track.
46 | * 47 | *The range is expressed in arbitrary units that must be the same as the 48 | * units used by {@link #computeHorizontalScrollRange()} and 49 | * {@link #computeHorizontalScrollExtent()}.
50 | * 51 | *The default offset is the scroll offset of this view.
52 | * 53 | * @return the horizontal offset of the scrollbar's thumb 54 | * 55 | * @see #computeHorizontalScrollRange() 56 | * @see #computeHorizontalScrollExtent() 57 | * @see android.widget.ScrollBarDrawable 58 | */ 59 | int computeHorizontalScrollOffset(); 60 | 61 | /** 62 | *Compute the horizontal extent of the horizontal scrollbar's thumb 63 | * within the horizontal range. This value is used to compute the length 64 | * of the thumb within the scrollbar's track.
65 | * 66 | *The range is expressed in arbitrary units that must be the same as the 67 | * units used by {@link #computeHorizontalScrollRange()} and 68 | * {@link #computeHorizontalScrollOffset()}.
69 | * 70 | *The default extent is the drawing width of this view.
71 | * 72 | * @return the horizontal extent of the scrollbar's thumb 73 | * 74 | * @see #computeHorizontalScrollRange() 75 | * @see #computeHorizontalScrollOffset() 76 | * @see android.widget.ScrollBarDrawable 77 | */ 78 | int computeHorizontalScrollExtent(); 79 | 80 | /** 81 | *Compute the vertical range that the vertical scrollbar represents.
82 | * 83 | *The range is expressed in arbitrary units that must be the same as the 84 | * units used by {@link #computeVerticalScrollExtent()} and 85 | * {@link #computeVerticalScrollOffset()}.
86 | * 87 | * @return the total vertical range represented by the vertical scrollbar 88 | * 89 | *The default range is the drawing height of this view.
90 | * 91 | * @see #computeVerticalScrollExtent() 92 | * @see #computeVerticalScrollOffset() 93 | * @see android.widget.ScrollBarDrawable 94 | */ 95 | int computeVerticalScrollRange(); 96 | 97 | /** 98 | *Compute the vertical offset of the vertical scrollbar's thumb 99 | * within the horizontal range. This value is used to compute the position 100 | * of the thumb within the scrollbar's track.
101 | * 102 | *The range is expressed in arbitrary units that must be the same as the 103 | * units used by {@link #computeVerticalScrollRange()} and 104 | * {@link #computeVerticalScrollExtent()}.
105 | * 106 | *The default offset is the scroll offset of this view.
107 | * 108 | * @return the vertical offset of the scrollbar's thumb 109 | * 110 | * @see #computeVerticalScrollRange() 111 | * @see #computeVerticalScrollExtent() 112 | * @see android.widget.ScrollBarDrawable 113 | */ 114 | int computeVerticalScrollOffset(); 115 | 116 | /** 117 | *Compute the vertical extent of the vertical scrollbar's thumb 118 | * within the vertical range. This value is used to compute the length 119 | * of the thumb within the scrollbar's track.
120 | * 121 | *The range is expressed in arbitrary units that must be the same as the 122 | * units used by {@link #computeVerticalScrollRange()} and 123 | * {@link #computeVerticalScrollOffset()}.
124 | * 125 | *The default extent is the drawing height of this view.
126 | * 127 | * @return the vertical extent of the scrollbar's thumb 128 | * 129 | * @see #computeVerticalScrollRange() 130 | * @see #computeVerticalScrollOffset() 131 | * @see android.widget.ScrollBarDrawable 132 | */ 133 | int computeVerticalScrollExtent(); 134 | } -------------------------------------------------------------------------------- /android/src/com/rkam/swiperefreshlayout/SwipeProgressBar.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.rkam.swiperefreshlayout; 18 | 19 | import android.graphics.Canvas; 20 | import android.graphics.Paint; 21 | import android.graphics.Rect; 22 | import android.graphics.RectF; 23 | import android.support.v4.view.ViewCompat; 24 | import android.view.View; 25 | import android.view.animation.AnimationUtils; 26 | import android.view.animation.Interpolator; 27 | 28 | 29 | /** 30 | * Custom progress bar that shows a cycle of colors as widening circles that 31 | * overdraw each other. When finished, the bar is cleared from the inside out as 32 | * the main cycle continues. Before running, this can also indicate how close 33 | * the user is to triggering something (e.g. how far they need to pull down to 34 | * trigger a refresh). 35 | */ 36 | final class SwipeProgressBar { 37 | 38 | // Default progress animation colors are grays. 39 | private final static int COLOR1 = 0xB3000000; 40 | private final static int COLOR2 = 0x80000000; 41 | private final static int COLOR3 = 0x4d000000; 42 | private final static int COLOR4 = 0x1a000000; 43 | 44 | // The duration of the animation cycle. 45 | private static final int ANIMATION_DURATION_MS = 2000; 46 | 47 | // The duration of the animation to clear the bar. 48 | private static final int FINISH_ANIMATION_DURATION_MS = 1000; 49 | 50 | // Interpolator for varying the speed of the animation. 51 | private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); 52 | 53 | private final Paint mPaint = new Paint(); 54 | private final RectF mClipRect = new RectF(); 55 | private float mTriggerPercentage; 56 | private long mStartTime; 57 | private long mFinishTime; 58 | private boolean mRunning; 59 | 60 | // Colors used when rendering the animation, 61 | private int mColor1; 62 | private int mColor2; 63 | private int mColor3; 64 | private int mColor4; 65 | private View mParent; 66 | 67 | private Rect mBounds = new Rect(); 68 | 69 | public SwipeProgressBar(View parent) { 70 | mParent = parent; 71 | mColor1 = COLOR1; 72 | mColor2 = COLOR2; 73 | mColor3 = COLOR3; 74 | mColor4 = COLOR4; 75 | } 76 | 77 | /** 78 | * Set the four colors used in the progress animation. The first color will 79 | * also be the color of the bar that grows in response to a user swipe 80 | * gesture. 81 | * 82 | * @param color1 Integer representation of a color. 83 | * @param color2 Integer representation of a color. 84 | * @param color3 Integer representation of a color. 85 | * @param color4 Integer representation of a color. 86 | */ 87 | void setColorScheme(int color1, int color2, int color3, int color4) { 88 | mColor1 = color1; 89 | mColor2 = color2; 90 | mColor3 = color3; 91 | mColor4 = color4; 92 | } 93 | 94 | /** 95 | * Update the progress the user has made toward triggering the swipe 96 | * gesture. and use this value to update the percentage of the trigger that 97 | * is shown. 98 | */ 99 | void setTriggerPercentage(float triggerPercentage) { 100 | mTriggerPercentage = triggerPercentage; 101 | mStartTime = 0; 102 | ViewCompat.postInvalidateOnAnimation( 103 | mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); 104 | } 105 | 106 | /** 107 | * Start showing the progress animation. 108 | */ 109 | void start() { 110 | if (!mRunning) { 111 | mTriggerPercentage = 0; 112 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); 113 | mRunning = true; 114 | mParent.postInvalidate(); 115 | } 116 | } 117 | 118 | /** 119 | * Stop showing the progress animation. 120 | */ 121 | void stop() { 122 | if (mRunning) { 123 | mTriggerPercentage = 0; 124 | mFinishTime = AnimationUtils.currentAnimationTimeMillis(); 125 | mRunning = false; 126 | mParent.postInvalidate(); 127 | } 128 | } 129 | 130 | /** 131 | * @return Return whether the progress animation is currently running. 132 | */ 133 | boolean isRunning() { 134 | return mRunning || mFinishTime > 0; 135 | } 136 | 137 | void draw(Canvas canvas) { 138 | final int width = mBounds.width(); 139 | final int height = mBounds.height(); 140 | final int cx = width / 2; 141 | final int cy = height / 2; 142 | boolean drawTriggerWhileFinishing = false; 143 | int restoreCount = canvas.save(); 144 | canvas.clipRect(mBounds); 145 | 146 | if (mRunning || (mFinishTime > 0)) { 147 | long now = AnimationUtils.currentAnimationTimeMillis(); 148 | long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; 149 | long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; 150 | float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); 151 | 152 | // If we're not running anymore, that means we're running through 153 | // the finish animation. 154 | if (!mRunning) { 155 | // If the finish animation is done, don't draw anything, and 156 | // don't repost. 157 | if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { 158 | mFinishTime = 0; 159 | return; 160 | } 161 | 162 | // Otherwise, use a 0 opacity alpha layer to clear the animation 163 | // from the inside out. This layer will prevent the circles from 164 | // drawing within its bounds. 165 | long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; 166 | float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); 167 | float pct = (finishProgress / 100f); 168 | // Radius of the circle is half of the screen. 169 | float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); 170 | mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); 171 | canvas.saveLayerAlpha(mClipRect, 0, 0); 172 | // Only draw the trigger if there is a space in the center of 173 | // this refreshing view that needs to be filled in by the 174 | // trigger. If the progress view is just still animating, let it 175 | // continue animating. 176 | drawTriggerWhileFinishing = true; 177 | } 178 | 179 | // First fill in with the last color that would have finished drawing. 180 | if (iterations == 0) { 181 | canvas.drawColor(mColor1); 182 | } else { 183 | if (rawProgress >= 0 && rawProgress < 25) { 184 | canvas.drawColor(mColor4); 185 | } else if (rawProgress >= 25 && rawProgress < 50) { 186 | canvas.drawColor(mColor1); 187 | } else if (rawProgress >= 50 && rawProgress < 75) { 188 | canvas.drawColor(mColor2); 189 | } else { 190 | canvas.drawColor(mColor3); 191 | } 192 | } 193 | 194 | // Then draw up to 4 overlapping concentric circles of varying radii, based on how far 195 | // along we are in the cycle. 196 | // progress 0-50 draw mColor2 197 | // progress 25-75 draw mColor3 198 | // progress 50-100 draw mColor4 199 | // progress 75 (wrap to 25) draw mColor1 200 | if ((rawProgress >= 0 && rawProgress <= 25)) { 201 | float pct = (((rawProgress + 25) * 2) / 100f); 202 | drawCircle(canvas, cx, cy, mColor1, pct); 203 | } 204 | if (rawProgress >= 0 && rawProgress <= 50) { 205 | float pct = ((rawProgress * 2) / 100f); 206 | drawCircle(canvas, cx, cy, mColor2, pct); 207 | } 208 | if (rawProgress >= 25 && rawProgress <= 75) { 209 | float pct = (((rawProgress - 25) * 2) / 100f); 210 | drawCircle(canvas, cx, cy, mColor3, pct); 211 | } 212 | if (rawProgress >= 50 && rawProgress <= 100) { 213 | float pct = (((rawProgress - 50) * 2) / 100f); 214 | drawCircle(canvas, cx, cy, mColor4, pct); 215 | } 216 | if ((rawProgress >= 75 && rawProgress <= 100)) { 217 | float pct = (((rawProgress - 75) * 2) / 100f); 218 | drawCircle(canvas, cx, cy, mColor1, pct); 219 | } 220 | if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { 221 | // There is some portion of trigger to draw. Restore the canvas, 222 | // then draw the trigger. Otherwise, the trigger does not appear 223 | // until after the bar has finished animating and appears to 224 | // just jump in at a larger width than expected. 225 | canvas.restoreToCount(restoreCount); 226 | restoreCount = canvas.save(); 227 | canvas.clipRect(mBounds); 228 | drawTrigger(canvas, cx, cy); 229 | } 230 | // Keep running until we finish out the last cycle. 231 | ViewCompat.postInvalidateOnAnimation( 232 | mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); 233 | } else { 234 | // Otherwise if we're in the middle of a trigger, draw that. 235 | if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { 236 | drawTrigger(canvas, cx, cy); 237 | } 238 | } 239 | canvas.restoreToCount(restoreCount); 240 | } 241 | 242 | private void drawTrigger(Canvas canvas, int cx, int cy) { 243 | mPaint.setColor(mColor1); 244 | canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); 245 | } 246 | 247 | /** 248 | * Draws a circle centered in the view. 249 | * 250 | * @param canvas the canvas to draw on 251 | * @param cx the center x coordinate 252 | * @param cy the center y coordinate 253 | * @param color the color to draw 254 | * @param pct the percentage of the view that the circle should cover 255 | */ 256 | private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { 257 | mPaint.setColor(color); 258 | canvas.save(); 259 | canvas.translate(cx, cy); 260 | float radiusScale = INTERPOLATOR.getInterpolation(pct); 261 | canvas.scale(radiusScale, radiusScale); 262 | canvas.drawCircle(0, 0, cx, mPaint); 263 | canvas.restore(); 264 | } 265 | 266 | /** 267 | * Set the drawing bounds of this SwipeProgressBar. 268 | */ 269 | void setBounds(int left, int top, int right, int bottom) { 270 | mBounds.left = left; 271 | mBounds.top = top; 272 | mBounds.right = right; 273 | mBounds.bottom = bottom; 274 | } 275 | } -------------------------------------------------------------------------------- /android/src/com/rkam/swiperefreshlayout/SwipeRefresh.java: -------------------------------------------------------------------------------- 1 | package com.rkam.swiperefreshlayout; 2 | 3 | import org.appcelerator.kroll.KrollDict; 4 | import org.appcelerator.titanium.TiApplication; 5 | import org.appcelerator.titanium.proxy.TiViewProxy; 6 | import org.appcelerator.titanium.util.TiRHelper; 7 | import org.appcelerator.titanium.util.TiRHelper.ResourceNotFoundException; 8 | import org.appcelerator.titanium.view.TiUIView; 9 | 10 | import android.util.Log; 11 | import android.view.LayoutInflater; 12 | 13 | import com.rkam.swiperefreshlayout.SwipeRefreshLayout.OnRefreshListener; 14 | 15 | public class SwipeRefresh extends TiUIView { 16 | 17 | private MySwipeRefreshLayout layout; 18 | private TiViewProxy view; 19 | 20 | public static final String PROPERTY_VIEW = "view"; 21 | public static final String PROPERTY_COLOR_SCHEME = "colorScheme"; 22 | private static final String TAG = "SwipeRefresh"; 23 | 24 | int color1 = 0; 25 | int color2 = 0; 26 | int color3 = 0; 27 | int color4 = 0; 28 | int layout_swipe_refresh = 0; 29 | 30 | // Constructor for SwipeRefresh 31 | public SwipeRefresh(final SwipeRefreshProxy proxy) { 32 | super(proxy); 33 | 34 | try { 35 | layout_swipe_refresh = TiRHelper.getResource("layout.swipe_refresh"); 36 | color1 = TiRHelper.getResource("color.color1"); 37 | color2 = TiRHelper.getResource("color.color2"); 38 | color3 = TiRHelper.getResource("color.color3"); 39 | color4 = TiRHelper.getResource("color.color4"); 40 | } 41 | catch (ResourceNotFoundException e) { 42 | Log.e(TAG, "Resources not found!"); 43 | } 44 | 45 | LayoutInflater inflater = LayoutInflater.from(TiApplication.getInstance()); 46 | layout = (MySwipeRefreshLayout) inflater.inflate(layout_swipe_refresh, null, false); 47 | 48 | layout.setOnRefreshListener(new OnRefreshListener() { 49 | @Override 50 | public void onRefresh() { 51 | if (proxy.hasListeners("refreshing")) { 52 | proxy.fireEvent("refreshing", null); 53 | } 54 | } 55 | }); 56 | 57 | setNativeView(layout); 58 | } 59 | 60 | @Override 61 | public void processProperties(KrollDict d) { 62 | if (d.containsKey(PROPERTY_VIEW)) { 63 | Object view = d.get(PROPERTY_VIEW); 64 | if (view != null && view instanceof TiViewProxy) { 65 | this.view = (TiViewProxy) view; 66 | this.layout.setNativeView(this.view.getOrCreateView().getNativeView()); 67 | this.layout.addView(this.view.getOrCreateView().getOuterView()); 68 | this.layout.setColorScheme(color1, color2, color3, color4); 69 | } 70 | } 71 | super.processProperties(d); 72 | } 73 | 74 | public void add(TiViewProxy view) { 75 | if (view != null && view instanceof TiViewProxy) { 76 | this.view = (TiViewProxy) view; 77 | this.layout.setNativeView(this.view.getOrCreateView().getNativeView()); 78 | this.layout.addView(this.view.getOrCreateView().getOuterView()); 79 | this.layout.setColorScheme(color1, color2, color3, color4); 80 | } 81 | } 82 | 83 | public boolean isRefreshing() { 84 | return this.layout.isRefreshing(); 85 | } 86 | 87 | public void setRefreshing(final boolean refreshing) { 88 | if (this.view != null) { 89 | this.view.getActivity().runOnUiThread(new Runnable() { 90 | @Override 91 | public void run() { 92 | layout.setRefreshing(refreshing); 93 | } 94 | }); 95 | } 96 | } 97 | 98 | public void setEnabled(final boolean refreshEnabled) { 99 | if (this.view != null) { 100 | this.view.getActivity().runOnUiThread(new Runnable() { 101 | @Override 102 | public void run() { 103 | layout.setEnabled(refreshEnabled); 104 | } 105 | }); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /android/src/com/rkam/swiperefreshlayout/SwipeRefreshLayout.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.rkam.swiperefreshlayout; 18 | 19 | import java.lang.reflect.InvocationTargetException; 20 | import java.lang.reflect.Method; 21 | 22 | import android.content.Context; 23 | import android.content.res.Resources; 24 | import android.content.res.TypedArray; 25 | import android.support.v4.view.MotionEventCompat; 26 | import android.support.v4.view.ViewCompat; 27 | import android.util.AttributeSet; 28 | import android.util.DisplayMetrics; 29 | import android.util.Log; 30 | import android.view.MotionEvent; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | import android.view.ViewGroup; 34 | import android.view.animation.Animation; 35 | import android.view.animation.Animation.AnimationListener; 36 | import android.view.animation.DecelerateInterpolator; 37 | import android.view.animation.Transformation; 38 | import android.widget.AbsListView; 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 SwipeRefreshLayout 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 = SwipeRefreshLayout.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 OnRefreshListener mListener; 98 | private boolean mRefreshing = false; 99 | private int mTouchSlop; 100 | private float mTotalDragDistance = -1; 101 | private int mMediumAnimationDuration; 102 | private int mCurrentTargetOffsetTop; 103 | // Whether or not the starting offset has been determined. 104 | private boolean mOriginalOffsetCalculated = false; 105 | 106 | private float mInitialMotionY; 107 | private float mInitialDownY; 108 | private boolean mIsBeingDragged; 109 | private int mActivePointerId = INVALID_POINTER; 110 | // Whether this item is scaled up rather than clipped 111 | private boolean mScale; 112 | 113 | // Target is returning to its start offset because it was cancelled or a 114 | // refresh was triggered. 115 | private boolean mReturningToStart; 116 | private final DecelerateInterpolator mDecelerateInterpolator; 117 | private static final int[] LAYOUT_ATTRS = new int[] { 118 | android.R.attr.enabled 119 | }; 120 | 121 | private CircleImageView mCircleView; 122 | private int mCircleViewIndex = -1; 123 | 124 | protected int mFrom; 125 | 126 | private float mStartingScale; 127 | 128 | protected int mOriginalOffsetTop; 129 | 130 | private MaterialProgressDrawable mProgress; 131 | 132 | private Animation mScaleAnimation; 133 | 134 | private Animation mScaleDownAnimation; 135 | 136 | private Animation mAlphaStartAnimation; 137 | 138 | private Animation mAlphaMaxAnimation; 139 | 140 | private Animation mScaleDownToStartAnimation; 141 | 142 | private float mSpinnerFinalOffset; 143 | 144 | private boolean mNotify; 145 | 146 | private int mCircleWidth; 147 | 148 | private int mCircleHeight; 149 | 150 | // Whether the client has set a custom starting position; 151 | private boolean mUsingCustomStart; 152 | 153 | private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { 154 | @Override 155 | public void onAnimationStart(Animation animation) { 156 | } 157 | 158 | @Override 159 | public void onAnimationRepeat(Animation animation) { 160 | } 161 | 162 | @Override 163 | public void onAnimationEnd(Animation animation) { 164 | if (mRefreshing) { 165 | // Make sure the progress view is fully visible 166 | mProgress.setAlpha(MAX_ALPHA); 167 | mProgress.start(); 168 | if (mNotify) { 169 | if (mListener != null) { 170 | mListener.onRefresh(); 171 | } 172 | } 173 | } else { 174 | mProgress.stop(); 175 | mCircleView.setVisibility(View.GONE); 176 | setColorViewAlpha(MAX_ALPHA); 177 | // Return the circle to its start position 178 | if (mScale) { 179 | setAnimationProgress(0 /* animation complete and view is hidden */); 180 | } else { 181 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, 182 | true /* requires update */); 183 | } 184 | } 185 | mCurrentTargetOffsetTop = mCircleView.getTop(); 186 | } 187 | }; 188 | 189 | private void setColorViewAlpha(int targetAlpha) { 190 | mCircleView.getBackground().setAlpha(targetAlpha); 191 | mProgress.setAlpha(targetAlpha); 192 | } 193 | 194 | /** 195 | * The refresh indicator starting and resting position is always positioned 196 | * near the top of the refreshing content. This position is a consistent 197 | * location, but can be adjusted in either direction based on whether or not 198 | * there is a toolbar or actionbar present. 199 | * 200 | * @param scale Set to true if there is no view at a higher z-order than 201 | * where the progress spinner is set to appear. 202 | * @param start The offset in pixels from the top of this view at which the 203 | * progress spinner should appear. 204 | * @param end The offset in pixels from the top of this view at which the 205 | * progress spinner should come to rest after a successful swipe 206 | * gesture. 207 | */ 208 | public void setProgressViewOffset(boolean scale, int start, int end) { 209 | mScale = scale; 210 | mCircleView.setVisibility(View.GONE); 211 | mOriginalOffsetTop = mCurrentTargetOffsetTop = start; 212 | mSpinnerFinalOffset = end; 213 | mUsingCustomStart = true; 214 | mCircleView.invalidate(); 215 | } 216 | 217 | /** 218 | * The refresh indicator resting position is always positioned near the top 219 | * of the refreshing content. This position is a consistent location, but 220 | * can be adjusted in either direction based on whether or not there is a 221 | * toolbar or actionbar present. 222 | * 223 | * @param scale Set to true if there is no view at a higher z-order than 224 | * where the progress spinner is set to appear. 225 | * @param end The offset in pixels from the top of this view at which the 226 | * progress spinner should come to rest after a successful swipe 227 | * gesture. 228 | */ 229 | public void setProgressViewEndTarget(boolean scale, int end) { 230 | mSpinnerFinalOffset = end; 231 | mScale = scale; 232 | mCircleView.invalidate(); 233 | } 234 | 235 | /** 236 | * One of DEFAULT, or LARGE. 237 | */ 238 | public void setSize(int size) { 239 | if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { 240 | return; 241 | } 242 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 243 | if (size == MaterialProgressDrawable.LARGE) { 244 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); 245 | } else { 246 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 247 | } 248 | // force the bounds of the progress circle inside the circle view to 249 | // update by setting it to null before updating its size and then 250 | // re-setting it 251 | mCircleView.setImageDrawable(null); 252 | mProgress.updateSizes(size); 253 | mCircleView.setImageDrawable(mProgress); 254 | } 255 | 256 | /** 257 | * Simple constructor to use when creating a SwipeRefreshLayout from code. 258 | * 259 | * @param context 260 | */ 261 | public SwipeRefreshLayout(Context context) { 262 | this(context, null); 263 | } 264 | 265 | /** 266 | * Constructor that is called when inflating SwipeRefreshLayout from XML. 267 | * 268 | * @param context 269 | * @param attrs 270 | */ 271 | public SwipeRefreshLayout(Context context, AttributeSet attrs) { 272 | super(context, attrs); 273 | 274 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 275 | 276 | mMediumAnimationDuration = getResources().getInteger( 277 | android.R.integer.config_mediumAnimTime); 278 | 279 | setWillNotDraw(false); 280 | mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); 281 | 282 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 283 | setEnabled(a.getBoolean(0, true)); 284 | a.recycle(); 285 | 286 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 287 | mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 288 | mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); 289 | 290 | createProgressView(); 291 | SwipeRefreshLayout.setChildrenDrawingOrderEnabled(this, true); 292 | // the absolute offset has to take into account that the circle starts at an offset 293 | mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; 294 | mTotalDragDistance = mSpinnerFinalOffset; 295 | } 296 | 297 | protected int getChildDrawingOrder(int childCount, int i) { 298 | if (mCircleViewIndex < 0) { 299 | return i; 300 | } else if (i == childCount - 1) { 301 | // Draw the selected child last 302 | return mCircleViewIndex; 303 | } else if (i >= mCircleViewIndex) { 304 | // Move the children after the selected child earlier one 305 | return i + 1; 306 | } else { 307 | // Keep the children before the selected child the same 308 | return i; 309 | } 310 | } 311 | 312 | private void createProgressView() { 313 | mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); 314 | mProgress = new MaterialProgressDrawable(getContext(), this); 315 | mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); 316 | mCircleView.setImageDrawable(mProgress); 317 | mCircleView.setVisibility(View.GONE); 318 | addView(mCircleView); 319 | } 320 | 321 | /** 322 | * Set the listener to be notified when a refresh is triggered via the swipe 323 | * gesture. 324 | */ 325 | public void setOnRefreshListener(OnRefreshListener listener) { 326 | mListener = listener; 327 | } 328 | 329 | /** 330 | * Pre API 11, alpha is used to make the progress circle appear instead of scale. 331 | */ 332 | private boolean isAlphaUsedForScale() { 333 | return android.os.Build.VERSION.SDK_INT < 11; 334 | } 335 | 336 | /** 337 | * Notify the widget that refresh state has changed. Do not call this when 338 | * refresh is triggered by a swipe gesture. 339 | * 340 | * @param refreshing Whether or not the view should show refresh progress. 341 | */ 342 | public void setRefreshing(boolean refreshing) { 343 | if (refreshing && mRefreshing != refreshing) { 344 | // scale and show 345 | mRefreshing = refreshing; 346 | int endTarget = 0; 347 | if (!mUsingCustomStart) { 348 | endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); 349 | } else { 350 | endTarget = (int) mSpinnerFinalOffset; 351 | } 352 | setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, 353 | true /* requires update */); 354 | mNotify = false; 355 | startScaleUpAnimation(mRefreshListener); 356 | } else { 357 | setRefreshing(refreshing, false /* notify */); 358 | } 359 | } 360 | 361 | private void startScaleUpAnimation(AnimationListener listener) { 362 | mCircleView.setVisibility(View.VISIBLE); 363 | if (android.os.Build.VERSION.SDK_INT >= 11) { 364 | // Pre API 11, alpha is used in place of scale up to show the 365 | // progress circle appearing. 366 | // Don't adjust the alpha during appearance otherwise. 367 | mProgress.setAlpha(MAX_ALPHA); 368 | } 369 | mScaleAnimation = new Animation() { 370 | @Override 371 | public void applyTransformation(float interpolatedTime, Transformation t) { 372 | setAnimationProgress(interpolatedTime); 373 | } 374 | }; 375 | mScaleAnimation.setDuration(mMediumAnimationDuration); 376 | if (listener != null) { 377 | mCircleView.setAnimationListener(listener); 378 | } 379 | mCircleView.clearAnimation(); 380 | mCircleView.startAnimation(mScaleAnimation); 381 | } 382 | 383 | /** 384 | * Pre API 11, this does an alpha animation. 385 | * @param progress 386 | */ 387 | private void setAnimationProgress(float progress) { 388 | if (isAlphaUsedForScale()) { 389 | setColorViewAlpha((int) (progress * MAX_ALPHA)); 390 | } else { 391 | mCircleView.setScaleX(progress); 392 | mCircleView.setScaleY(progress); 393 | } 394 | } 395 | 396 | private void setRefreshing(boolean refreshing, final boolean notify) { 397 | if (mRefreshing != refreshing) { 398 | mNotify = notify; 399 | ensureTarget(); 400 | mRefreshing = refreshing; 401 | if (mRefreshing) { 402 | animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); 403 | } else { 404 | startScaleDownAnimation(mRefreshListener); 405 | } 406 | } 407 | } 408 | 409 | private void startScaleDownAnimation(Animation.AnimationListener listener) { 410 | mScaleDownAnimation = new Animation() { 411 | @Override 412 | public void applyTransformation(float interpolatedTime, Transformation t) { 413 | setAnimationProgress(1 - interpolatedTime); 414 | } 415 | }; 416 | mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); 417 | mCircleView.setAnimationListener(listener); 418 | mCircleView.clearAnimation(); 419 | mCircleView.startAnimation(mScaleDownAnimation); 420 | } 421 | 422 | private void startProgressAlphaStartAnimation() { 423 | mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); 424 | } 425 | 426 | private void startProgressAlphaMaxAnimation() { 427 | mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); 428 | } 429 | 430 | private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { 431 | // Pre API 11, alpha is used in place of scale. Don't also use it to 432 | // show the trigger point. 433 | if (mScale && isAlphaUsedForScale()) { 434 | return null; 435 | } 436 | Animation alpha = new Animation() { 437 | @Override 438 | public void applyTransformation(float interpolatedTime, Transformation t) { 439 | mProgress 440 | .setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) 441 | * interpolatedTime))); 442 | } 443 | }; 444 | alpha.setDuration(ALPHA_ANIMATION_DURATION); 445 | // Clear out the previous animation listeners. 446 | mCircleView.setAnimationListener(null); 447 | mCircleView.clearAnimation(); 448 | mCircleView.startAnimation(alpha); 449 | return alpha; 450 | } 451 | 452 | /** 453 | * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)} 454 | */ 455 | @Deprecated 456 | public void setProgressBackgroundColor(int colorRes) { 457 | setProgressBackgroundColorSchemeResource(colorRes); 458 | } 459 | 460 | /** 461 | * Set the background color of the progress spinner disc. 462 | * 463 | * @param colorRes Resource id of the color. 464 | */ 465 | public void setProgressBackgroundColorSchemeResource(int colorRes) { 466 | setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes)); 467 | } 468 | 469 | /** 470 | * Set the background color of the progress spinner disc. 471 | * 472 | * @param color 473 | */ 474 | public void setProgressBackgroundColorSchemeColor(int color) { 475 | mCircleView.setBackgroundColor(color); 476 | mProgress.setBackgroundColor(color); 477 | } 478 | 479 | /** 480 | * @deprecated Use {@link #setColorSchemeResources(int...)} 481 | */ 482 | @Deprecated 483 | public void setColorScheme(int... colors) { 484 | setColorSchemeResources(colors); 485 | } 486 | 487 | /** 488 | * Set the color resources used in the progress animation from color resources. 489 | * The first color will also be the color of the bar that grows in response 490 | * to a user swipe gesture. 491 | * 492 | * @param colorResIds 493 | */ 494 | public void setColorSchemeResources(int... colorResIds) { 495 | final Resources res = getResources(); 496 | int[] colorRes = new int[colorResIds.length]; 497 | for (int i = 0; i < colorResIds.length; i++) { 498 | colorRes[i] = res.getColor(colorResIds[i]); 499 | } 500 | setColorSchemeColors(colorRes); 501 | } 502 | 503 | /** 504 | * Set the colors used in the progress animation. The first 505 | * color will also be the color of the bar that grows in response to a user 506 | * swipe gesture. 507 | * 508 | * @param colors 509 | */ 510 | public void setColorSchemeColors(int... colors) { 511 | ensureTarget(); 512 | mProgress.setColorSchemeColors(colors); 513 | } 514 | 515 | /** 516 | * @return Whether the SwipeRefreshWidget is actively showing refresh 517 | * progress. 518 | */ 519 | public boolean isRefreshing() { 520 | return mRefreshing; 521 | } 522 | 523 | private void ensureTarget() { 524 | // Don't bother getting the parent height if the parent hasn't been laid 525 | // out yet. 526 | if (mTarget == null) { 527 | for (int i = 0; i < getChildCount(); i++) { 528 | View child = getChildAt(i); 529 | if (!child.equals(mCircleView)) { 530 | mTarget = child; 531 | break; 532 | } 533 | } 534 | } 535 | } 536 | 537 | /** 538 | * Set the distance to trigger a sync in dips 539 | * 540 | * @param distance 541 | */ 542 | public void setDistanceToTriggerSync(int distance) { 543 | mTotalDragDistance = distance; 544 | } 545 | 546 | @Override 547 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 548 | final int width = getMeasuredWidth(); 549 | final int height = getMeasuredHeight(); 550 | if (getChildCount() == 0) { 551 | return; 552 | } 553 | if (mTarget == null) { 554 | ensureTarget(); 555 | } 556 | if (mTarget == null) { 557 | return; 558 | } 559 | final View child = mTarget; 560 | final int childLeft = getPaddingLeft(); 561 | final int childTop = getPaddingTop(); 562 | final int childWidth = width - getPaddingLeft() - getPaddingRight(); 563 | final int childHeight = height - getPaddingTop() - getPaddingBottom(); 564 | child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 565 | int circleWidth = mCircleView.getMeasuredWidth(); 566 | int circleHeight = mCircleView.getMeasuredHeight(); 567 | mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, 568 | (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); 569 | } 570 | 571 | @Override 572 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 573 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 574 | if (mTarget == null) { 575 | ensureTarget(); 576 | } 577 | if (mTarget == null) { 578 | return; 579 | } 580 | mTarget.measure(MeasureSpec.makeMeasureSpec( 581 | getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), 582 | MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( 583 | getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); 584 | mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), 585 | MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); 586 | if (!mUsingCustomStart && !mOriginalOffsetCalculated) { 587 | mOriginalOffsetCalculated = true; 588 | mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 589 | } 590 | mCircleViewIndex = -1; 591 | // Get the index of the circleview. 592 | for (int index = 0; index < getChildCount(); index++) { 593 | if (getChildAt(index) == mCircleView) { 594 | mCircleViewIndex = index; 595 | break; 596 | } 597 | } 598 | } 599 | 600 | /** 601 | * Get the diameter of the progress circle that is displayed as part of the 602 | * swipe to refresh layout. This is not valid until a measure pass has 603 | * completed. 604 | * 605 | * @return Diameter in pixels of the progress circle view. 606 | */ 607 | public int getProgressCircleDiameter() { 608 | return mCircleView != null ?mCircleView.getMeasuredHeight() : 0; 609 | } 610 | 611 | /** 612 | * @return Whether it is possible for the child view of this layout to 613 | * scroll up. Override this if the child view is a custom view. 614 | */ 615 | public boolean canChildScrollUp() { 616 | if (android.os.Build.VERSION.SDK_INT < 14) { 617 | if (mTarget instanceof AbsListView) { 618 | final AbsListView absListView = (AbsListView) mTarget; 619 | return absListView.getChildCount() > 0 620 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 621 | .getTop() < absListView.getPaddingTop()); 622 | } else { 623 | return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0; 624 | } 625 | } else { 626 | return ViewCompat.canScrollVertically(mTarget, -1); 627 | } 628 | } 629 | 630 | @Override 631 | public boolean onInterceptTouchEvent(MotionEvent ev) { 632 | ensureTarget(); 633 | 634 | final int action = MotionEventCompat.getActionMasked(ev); 635 | 636 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 637 | mReturningToStart = false; 638 | } 639 | 640 | if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { 641 | // Fail fast if we're not in a state where a swipe is possible 642 | return false; 643 | } 644 | 645 | switch (action) { 646 | case MotionEvent.ACTION_DOWN: 647 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); 648 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 649 | mIsBeingDragged = false; 650 | final float initialDownY = getMotionEventY(ev, mActivePointerId); 651 | if (initialDownY == -1) { 652 | return false; 653 | } 654 | mInitialDownY = initialDownY; 655 | break; 656 | 657 | case MotionEvent.ACTION_MOVE: 658 | if (mActivePointerId == INVALID_POINTER) { 659 | Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); 660 | return false; 661 | } 662 | 663 | final float y = getMotionEventY(ev, mActivePointerId); 664 | if (y == -1) { 665 | return false; 666 | } 667 | final float yDiff = y - mInitialDownY; 668 | if (yDiff > mTouchSlop && !mIsBeingDragged) { 669 | mInitialMotionY = mInitialDownY + mTouchSlop; 670 | mIsBeingDragged = true; 671 | mProgress.setAlpha(STARTING_PROGRESS_ALPHA); 672 | } 673 | break; 674 | 675 | case MotionEventCompat.ACTION_POINTER_UP: 676 | onSecondaryPointerUp(ev); 677 | break; 678 | 679 | case MotionEvent.ACTION_UP: 680 | case MotionEvent.ACTION_CANCEL: 681 | mIsBeingDragged = false; 682 | mActivePointerId = INVALID_POINTER; 683 | break; 684 | } 685 | 686 | return mIsBeingDragged; 687 | } 688 | 689 | private float getMotionEventY(MotionEvent ev, int activePointerId) { 690 | final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); 691 | if (index < 0) { 692 | return -1; 693 | } 694 | return MotionEventCompat.getY(ev, index); 695 | } 696 | 697 | @Override 698 | public void requestDisallowInterceptTouchEvent(boolean b) { 699 | // Nope. 700 | } 701 | 702 | private boolean isAnimationRunning(Animation animation) { 703 | return animation != null && animation.hasStarted() && !animation.hasEnded(); 704 | } 705 | 706 | @Override 707 | public boolean onTouchEvent(MotionEvent ev) { 708 | final int action = MotionEventCompat.getActionMasked(ev); 709 | 710 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 711 | mReturningToStart = false; 712 | } 713 | 714 | if (!isEnabled() || mReturningToStart || canChildScrollUp()) { 715 | // Fail fast if we're not in a state where a swipe is possible 716 | return false; 717 | } 718 | 719 | switch (action) { 720 | case MotionEvent.ACTION_DOWN: 721 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 722 | mIsBeingDragged = false; 723 | break; 724 | 725 | case MotionEvent.ACTION_MOVE: { 726 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 727 | if (pointerIndex < 0) { 728 | Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 729 | return false; 730 | } 731 | 732 | final float y = MotionEventCompat.getY(ev, pointerIndex); 733 | final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 734 | if (mIsBeingDragged) { 735 | mProgress.showArrow(true); 736 | float originalDragPercent = overscrollTop / mTotalDragDistance; 737 | if (originalDragPercent < 0) { 738 | return false; 739 | } 740 | float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); 741 | float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; 742 | float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; 743 | float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset 744 | - mOriginalOffsetTop : mSpinnerFinalOffset; 745 | float tensionSlingshotPercent = Math.max(0, 746 | Math.min(extraOS, slingshotDist * 2) / slingshotDist); 747 | float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( 748 | (tensionSlingshotPercent / 4), 2)) * 2f; 749 | float extraMove = (slingshotDist) * tensionPercent * 2; 750 | 751 | int targetY = mOriginalOffsetTop 752 | + (int) ((slingshotDist * dragPercent) + extraMove); 753 | // where 1.0f is a full circle 754 | if (mCircleView.getVisibility() != View.VISIBLE) { 755 | mCircleView.setVisibility(View.VISIBLE); 756 | } 757 | if (!mScale && !isAlphaUsedForScale()) { 758 | mCircleView.setScaleX(1f); 759 | mCircleView.setScaleY(1f); 760 | } 761 | if (overscrollTop < mTotalDragDistance) { 762 | if (mScale) { 763 | setAnimationProgress(overscrollTop / mTotalDragDistance); 764 | } 765 | if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA 766 | && !isAnimationRunning(mAlphaStartAnimation)) { 767 | // Animate the alpha 768 | startProgressAlphaStartAnimation(); 769 | } 770 | float strokeStart = adjustedPercent * .8f; 771 | mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); 772 | mProgress.setArrowScale(Math.min(1f, adjustedPercent)); 773 | } else { 774 | if (mProgress.getAlpha() < MAX_ALPHA 775 | && !isAnimationRunning(mAlphaMaxAnimation)) { 776 | // Animate the alpha 777 | startProgressAlphaMaxAnimation(); 778 | } 779 | } 780 | float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; 781 | mProgress.setProgressRotation(rotation); 782 | setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, 783 | true /* requires update */); 784 | } 785 | break; 786 | } 787 | case MotionEventCompat.ACTION_POINTER_DOWN: { 788 | final int index = MotionEventCompat.getActionIndex(ev); 789 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 790 | break; 791 | } 792 | 793 | case MotionEventCompat.ACTION_POINTER_UP: 794 | onSecondaryPointerUp(ev); 795 | break; 796 | 797 | case MotionEvent.ACTION_UP: 798 | case MotionEvent.ACTION_CANCEL: { 799 | if (mActivePointerId == INVALID_POINTER) { 800 | if (action == MotionEvent.ACTION_UP) { 801 | Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); 802 | } 803 | return false; 804 | } 805 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 806 | final float y = MotionEventCompat.getY(ev, pointerIndex); 807 | final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 808 | mIsBeingDragged = false; 809 | if (overscrollTop > mTotalDragDistance) { 810 | setRefreshing(true, true /* notify */); 811 | } else { 812 | // cancel refresh 813 | mRefreshing = false; 814 | mProgress.setStartEndTrim(0f, 0f); 815 | Animation.AnimationListener listener = null; 816 | if (!mScale) { 817 | listener = new Animation.AnimationListener() { 818 | 819 | @Override 820 | public void onAnimationStart(Animation animation) { 821 | } 822 | 823 | @Override 824 | public void onAnimationEnd(Animation animation) { 825 | if (!mScale) { 826 | startScaleDownAnimation(null); 827 | } 828 | } 829 | 830 | @Override 831 | public void onAnimationRepeat(Animation animation) { 832 | } 833 | 834 | }; 835 | } 836 | animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); 837 | mProgress.showArrow(false); 838 | } 839 | mActivePointerId = INVALID_POINTER; 840 | return false; 841 | } 842 | } 843 | 844 | return true; 845 | } 846 | 847 | private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { 848 | mFrom = from; 849 | mAnimateToCorrectPosition.reset(); 850 | mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); 851 | mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); 852 | if (listener != null) { 853 | mCircleView.setAnimationListener(listener); 854 | } 855 | mCircleView.clearAnimation(); 856 | mCircleView.startAnimation(mAnimateToCorrectPosition); 857 | } 858 | 859 | private void animateOffsetToStartPosition(int from, AnimationListener listener) { 860 | if (mScale) { 861 | // Scale the item back down 862 | startScaleDownReturnToStartAnimation(from, listener); 863 | } else { 864 | mFrom = from; 865 | mAnimateToStartPosition.reset(); 866 | mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); 867 | mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); 868 | if (listener != null) { 869 | mCircleView.setAnimationListener(listener); 870 | } 871 | mCircleView.clearAnimation(); 872 | mCircleView.startAnimation(mAnimateToStartPosition); 873 | } 874 | } 875 | 876 | private final Animation mAnimateToCorrectPosition = new Animation() { 877 | @Override 878 | public void applyTransformation(float interpolatedTime, Transformation t) { 879 | int targetTop = 0; 880 | int endTarget = 0; 881 | if (!mUsingCustomStart) { 882 | endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); 883 | } else { 884 | endTarget = (int) mSpinnerFinalOffset; 885 | } 886 | targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); 887 | int offset = targetTop - mCircleView.getTop(); 888 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 889 | mProgress.setArrowScale(1 - interpolatedTime); 890 | } 891 | }; 892 | 893 | private void moveToStart(float interpolatedTime) { 894 | int targetTop = 0; 895 | targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); 896 | int offset = targetTop - mCircleView.getTop(); 897 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 898 | } 899 | 900 | private final Animation mAnimateToStartPosition = new Animation() { 901 | @Override 902 | public void applyTransformation(float interpolatedTime, Transformation t) { 903 | moveToStart(interpolatedTime); 904 | } 905 | }; 906 | 907 | private void startScaleDownReturnToStartAnimation(int from, 908 | Animation.AnimationListener listener) { 909 | mFrom = from; 910 | if (isAlphaUsedForScale()) { 911 | mStartingScale = mProgress.getAlpha(); 912 | } else { 913 | mStartingScale = mCircleView.getScaleX(); 914 | } 915 | mScaleDownToStartAnimation = new Animation() { 916 | @Override 917 | public void applyTransformation(float interpolatedTime, Transformation t) { 918 | float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); 919 | setAnimationProgress(targetScale); 920 | moveToStart(interpolatedTime); 921 | } 922 | }; 923 | mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); 924 | if (listener != null) { 925 | mCircleView.setAnimationListener(listener); 926 | } 927 | mCircleView.clearAnimation(); 928 | mCircleView.startAnimation(mScaleDownToStartAnimation); 929 | } 930 | 931 | private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { 932 | mCircleView.bringToFront(); 933 | mCircleView.offsetTopAndBottom(offset); 934 | mCurrentTargetOffsetTop = mCircleView.getTop(); 935 | if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { 936 | invalidate(); 937 | } 938 | } 939 | 940 | private void onSecondaryPointerUp(MotionEvent ev) { 941 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 942 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 943 | if (pointerId == mActivePointerId) { 944 | // This was our active pointer going up. Choose a new 945 | // active pointer and adjust accordingly. 946 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 947 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 948 | } 949 | } 950 | 951 | /** 952 | * Classes that wish to be notified when the swipe gesture correctly 953 | * triggers a refresh should implement this interface. 954 | */ 955 | public interface OnRefreshListener { 956 | public void onRefresh(); 957 | } 958 | 959 | public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { 960 | final String TAG = "ViewCompat"; 961 | Method sChildrenDrawingOrderMethod = null; 962 | 963 | 964 | try { 965 | sChildrenDrawingOrderMethod = ViewGroup.class 966 | .getDeclaredMethod("setChildrenDrawingOrderEnabled", boolean.class); 967 | } catch (NoSuchMethodException e) { 968 | Log.e(TAG, "Unable to find childrenDrawingOrderEnabled", e); 969 | } 970 | sChildrenDrawingOrderMethod.setAccessible(true); 971 | 972 | try { 973 | sChildrenDrawingOrderMethod.invoke(viewGroup, enabled); 974 | } catch (IllegalAccessException e) { 975 | Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); 976 | } catch (IllegalArgumentException e) { 977 | Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); 978 | } catch (InvocationTargetException e) { 979 | Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); 980 | } 981 | } 982 | } -------------------------------------------------------------------------------- /android/src/com/rkam/swiperefreshlayout/SwipeRefreshProxy.java: -------------------------------------------------------------------------------- 1 | package com.rkam.swiperefreshlayout; 2 | 3 | import org.appcelerator.kroll.annotations.Kroll; 4 | import org.appcelerator.titanium.proxy.TiViewProxy; 5 | import org.appcelerator.titanium.view.TiUIView; 6 | 7 | import android.app.Activity; 8 | 9 | @Kroll.proxy(creatableInModule=SwiperefreshlayoutModule.class) 10 | public class SwipeRefreshProxy extends TiViewProxy { 11 | 12 | private SwipeRefresh swipeRefresh; 13 | 14 | public SwipeRefreshProxy() { 15 | super(); 16 | this.swipeRefresh = new SwipeRefresh(this); 17 | } 18 | 19 | @Override 20 | public TiUIView createView(Activity activity) { 21 | return this.swipeRefresh; 22 | } 23 | 24 | @Kroll.method 25 | public void setRefreshing(boolean refreshing) { 26 | if (this.swipeRefresh != null){ 27 | this.swipeRefresh.setRefreshing(refreshing); 28 | } 29 | } 30 | 31 | @Kroll.method 32 | public void add(TiViewProxy view) { 33 | this.swipeRefresh.add(view); 34 | return; 35 | } 36 | 37 | @Kroll.method @Kroll.getProperty 38 | public boolean isRefreshing() { 39 | if (this.swipeRefresh != null){ 40 | return this.swipeRefresh.isRefreshing(); 41 | } 42 | return false; 43 | } 44 | 45 | @Kroll.method 46 | public void setEnabled(boolean refreshEnabled) { 47 | if (this.swipeRefresh != null){ 48 | this.swipeRefresh.setEnabled(refreshEnabled); 49 | } 50 | return; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /android/src/com/rkam/swiperefreshlayout/SwiperefreshlayoutModule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by the Titanium Module SDK helper for Android 3 | * Appcelerator Titanium Mobile 4 | * Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved. 5 | * Licensed under the terms of the Apache Public License 6 | * Please see the LICENSE included with this distribution for details. 7 | * 8 | */ 9 | package com.rkam.swiperefreshlayout; 10 | 11 | import org.appcelerator.kroll.KrollModule; 12 | import org.appcelerator.kroll.annotations.Kroll; 13 | 14 | import org.appcelerator.titanium.TiApplication; 15 | import org.appcelerator.kroll.common.Log; 16 | 17 | @Kroll.module(name="Swiperefreshlayout", id="com.rkam.swiperefreshlayout") 18 | public class SwiperefreshlayoutModule extends KrollModule 19 | { 20 | 21 | // Standard Debugging variables 22 | private static final String TAG = "SwiperefreshlayoutModule"; 23 | 24 | // You can define constants with @Kroll.constant, for example: 25 | // @Kroll.constant public static final String EXTERNAL_NAME = value; 26 | 27 | public SwiperefreshlayoutModule() 28 | { 29 | super(); 30 | } 31 | 32 | @Kroll.onAppCreate 33 | public static void onAppCreate(TiApplication app) 34 | { 35 | Log.d(TAG, "inside onAppCreate"); 36 | // put module init code that needs to run when the application is created 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /android/timodule.xml: -------------------------------------------------------------------------------- 1 | 2 |