├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── README.md ├── SwipeRefreshLoadDemo.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zfeng │ │ └── swiperefreshload │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zfeng │ │ └── swiperefreshload │ │ ├── CircleImageView.java │ │ ├── ListViewDemoActivity.java │ │ ├── MaterialProgressDrawable.java │ │ ├── RecyclerViewDemoActivity.java │ │ └── SwipeRefreshLoadLayout.java │ └── res │ ├── layout │ ├── activity_main.xml │ └── activity_recycler.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── ids.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | TestSwipeRefreshLayout -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwipeRefreshLoadDemo 2 | 3 | 我写的新组件RecyclerViewPullRefresh #https://github.com/AndyZhaofeng/RecyclerViewPullRefresh# 4 | 5 | 6 | 改写的android.support.v4.widget.SwipeRefreshLayout类,使其可以上拉加载更多数据。其中对android.support.v7.widget.RecyclerView支持更好一些,当然也支持listview; 7 | 8 | 其中ListViewDemoActivity.java是ListView的例子,点击“菜单”选择“RecyclerViewDemo”进入RecyclerViewDemoActivity.java,RecyclerViewDemoActivity.java是RecyclerView的例子。 9 | -------------------------------------------------------------------------------- /SwipeRefreshLoadDemo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.yeahis.testswiperefreshlayout" 9 | minSdkVersion 15 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:21.0.3' 25 | compile 'com.android.support:recyclerview-v7:21.0.3' 26 | compile 'com.android.support:support-v4:21.0.3' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\android\adt-bundle-windows-x86_64-20140702\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/zfeng/swiperefreshload/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.zfeng.swiperefreshload; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/zfeng/swiperefreshload/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package com.zfeng.swiperefreshload; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.RadialGradient; 9 | import android.graphics.Shader; 10 | import android.graphics.drawable.ShapeDrawable; 11 | import android.graphics.drawable.shapes.OvalShape; 12 | import android.support.v4.view.ViewCompat; 13 | import android.view.animation.Animation; 14 | import android.widget.ImageView; 15 | 16 | /** 17 | * Created by zfeng on 2015-2-26. 18 | */ 19 | public class CircleImageView extends ImageView { 20 | 21 | private static final int KEY_SHADOW_COLOR = 0x1E000000; 22 | private static final int FILL_SHADOW_COLOR = 0x3D000000; 23 | // PX 24 | private static final float X_OFFSET = 0f; 25 | private static final float Y_OFFSET = 1.75f; 26 | private static final float SHADOW_RADIUS = 3.5f; 27 | private static final int SHADOW_ELEVATION = 4; 28 | 29 | private Animation.AnimationListener mListener; 30 | private int mShadowRadius; 31 | 32 | public CircleImageView(Context context, int color, final float radius) { 33 | super(context); 34 | final float density = getContext().getResources().getDisplayMetrics().density; 35 | final int diameter = (int) (radius * density * 2); 36 | final int shadowYOffset = (int) (density * Y_OFFSET); 37 | final int shadowXOffset = (int) (density * X_OFFSET); 38 | 39 | mShadowRadius = (int) (density * SHADOW_RADIUS); 40 | 41 | ShapeDrawable circle; 42 | if (elevationSupported()) { 43 | circle = new ShapeDrawable(new OvalShape()); 44 | ViewCompat.setElevation(this, SHADOW_ELEVATION * density); 45 | } else { 46 | OvalShape oval = new OvalShadow(mShadowRadius, diameter); 47 | circle = new ShapeDrawable(oval); 48 | ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); 49 | circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, 50 | KEY_SHADOW_COLOR); 51 | final int padding = (int) mShadowRadius; 52 | // set padding so the inner image sits correctly within the shadow. 53 | setPadding(padding, padding, padding, padding); 54 | } 55 | circle.getPaint().setColor(color); 56 | setBackgroundDrawable(circle); 57 | } 58 | 59 | private boolean elevationSupported() { 60 | return android.os.Build.VERSION.SDK_INT >= 21; 61 | } 62 | 63 | @Override 64 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 65 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 66 | if (!elevationSupported()) { 67 | setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight() 68 | + mShadowRadius*2); 69 | } 70 | } 71 | 72 | public void setAnimationListener(Animation.AnimationListener listener) { 73 | mListener = listener; 74 | } 75 | 76 | @Override 77 | public void onAnimationStart() { 78 | super.onAnimationStart(); 79 | if (mListener != null) { 80 | mListener.onAnimationStart(getAnimation()); 81 | } 82 | } 83 | 84 | @Override 85 | public void onAnimationEnd() { 86 | super.onAnimationEnd(); 87 | if (mListener != null) { 88 | mListener.onAnimationEnd(getAnimation()); 89 | } 90 | } 91 | 92 | /** 93 | * Update the background color of the circle image view. 94 | */ 95 | public void setBackgroundColor(int colorRes) { 96 | if (getBackground() instanceof ShapeDrawable) { 97 | final Resources res = getResources(); 98 | ((ShapeDrawable) getBackground()).getPaint().setColor(res.getColor(colorRes)); 99 | } 100 | } 101 | 102 | private class OvalShadow extends OvalShape { 103 | private RadialGradient mRadialGradient; 104 | private int mShadowRadius; 105 | private Paint mShadowPaint; 106 | private int mCircleDiameter; 107 | 108 | public OvalShadow(int shadowRadius, int circleDiameter) { 109 | super(); 110 | mShadowPaint = new Paint(); 111 | mShadowRadius = shadowRadius; 112 | mCircleDiameter = circleDiameter; 113 | mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, 114 | mShadowRadius, new int[] { 115 | FILL_SHADOW_COLOR, Color.TRANSPARENT 116 | }, null, Shader.TileMode.CLAMP); 117 | mShadowPaint.setShader(mRadialGradient); 118 | } 119 | 120 | @Override 121 | public void draw(Canvas canvas, Paint paint) { 122 | final int viewWidth = CircleImageView.this.getWidth(); 123 | final int viewHeight = CircleImageView.this.getHeight(); 124 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), 125 | mShadowPaint); 126 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zfeng/swiperefreshload/ListViewDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.zfeng.swiperefreshload; 2 | 3 | import android.content.Intent; 4 | import android.os.Handler; 5 | import android.support.v4.widget.SwipeRefreshLayout; 6 | import android.support.v7.app.ActionBarActivity; 7 | import android.os.Bundle; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.BaseAdapter; 14 | import android.widget.ListView; 15 | import android.widget.TextView; 16 | 17 | import java.util.ArrayList; 18 | 19 | public class ListViewDemoActivity extends ActionBarActivity 20 | { 21 | private SwipeRefreshLayout sRLayout; 22 | private SwipeRefreshLoadLayout swipeRefreshLayout; 23 | private ListView listView; 24 | private TestBaseAdapter baseAdapter; 25 | private String[] arrays; 26 | private ArrayList arrayList; 27 | 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) 31 | { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | swipeRefreshLayout=(SwipeRefreshLoadLayout)findViewById(R.id.main_swipe); 35 | listView=(ListView)findViewById(R.id.main_list); 36 | arrays=getResources().getStringArray(R.array.test_demo); 37 | arrayList=new ArrayList(); 38 | 39 | for(int i=0;i mAnimators = new ArrayList(); 86 | 87 | /** The indicator ring, used to manage animation state. */ 88 | private final Ring mRing; 89 | 90 | /** Canvas rotation in degrees. */ 91 | private float mRotation; 92 | 93 | /** Layout info for the arrowhead in dp */ 94 | private static final int ARROW_WIDTH = 10; 95 | private static final int ARROW_HEIGHT = 5; 96 | private static final float ARROW_OFFSET_ANGLE = 5; 97 | 98 | /** Layout info for the arrowhead for the large spinner in dp */ 99 | private static final int ARROW_WIDTH_LARGE = 12; 100 | private static final int ARROW_HEIGHT_LARGE = 6; 101 | private static final float MAX_PROGRESS_ARC = .8f; 102 | 103 | private Resources mResources; 104 | private View mParent; 105 | private Animation mAnimation; 106 | private float mRotationCount; 107 | private double mWidth; 108 | private double mHeight; 109 | boolean mFinishing; 110 | 111 | public MaterialProgressDrawable(Context context, View parent) { 112 | mParent = parent; 113 | mResources = context.getResources(); 114 | 115 | mRing = new Ring(mCallback); 116 | mRing.setColors(COLORS); 117 | 118 | updateSizes(DEFAULT); 119 | setupAnimators(); 120 | } 121 | 122 | private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 123 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 124 | final Ring ring = mRing; 125 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 126 | final float screenDensity = metrics.density; 127 | 128 | mWidth = progressCircleWidth * screenDensity; 129 | mHeight = progressCircleHeight * screenDensity; 130 | ring.setStrokeWidth((float) strokeWidth * screenDensity); 131 | ring.setCenterRadius(centerRadius * screenDensity); 132 | ring.setColorIndex(0); 133 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 134 | ring.setInsets((int) mWidth, (int) mHeight); 135 | } 136 | 137 | /** 138 | * Set the overall size for the progress spinner. This updates the radius 139 | * and stroke width of the ring. 140 | * 141 | */ 142 | public void updateSizes(@ProgressDrawableSize int size) { 143 | if (size == LARGE) { 144 | setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 145 | STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 146 | } else { 147 | setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 148 | ARROW_WIDTH, ARROW_HEIGHT); 149 | } 150 | } 151 | 152 | /** 153 | * @param show Set to true to display the arrowhead on the progress spinner. 154 | */ 155 | public void showArrow(boolean show) { 156 | mRing.setShowArrow(show); 157 | } 158 | 159 | /** 160 | * @param scale Set the scale of the arrowhead for the spinner. 161 | */ 162 | public void setArrowScale(float scale) { 163 | mRing.setArrowScale(scale); 164 | } 165 | 166 | /** 167 | * Set the start and end trim for the progress spinner arc. 168 | * 169 | * @param startAngle start angle 170 | * @param endAngle end angle 171 | */ 172 | public void setStartEndTrim(float startAngle, float endAngle) { 173 | mRing.setStartTrim(startAngle); 174 | mRing.setEndTrim(endAngle); 175 | } 176 | 177 | /** 178 | * Set the amount of rotation to apply to the progress spinner. 179 | * 180 | * @param rotation Rotation is from [0..1] 181 | */ 182 | public void setProgressRotation(float rotation) { 183 | mRing.setRotation(rotation); 184 | } 185 | 186 | /** 187 | * Update the background color of the circle image view. 188 | */ 189 | public void setBackgroundColor(int color) { 190 | mRing.setBackgroundColor(color); 191 | } 192 | 193 | /** 194 | * Set the colors used in the progress animation from color resources. 195 | * The first color will also be the color of the bar that grows in response 196 | * to a user swipe gesture. 197 | * 198 | * @param colors 199 | */ 200 | public void setColorSchemeColors(int... colors) { 201 | mRing.setColors(colors); 202 | mRing.setColorIndex(0); 203 | } 204 | 205 | @Override 206 | public int getIntrinsicHeight() { 207 | return (int) mHeight; 208 | } 209 | 210 | @Override 211 | public int getIntrinsicWidth() { 212 | return (int) mWidth; 213 | } 214 | 215 | @Override 216 | public void draw(Canvas c) { 217 | final Rect bounds = getBounds(); 218 | final int saveCount = c.save(); 219 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 220 | mRing.draw(c, bounds); 221 | c.restoreToCount(saveCount); 222 | } 223 | 224 | @Override 225 | public void setAlpha(int alpha) { 226 | mRing.setAlpha(alpha); 227 | } 228 | 229 | public int getAlpha() { 230 | return mRing.getAlpha(); 231 | } 232 | 233 | @Override 234 | public void setColorFilter(ColorFilter colorFilter) { 235 | mRing.setColorFilter(colorFilter); 236 | } 237 | 238 | @SuppressWarnings("unused") 239 | void setRotation(float rotation) { 240 | mRotation = rotation; 241 | invalidateSelf(); 242 | } 243 | 244 | @SuppressWarnings("unused") 245 | private float getRotation() { 246 | return mRotation; 247 | } 248 | 249 | @Override 250 | public int getOpacity() { 251 | return PixelFormat.TRANSLUCENT; 252 | } 253 | 254 | @Override 255 | public boolean isRunning() { 256 | final ArrayList animators = mAnimators; 257 | final int N = animators.size(); 258 | for (int i = 0; i < N; i++) { 259 | final Animation animator = animators.get(i); 260 | if (animator.hasStarted() && !animator.hasEnded()) { 261 | return true; 262 | } 263 | } 264 | return false; 265 | } 266 | 267 | @Override 268 | public void start() { 269 | mAnimation.reset(); 270 | mRing.storeOriginals(); 271 | // Already showing some part of the ring 272 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 273 | mFinishing = true; 274 | mAnimation.setDuration(ANIMATION_DURATION/2); 275 | mParent.startAnimation(mAnimation); 276 | } else { 277 | mRing.setColorIndex(0); 278 | mRing.resetOriginals(); 279 | mAnimation.setDuration(ANIMATION_DURATION); 280 | mParent.startAnimation(mAnimation); 281 | } 282 | } 283 | 284 | @Override 285 | public void stop() { 286 | mParent.clearAnimation(); 287 | setRotation(0); 288 | mRing.setShowArrow(false); 289 | mRing.setColorIndex(0); 290 | mRing.resetOriginals(); 291 | } 292 | 293 | private void applyFinishTranslation(float interpolatedTime, Ring ring) { 294 | // shrink back down and complete a full rotation before 295 | // starting other circles 296 | // Rotation goes between [0..1]. 297 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 298 | + 1f); 299 | final float startTrim = ring.getStartingStartTrim() 300 | + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) * interpolatedTime; 301 | ring.setStartTrim(startTrim); 302 | final float rotation = ring.getStartingRotation() 303 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 304 | ring.setRotation(rotation); 305 | } 306 | 307 | private void setupAnimators() { 308 | final Ring ring = mRing; 309 | final Animation animation = new Animation() { 310 | @Override 311 | public void applyTransformation(float interpolatedTime, Transformation t) { 312 | if (mFinishing) { 313 | applyFinishTranslation(interpolatedTime, ring); 314 | } else { 315 | // The minProgressArc is calculated from 0 to create an 316 | // angle that 317 | // matches the stroke width. 318 | final float minProgressArc = (float) Math.toRadians( 319 | ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); 320 | final float startingEndTrim = ring.getStartingEndTrim(); 321 | final float startingTrim = ring.getStartingStartTrim(); 322 | final float startingRotation = ring.getStartingRotation(); 323 | 324 | // Offset the minProgressArc to where the endTrim is 325 | // located. 326 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 327 | final float endTrim = startingEndTrim + (minArc 328 | * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 329 | ring.setEndTrim(endTrim); 330 | 331 | final float startTrim = startingTrim + (MAX_PROGRESS_ARC 332 | * END_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 333 | ring.setStartTrim(startTrim); 334 | 335 | final float rotation = startingRotation + (0.25f * interpolatedTime); 336 | ring.setRotation(rotation); 337 | 338 | float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 339 | + (720.0f * (mRotationCount / NUM_POINTS)); 340 | setRotation(groupRotation); 341 | } 342 | } 343 | }; 344 | animation.setRepeatCount(Animation.INFINITE); 345 | animation.setRepeatMode(Animation.RESTART); 346 | animation.setInterpolator(LINEAR_INTERPOLATOR); 347 | animation.setAnimationListener(new Animation.AnimationListener() { 348 | 349 | @Override 350 | public void onAnimationStart(Animation animation) { 351 | mRotationCount = 0; 352 | } 353 | 354 | @Override 355 | public void onAnimationEnd(Animation animation) { 356 | // do nothing 357 | } 358 | 359 | @Override 360 | public void onAnimationRepeat(Animation animation) { 361 | ring.storeOriginals(); 362 | ring.goToNextColor(); 363 | ring.setStartTrim(ring.getEndTrim()); 364 | if (mFinishing) { 365 | // finished closing the last ring from the swipe gesture; go 366 | // into progress mode 367 | mFinishing = false; 368 | animation.setDuration(ANIMATION_DURATION); 369 | ring.setShowArrow(false); 370 | } else { 371 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 372 | } 373 | } 374 | }); 375 | mAnimation = animation; 376 | } 377 | 378 | private final Callback mCallback = new Callback() { 379 | @Override 380 | public void invalidateDrawable(Drawable d) { 381 | invalidateSelf(); 382 | } 383 | 384 | @Override 385 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 386 | scheduleSelf(what, when); 387 | } 388 | 389 | @Override 390 | public void unscheduleDrawable(Drawable d, Runnable what) { 391 | unscheduleSelf(what); 392 | } 393 | }; 394 | 395 | private static class Ring { 396 | private final RectF mTempBounds = new RectF(); 397 | private final Paint mPaint = new Paint(); 398 | private final Paint mArrowPaint = new Paint(); 399 | 400 | private final Callback mCallback; 401 | 402 | private float mStartTrim = 0.0f; 403 | private float mEndTrim = 0.0f; 404 | private float mRotation = 0.0f; 405 | private float mStrokeWidth = 5.0f; 406 | private float mStrokeInset = 2.5f; 407 | 408 | private int[] mColors; 409 | // mColorIndex represents the offset into the available mColors that the 410 | // progress circle should currently display. As the progress circle is 411 | // animating, the mColorIndex moves by one to the next available color. 412 | private int mColorIndex; 413 | private float mStartingStartTrim; 414 | private float mStartingEndTrim; 415 | private float mStartingRotation; 416 | private boolean mShowArrow; 417 | private Path mArrow; 418 | private float mArrowScale; 419 | private double mRingCenterRadius; 420 | private int mArrowWidth; 421 | private int mArrowHeight; 422 | private int mAlpha; 423 | private final Paint mCirclePaint = new Paint(); 424 | private int mBackgroundColor; 425 | 426 | public Ring(Callback callback) { 427 | mCallback = callback; 428 | 429 | mPaint.setStrokeCap(Paint.Cap.SQUARE); 430 | mPaint.setAntiAlias(true); 431 | mPaint.setStyle(Style.STROKE); 432 | 433 | mArrowPaint.setStyle(Paint.Style.FILL); 434 | mArrowPaint.setAntiAlias(true); 435 | } 436 | 437 | public void setBackgroundColor(int color) { 438 | mBackgroundColor = color; 439 | } 440 | 441 | /** 442 | * Set the dimensions of the arrowhead. 443 | * 444 | * @param width Width of the hypotenuse of the arrow head 445 | * @param height Height of the arrow point 446 | */ 447 | public void setArrowDimensions(float width, float height) { 448 | mArrowWidth = (int) width; 449 | mArrowHeight = (int) height; 450 | } 451 | 452 | /** 453 | * Draw the progress spinner 454 | */ 455 | public void draw(Canvas c, Rect bounds) { 456 | final RectF arcBounds = mTempBounds; 457 | arcBounds.set(bounds); 458 | arcBounds.inset(mStrokeInset, mStrokeInset); 459 | 460 | final float startAngle = (mStartTrim + mRotation) * 360; 461 | final float endAngle = (mEndTrim + mRotation) * 360; 462 | float sweepAngle = endAngle - startAngle; 463 | 464 | mPaint.setColor(mColors[mColorIndex]); 465 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 466 | 467 | drawTriangle(c, startAngle, sweepAngle, bounds); 468 | 469 | if (mAlpha < 255) { 470 | mCirclePaint.setColor(mBackgroundColor); 471 | mCirclePaint.setAlpha(255 - mAlpha); 472 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 473 | mCirclePaint); 474 | } 475 | } 476 | 477 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 478 | if (mShowArrow) { 479 | if (mArrow == null) { 480 | mArrow = new android.graphics.Path(); 481 | mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); 482 | } else { 483 | mArrow.reset(); 484 | } 485 | 486 | // Adjust the position of the triangle so that it is inset as 487 | // much as the arc, but also centered on the arc. 488 | float inset = (int) mStrokeInset / 2 * mArrowScale; 489 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 490 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 491 | 492 | // Update the path each time. This works around an issue in SKIA 493 | // where concatenating a rotation matrix to a scale matrix 494 | // ignored a starting negative rotation. This appears to have 495 | // been fixed as of API 21. 496 | mArrow.moveTo(0, 0); 497 | mArrow.lineTo(mArrowWidth * mArrowScale, 0); 498 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 499 | * mArrowScale)); 500 | mArrow.offset(x - inset, y); 501 | mArrow.close(); 502 | // draw a triangle 503 | mArrowPaint.setColor(mColors[mColorIndex]); 504 | c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 505 | bounds.exactCenterY()); 506 | c.drawPath(mArrow, mArrowPaint); 507 | } 508 | } 509 | 510 | /** 511 | * Set the colors the progress spinner alternates between. 512 | * 513 | * @param colors Array of integers describing the colors. Must be non-null. 514 | */ 515 | public void setColors(@NonNull int[] colors) { 516 | mColors = colors; 517 | // if colors are reset, make sure to reset the color index as well 518 | setColorIndex(0); 519 | } 520 | 521 | /** 522 | * @param index Index into the color array of the color to display in 523 | * the progress spinner. 524 | */ 525 | public void setColorIndex(int index) { 526 | mColorIndex = index; 527 | } 528 | 529 | /** 530 | * Proceed to the next available ring color. This will automatically 531 | * wrap back to the beginning of colors. 532 | */ 533 | public void goToNextColor() { 534 | mColorIndex = (mColorIndex + 1) % (mColors.length); 535 | } 536 | 537 | public void setColorFilter(ColorFilter filter) { 538 | mPaint.setColorFilter(filter); 539 | invalidateSelf(); 540 | } 541 | 542 | /** 543 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 544 | */ 545 | public void setAlpha(int alpha) { 546 | mAlpha = alpha; 547 | } 548 | 549 | /** 550 | * @return Current alpha of the progress spinner and arrowhead. 551 | */ 552 | public int getAlpha() { 553 | return mAlpha; 554 | } 555 | 556 | /** 557 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 558 | */ 559 | public void setStrokeWidth(float strokeWidth) { 560 | mStrokeWidth = strokeWidth; 561 | mPaint.setStrokeWidth(strokeWidth); 562 | invalidateSelf(); 563 | } 564 | 565 | @SuppressWarnings("unused") 566 | public float getStrokeWidth() { 567 | return mStrokeWidth; 568 | } 569 | 570 | @SuppressWarnings("unused") 571 | public void setStartTrim(float startTrim) { 572 | mStartTrim = startTrim; 573 | invalidateSelf(); 574 | } 575 | 576 | @SuppressWarnings("unused") 577 | public float getStartTrim() { 578 | return mStartTrim; 579 | } 580 | 581 | public float getStartingStartTrim() { 582 | return mStartingStartTrim; 583 | } 584 | 585 | public float getStartingEndTrim() { 586 | return mStartingEndTrim; 587 | } 588 | 589 | @SuppressWarnings("unused") 590 | public void setEndTrim(float endTrim) { 591 | mEndTrim = endTrim; 592 | invalidateSelf(); 593 | } 594 | 595 | @SuppressWarnings("unused") 596 | public float getEndTrim() { 597 | return mEndTrim; 598 | } 599 | 600 | @SuppressWarnings("unused") 601 | public void setRotation(float rotation) { 602 | mRotation = rotation; 603 | invalidateSelf(); 604 | } 605 | 606 | @SuppressWarnings("unused") 607 | public float getRotation() { 608 | return mRotation; 609 | } 610 | 611 | public void setInsets(int width, int height) { 612 | final float minEdge = (float) Math.min(width, height); 613 | float insets; 614 | if (mRingCenterRadius <= 0 || minEdge < 0) { 615 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 616 | } else { 617 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 618 | } 619 | mStrokeInset = insets; 620 | } 621 | 622 | @SuppressWarnings("unused") 623 | public float getInsets() { 624 | return mStrokeInset; 625 | } 626 | 627 | /** 628 | * @param centerRadius Inner radius in px of the circle the progress 629 | * spinner arc traces. 630 | */ 631 | public void setCenterRadius(double centerRadius) { 632 | mRingCenterRadius = centerRadius; 633 | } 634 | 635 | public double getCenterRadius() { 636 | return mRingCenterRadius; 637 | } 638 | 639 | /** 640 | * @param show Set to true to show the arrow head on the progress spinner. 641 | */ 642 | public void setShowArrow(boolean show) { 643 | if (mShowArrow != show) { 644 | mShowArrow = show; 645 | invalidateSelf(); 646 | } 647 | } 648 | 649 | /** 650 | * @param scale Set the scale of the arrowhead for the spinner. 651 | */ 652 | public void setArrowScale(float scale) { 653 | if (scale != mArrowScale) { 654 | mArrowScale = scale; 655 | invalidateSelf(); 656 | } 657 | } 658 | 659 | /** 660 | * @return The amount the progress spinner is currently rotated, between [0..1]. 661 | */ 662 | public float getStartingRotation() { 663 | return mStartingRotation; 664 | } 665 | 666 | /** 667 | * If the start / end trim are offset to begin with, store them so that 668 | * animation starts from that offset. 669 | */ 670 | public void storeOriginals() { 671 | mStartingStartTrim = mStartTrim; 672 | mStartingEndTrim = mEndTrim; 673 | mStartingRotation = mRotation; 674 | } 675 | 676 | /** 677 | * Reset the progress spinner to default rotation, start and end angles. 678 | */ 679 | public void resetOriginals() { 680 | mStartingStartTrim = 0; 681 | mStartingEndTrim = 0; 682 | mStartingRotation = 0; 683 | setStartTrim(0); 684 | setEndTrim(0); 685 | setRotation(0); 686 | } 687 | 688 | private void invalidateSelf() { 689 | mCallback.invalidateDrawable(null); 690 | } 691 | } 692 | 693 | /** 694 | * Squishes the interpolation curve into the second half of the animation. 695 | */ 696 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 697 | @Override 698 | public float getInterpolation(float input) { 699 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 700 | } 701 | } 702 | 703 | /** 704 | * Squishes the interpolation curve into the first half of the animation. 705 | */ 706 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 707 | @Override 708 | public float getInterpolation(float input) { 709 | return super.getInterpolation(Math.min(1, input * 2.0f)); 710 | } 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /app/src/main/java/com/zfeng/swiperefreshload/RecyclerViewDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.zfeng.swiperefreshload; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.support.v7.app.ActionBarActivity; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.TextView; 12 | 13 | import java.util.ArrayList; 14 | 15 | /** 16 | * Created by zfeng on 2015-2-27. 17 | */ 18 | public class RecyclerViewDemoActivity extends ActionBarActivity 19 | { 20 | private SwipeRefreshLoadLayout swipeRefreshLayout; 21 | private RecyclerView recyclerView; 22 | 23 | private String[] arrays; 24 | private ArrayList arrayList; 25 | private DemoAdapter demoAdapter; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) 29 | { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_recycler); 32 | 33 | swipeRefreshLayout=(SwipeRefreshLoadLayout)findViewById(R.id.recycler_swipe); 34 | recyclerView=(RecyclerView)findViewById(R.id.recycler_list); 35 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 36 | 37 | arrays=getResources().getStringArray(R.array.test_demo); 38 | arrayList=new ArrayList(); 39 | 40 | for(int i=0;i<20;++i) 41 | { 42 | arrayList.add(i+""); 43 | } 44 | demoAdapter=new DemoAdapter(); 45 | recyclerView.setAdapter(demoAdapter); 46 | 47 | swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLoadLayout.OnRefreshListener() { 48 | @Override 49 | public void onRefresh() { 50 | refreshContent(); 51 | } 52 | }); 53 | swipeRefreshLayout.setLoadMoreListener(new SwipeRefreshLoadLayout.LoadMoreListener() 54 | { 55 | @Override 56 | public void loadMore() 57 | { 58 | loadMoreData(); 59 | } 60 | }); 61 | } 62 | 63 | private void refreshContent() 64 | { 65 | new Handler().postDelayed(new Runnable() 66 | { 67 | @Override 68 | public void run() 69 | { 70 | if(!arrayList.isEmpty()) 71 | { 72 | arrayList.clear(); 73 | } 74 | for(int i=0;i 102 | { 103 | class DemoViewHolder extends RecyclerView.ViewHolder 104 | { 105 | private TextView textView; 106 | 107 | public DemoViewHolder(View itemView) 108 | { 109 | super(itemView); 110 | this.textView=(TextView)itemView; 111 | } 112 | 113 | public void addDetails(int position) 114 | { 115 | textView.setText(arrayList.get(position)); 116 | } 117 | } 118 | 119 | @Override 120 | public DemoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 121 | { 122 | View view= LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1,parent,false); 123 | return new DemoViewHolder(view); 124 | } 125 | 126 | @Override 127 | public void onBindViewHolder(DemoViewHolder holder, int position) 128 | { 129 | holder.addDetails(position); 130 | } 131 | 132 | @Override 133 | public int getItemCount() 134 | { 135 | return arrayList.size(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/zfeng/swiperefreshload/SwipeRefreshLoadLayout.java: -------------------------------------------------------------------------------- 1 | package com.zfeng.swiperefreshload; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.TypedArray; 6 | import android.support.v4.view.MotionEventCompat; 7 | import android.support.v4.view.ViewCompat; 8 | import android.support.v4.widget.SwipeRefreshLayout; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.util.AttributeSet; 12 | import android.util.DisplayMetrics; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.view.ViewConfiguration; 17 | import android.view.ViewGroup; 18 | import android.view.animation.Animation; 19 | import android.view.animation.DecelerateInterpolator; 20 | import android.view.animation.Transformation; 21 | import android.widget.AbsListView; 22 | 23 | /** 24 | * Created by zfeng on 2015-2-26. 25 | */ 26 | public class SwipeRefreshLoadLayout extends ViewGroup { 27 | // Maps to ProgressBar.Large style 28 | public static final int LARGE = MaterialProgressDrawable.LARGE; 29 | // Maps to ProgressBar default style 30 | public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; 31 | 32 | private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); 33 | 34 | private static final int MAX_ALPHA = 255; 35 | private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); 36 | 37 | private static final int CIRCLE_DIAMETER = 40; 38 | private static final int CIRCLE_DIAMETER_LARGE = 56; 39 | 40 | private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; 41 | private static final int INVALID_POINTER = -1; 42 | private static final float DRAG_RATE = .5f; 43 | 44 | // Max amount of circle that can be filled by progress during swipe gesture, 45 | // where 1.0 is a full circle 46 | private static final float MAX_PROGRESS_ANGLE = .8f; 47 | 48 | private static final int SCALE_DOWN_DURATION = 150; 49 | 50 | private static final int ALPHA_ANIMATION_DURATION = 300; 51 | 52 | private static final int ANIMATE_TO_TRIGGER_DURATION = 200; 53 | 54 | private static final int ANIMATE_TO_START_DURATION = 200; 55 | 56 | // Default background for the progress spinner 57 | private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; 58 | // Default offset in dips from the top of the view to where the progress spinner should stop 59 | private static final int DEFAULT_CIRCLE_TARGET = 64; 60 | 61 | private View mTarget; // the target of the gesture 62 | private OnRefreshListener mListener; 63 | private LoadMoreListener mLoadMoreListener; 64 | private boolean mCanLoadMore=true; 65 | private boolean mRefreshing = false; 66 | private int mTouchSlop; 67 | private float mTotalDragDistance = -1; 68 | private int mMediumAnimationDuration; 69 | private int mCurrentTargetOffsetTop; 70 | // Whether or not the starting offset has been determined. 71 | private boolean mOriginalOffsetCalculated = false; 72 | 73 | private float mInitialMotionY; 74 | private boolean mIsBeingDragged; 75 | private int mActivePointerId = INVALID_POINTER; 76 | // Whether this item is scaled up rather than clipped 77 | private boolean mScale; 78 | 79 | // Target is returning to its start offset because it was cancelled or a 80 | // refresh was triggered. 81 | private boolean mReturningToStart; 82 | private final DecelerateInterpolator mDecelerateInterpolator; 83 | private static final int[] LAYOUT_ATTRS = new int[]{ 84 | android.R.attr.enabled 85 | }; 86 | 87 | private CircleImageView mCircleView; 88 | private int mCircleViewIndex = -1; 89 | 90 | protected int mFrom; 91 | 92 | private float mStartingScale; 93 | 94 | protected int mOriginalOffsetTop; 95 | 96 | private MaterialProgressDrawable mProgress; 97 | 98 | private Animation mScaleAnimation; 99 | 100 | private Animation mScaleDownAnimation; 101 | 102 | private Animation mAlphaStartAnimation; 103 | 104 | private Animation mAlphaMaxAnimation; 105 | 106 | private Animation mScaleDownToStartAnimation; 107 | 108 | private float mSpinnerFinalOffset; 109 | 110 | private boolean mNotify; 111 | 112 | private int mCircleWidth; 113 | 114 | private int mCircleHeight; 115 | 116 | // Whether the client has set a custom starting position; 117 | private boolean mUsingCustomStart; 118 | 119 | private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { 120 | @Override 121 | public void onAnimationStart(Animation animation) { 122 | } 123 | 124 | @Override 125 | public void onAnimationRepeat(Animation animation) { 126 | } 127 | 128 | @Override 129 | public void onAnimationEnd(Animation animation) { 130 | if (mRefreshing) { 131 | // Make sure the progress view is fully visible 132 | mProgress.setAlpha(MAX_ALPHA); 133 | mProgress.start(); 134 | if (mNotify) { 135 | if (mListener != null) { 136 | mListener.onRefresh(); 137 | } 138 | } 139 | } else { 140 | mProgress.stop(); 141 | mCircleView.setVisibility(View.GONE); 142 | setColorViewAlpha(MAX_ALPHA); 143 | // Return the circle to its start position 144 | if (mScale) { 145 | setAnimationProgress(0 /* animation complete and view is hidden */); 146 | } else { 147 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, 148 | true /* requires update */); 149 | } 150 | } 151 | mCurrentTargetOffsetTop = mCircleView.getTop(); 152 | } 153 | }; 154 | 155 | private void setColorViewAlpha(int targetAlpha) { 156 | mCircleView.getBackground().setAlpha(targetAlpha); 157 | mProgress.setAlpha(targetAlpha); 158 | } 159 | 160 | /** 161 | * The refresh indicator starting and resting position is always positioned 162 | * near the top of the refreshing content. This position is a consistent 163 | * location, but can be adjusted in either direction based on whether or not 164 | * there is a toolbar or actionbar present. 165 | * 166 | * @param scale Set to true if there is no view at a higher z-order than 167 | * where the progress spinner is set to appear. 168 | * @param start The offset in pixels from the top of this view at which the 169 | * progress spinner should appear. 170 | * @param end The offset in pixels from the top of this view at which the 171 | * progress spinner should come to rest after a successful swipe 172 | * gesture. 173 | */ 174 | public void setProgressViewOffset(boolean scale, int start, int end) { 175 | mScale = scale; 176 | mCircleView.setVisibility(View.GONE); 177 | mOriginalOffsetTop = mCurrentTargetOffsetTop = start; 178 | mSpinnerFinalOffset = end; 179 | mUsingCustomStart = true; 180 | mCircleView.invalidate(); 181 | } 182 | 183 | /** 184 | * The refresh indicator resting position is always positioned near the top 185 | * of the refreshing content. This position is a consistent location, but 186 | * can be adjusted in either direction based on whether or not there is a 187 | * toolbar or actionbar present. 188 | * 189 | * @param scale Set to true if there is no view at a higher z-order than 190 | * where the progress spinner is set to appear. 191 | * @param end The offset in pixels from the top of this view at which the 192 | * progress spinner should come to rest after a successful swipe 193 | * gesture. 194 | */ 195 | public void setProgressViewEndTarget(boolean scale, int end) { 196 | mSpinnerFinalOffset = end; 197 | mScale = scale; 198 | mCircleView.invalidate(); 199 | } 200 | 201 | /** 202 | * One of DEFAULT, or LARGE. 203 | */ 204 | public void setSize(int size) { 205 | if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { 206 | return; 207 | } 208 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 209 | if (size == MaterialProgressDrawable.LARGE) { 210 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); 211 | } else { 212 | mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 213 | } 214 | // force the bounds of the progress circle inside the circle view to 215 | // update by setting it to null before updating its size and then 216 | // re-setting it 217 | mCircleView.setImageDrawable(null); 218 | mProgress.updateSizes(size); 219 | mCircleView.setImageDrawable(mProgress); 220 | } 221 | 222 | /** 223 | * Simple constructor to use when creating a SwipeRefreshLayout from code. 224 | * 225 | * @param context 226 | */ 227 | public SwipeRefreshLoadLayout(Context context) { 228 | this(context, null); 229 | } 230 | 231 | /** 232 | * Constructor that is called when inflating SwipeRefreshLayout from XML. 233 | * 234 | * @param context 235 | * @param attrs 236 | */ 237 | public SwipeRefreshLoadLayout(Context context, AttributeSet attrs) { 238 | super(context, attrs); 239 | 240 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 241 | 242 | mMediumAnimationDuration = getResources().getInteger( 243 | android.R.integer.config_mediumAnimTime); 244 | 245 | setWillNotDraw(false); 246 | mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); 247 | 248 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 249 | setEnabled(a.getBoolean(0, true)); 250 | a.recycle(); 251 | 252 | final DisplayMetrics metrics = getResources().getDisplayMetrics(); 253 | mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 254 | mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); 255 | 256 | createProgressView(); 257 | ViewCompat.setChildrenDrawingOrderEnabled(this, true); 258 | // the absolute offset has to take into account that the circle starts at an offset 259 | mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; 260 | mTotalDragDistance = mSpinnerFinalOffset; 261 | } 262 | 263 | protected int getChildDrawingOrder(int childCount, int i) { 264 | if (mCircleViewIndex < 0) { 265 | return i; 266 | } else if (i == childCount - 1) { 267 | // Draw the selected child last 268 | return mCircleViewIndex; 269 | } else if (i >= mCircleViewIndex) { 270 | // Move the children after the selected child earlier one 271 | return i + 1; 272 | } else { 273 | // Keep the children before the selected child the same 274 | return i; 275 | } 276 | } 277 | 278 | private void createProgressView() { 279 | mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER / 2); 280 | mProgress = new MaterialProgressDrawable(getContext(), this); 281 | mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); 282 | mCircleView.setImageDrawable(mProgress); 283 | mCircleView.setVisibility(View.GONE); 284 | addView(mCircleView); 285 | } 286 | 287 | /** 288 | * Set the listener to be notified when a refresh is triggered via the swipe 289 | * gesture. 290 | */ 291 | public void setOnRefreshListener(OnRefreshListener listener) { 292 | mListener = listener; 293 | } 294 | public void setLoadMoreListener(LoadMoreListener listener){ 295 | mLoadMoreListener=listener; 296 | } 297 | 298 | /** 299 | * Pre API 11, alpha is used to make the progress circle appear instead of scale. 300 | */ 301 | private boolean isAlphaUsedForScale() { 302 | return android.os.Build.VERSION.SDK_INT < 11; 303 | } 304 | 305 | /** 306 | * Notify the widget that refresh state has changed. Do not call this when 307 | * refresh is triggered by a swipe gesture. 308 | * 309 | * @param refreshing Whether or not the view should show refresh progress. 310 | */ 311 | public void setRefreshing(boolean refreshing) { 312 | if (refreshing && mRefreshing != refreshing) { 313 | // scale and show 314 | mRefreshing = refreshing; 315 | int endTarget = 0; 316 | if (!mUsingCustomStart) { 317 | endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); 318 | } else { 319 | endTarget = (int) mSpinnerFinalOffset; 320 | } 321 | setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, 322 | true /* requires update */); 323 | mNotify = false; 324 | startScaleUpAnimation(mRefreshListener); 325 | } else { 326 | setRefreshing(refreshing, false /* notify */); 327 | } 328 | } 329 | public void setLoadMore(boolean load) 330 | { 331 | mCanLoadMore=!load; 332 | } 333 | 334 | private void startScaleUpAnimation(Animation.AnimationListener listener) { 335 | mCircleView.setVisibility(View.VISIBLE); 336 | if (android.os.Build.VERSION.SDK_INT >= 11) { 337 | // Pre API 11, alpha is used in place of scale up to show the 338 | // progress circle appearing. 339 | // Don't adjust the alpha during appearance otherwise. 340 | mProgress.setAlpha(MAX_ALPHA); 341 | } 342 | mScaleAnimation = new Animation() { 343 | @Override 344 | public void applyTransformation(float interpolatedTime, Transformation t) { 345 | setAnimationProgress(interpolatedTime); 346 | } 347 | }; 348 | mScaleAnimation.setDuration(mMediumAnimationDuration); 349 | if (listener != null) { 350 | mCircleView.setAnimationListener(listener); 351 | } 352 | mCircleView.clearAnimation(); 353 | mCircleView.startAnimation(mScaleAnimation); 354 | } 355 | 356 | /** 357 | * Pre API 11, this does an alpha animation. 358 | * 359 | * @param progress 360 | */ 361 | private void setAnimationProgress(float progress) { 362 | if (isAlphaUsedForScale()) { 363 | setColorViewAlpha((int) (progress * MAX_ALPHA)); 364 | } else { 365 | ViewCompat.setScaleX(mCircleView, progress); 366 | ViewCompat.setScaleY(mCircleView, progress); 367 | } 368 | } 369 | 370 | private void setRefreshing(boolean refreshing, final boolean notify) { 371 | if (mRefreshing != refreshing) { 372 | mNotify = notify; 373 | ensureTarget(); 374 | mRefreshing = refreshing; 375 | if (mRefreshing) { 376 | animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); 377 | } else { 378 | startScaleDownAnimation(mRefreshListener); 379 | } 380 | } 381 | } 382 | 383 | private void startScaleDownAnimation(Animation.AnimationListener listener) { 384 | mScaleDownAnimation = new Animation() { 385 | @Override 386 | public void applyTransformation(float interpolatedTime, Transformation t) { 387 | setAnimationProgress(1 - interpolatedTime); 388 | } 389 | }; 390 | mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); 391 | mCircleView.setAnimationListener(listener); 392 | mCircleView.clearAnimation(); 393 | mCircleView.startAnimation(mScaleDownAnimation); 394 | } 395 | 396 | private void startProgressAlphaStartAnimation() { 397 | mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); 398 | } 399 | 400 | private void startProgressAlphaMaxAnimation() { 401 | mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); 402 | } 403 | 404 | private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { 405 | // Pre API 11, alpha is used in place of scale. Don't also use it to 406 | // show the trigger point. 407 | if (mScale && isAlphaUsedForScale()) { 408 | return null; 409 | } 410 | Animation alpha = new Animation() { 411 | @Override 412 | public void applyTransformation(float interpolatedTime, Transformation t) { 413 | mProgress 414 | .setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) 415 | * interpolatedTime))); 416 | } 417 | }; 418 | alpha.setDuration(ALPHA_ANIMATION_DURATION); 419 | // Clear out the previous animation listeners. 420 | mCircleView.setAnimationListener(null); 421 | mCircleView.clearAnimation(); 422 | mCircleView.startAnimation(alpha); 423 | return alpha; 424 | } 425 | 426 | /** 427 | * Set the background color of the progress spinner disc. 428 | * 429 | * @param colorRes Resource id of the color. 430 | */ 431 | public void setProgressBackgroundColor(int colorRes) { 432 | mCircleView.setBackgroundColor(colorRes); 433 | mProgress.setBackgroundColor(getResources().getColor(colorRes)); 434 | } 435 | 436 | /** 437 | * @deprecated Use {@link #setColorSchemeResources(int...)} 438 | */ 439 | @Deprecated 440 | public void setColorScheme(int... colors) { 441 | setColorSchemeResources(colors); 442 | } 443 | 444 | /** 445 | * Set the color resources used in the progress animation from color resources. 446 | * The first color will also be the color of the bar that grows in response 447 | * to a user swipe gesture. 448 | * 449 | * @param colorResIds 450 | */ 451 | public void setColorSchemeResources(int... colorResIds) { 452 | final Resources res = getResources(); 453 | int[] colorRes = new int[colorResIds.length]; 454 | for (int i = 0; i < colorResIds.length; i++) { 455 | colorRes[i] = res.getColor(colorResIds[i]); 456 | } 457 | setColorSchemeColors(colorRes); 458 | } 459 | 460 | /** 461 | * Set the colors used in the progress animation. The first 462 | * color will also be the color of the bar that grows in response to a user 463 | * swipe gesture. 464 | * 465 | * @param colors 466 | */ 467 | public void setColorSchemeColors(int... colors) { 468 | ensureTarget(); 469 | mProgress.setColorSchemeColors(colors); 470 | } 471 | 472 | /** 473 | * @return Whether the SwipeRefreshWidget is actively showing refresh 474 | * progress. 475 | */ 476 | public boolean isRefreshing() { 477 | return mRefreshing; 478 | } 479 | 480 | private void ensureTarget() { 481 | // Don't bother getting the parent height if the parent hasn't been laid 482 | // out yet. 483 | if (mTarget == null) { 484 | for (int i = 0; i < getChildCount(); i++) { 485 | View child = getChildAt(i); 486 | if (!child.equals(mCircleView)) { 487 | mTarget = child; 488 | break; 489 | } 490 | } 491 | } 492 | } 493 | 494 | /** 495 | * Set the distance to trigger a sync in dips 496 | * 497 | * @param distance 498 | */ 499 | public void setDistanceToTriggerSync(int distance) { 500 | mTotalDragDistance = distance; 501 | } 502 | 503 | @Override 504 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 505 | final int width = getMeasuredWidth(); 506 | final int height = getMeasuredHeight(); 507 | if (getChildCount() == 0) { 508 | return; 509 | } 510 | if (mTarget == null) { 511 | ensureTarget(); 512 | } 513 | if (mTarget == null) { 514 | return; 515 | } 516 | final View child = mTarget; 517 | final int childLeft = getPaddingLeft(); 518 | final int childTop = getPaddingTop(); 519 | final int childWidth = width - getPaddingLeft() - getPaddingRight(); 520 | final int childHeight = height - getPaddingTop() - getPaddingBottom(); 521 | child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 522 | int circleWidth = mCircleView.getMeasuredWidth(); 523 | int circleHeight = mCircleView.getMeasuredHeight(); 524 | mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, 525 | (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); 526 | } 527 | 528 | @Override 529 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 530 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 531 | if (mTarget == null) { 532 | ensureTarget(); 533 | } 534 | if (mTarget == null) { 535 | return; 536 | } 537 | mTarget.measure(MeasureSpec.makeMeasureSpec( 538 | getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), 539 | MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( 540 | getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); 541 | mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), 542 | MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); 543 | if (!mUsingCustomStart && !mOriginalOffsetCalculated) { 544 | mOriginalOffsetCalculated = true; 545 | mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 546 | } 547 | mCircleViewIndex = -1; 548 | // Get the index of the circleview. 549 | for (int index = 0; index < getChildCount(); index++) { 550 | if (getChildAt(index) == mCircleView) { 551 | mCircleViewIndex = index; 552 | break; 553 | } 554 | } 555 | } 556 | 557 | /** 558 | * @return Whether it is possible for the child view of this layout to 559 | * scroll up. Override this if the child view is a custom view. 560 | */ 561 | public boolean canChildScrollUp() { 562 | if (android.os.Build.VERSION.SDK_INT < 14) { 563 | if (mTarget instanceof AbsListView) { 564 | final AbsListView absListView = (AbsListView) mTarget; 565 | return absListView.getChildCount() > 0 566 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 567 | .getTop() < absListView.getPaddingTop()); 568 | } else { 569 | return mTarget.getScrollY() > 0; 570 | } 571 | } else { 572 | boolean bool = ViewCompat.canScrollVertically(mTarget, -1); 573 | return bool; 574 | } 575 | } 576 | 577 | @Override 578 | public boolean onInterceptTouchEvent(MotionEvent ev) { 579 | ensureTarget(); 580 | 581 | final int action = MotionEventCompat.getActionMasked(ev); 582 | 583 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 584 | mReturningToStart = false; 585 | } 586 | 587 | if (!isEnabled() || mReturningToStart || mRefreshing) { 588 | // Fail fast if we're not in a state where a swipe is possible 589 | return false; 590 | } 591 | 592 | switch (action) { 593 | case MotionEvent.ACTION_DOWN: 594 | if (canChildScrollUp()) { 595 | } else { 596 | setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); 597 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 598 | mIsBeingDragged = false; 599 | final float initialMotionY = getMotionEventY(ev, mActivePointerId); 600 | if (initialMotionY == -1) { 601 | return false; 602 | } 603 | mInitialMotionY = initialMotionY; 604 | } 605 | 606 | case MotionEvent.ACTION_MOVE: 607 | if (canChildScrollUp()) { 608 | } else { 609 | if (mActivePointerId == INVALID_POINTER) { 610 | Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); 611 | return false; 612 | } 613 | 614 | final float y = getMotionEventY(ev, mActivePointerId); 615 | if (y == -1) { 616 | return false; 617 | } 618 | final float yDiff = y - mInitialMotionY; 619 | if (yDiff > mTouchSlop && !mIsBeingDragged) { 620 | mIsBeingDragged = true; 621 | mProgress.setAlpha(STARTING_PROGRESS_ALPHA); 622 | } 623 | break; 624 | } 625 | 626 | case MotionEventCompat.ACTION_POINTER_UP: 627 | if (canChildScrollUp()) { 628 | } else { 629 | onSecondaryPointerUp(ev); 630 | break; 631 | } 632 | 633 | case MotionEvent.ACTION_UP: 634 | case MotionEvent.ACTION_CANCEL: 635 | if (canChildScrollUp()) 636 | { 637 | if(mTarget instanceof AbsListView) 638 | { 639 | final AbsListView absListView = (AbsListView) mTarget; 640 | 641 | int lastPosition=absListView.getLastVisiblePosition(); 642 | if(lastPosition+1==absListView.getCount()) 643 | { 644 | if(mCanLoadMore) 645 | { 646 | mCanLoadMore=false; 647 | mLoadMoreListener.loadMore(); 648 | } 649 | } 650 | }else if(mTarget instanceof RecyclerView) 651 | { 652 | final RecyclerView recyclerView=(RecyclerView)mTarget; 653 | LinearLayoutManager lLManager=(LinearLayoutManager)recyclerView.getLayoutManager(); 654 | if((lLManager.findLastVisibleItemPosition()+1)==lLManager.getItemCount()) 655 | { 656 | if(mCanLoadMore) 657 | { 658 | mCanLoadMore=false; 659 | mLoadMoreListener.loadMore(); 660 | } 661 | } 662 | } 663 | } else { 664 | mIsBeingDragged = false; 665 | mActivePointerId = INVALID_POINTER; 666 | break; 667 | } 668 | } 669 | 670 | return mIsBeingDragged; 671 | } 672 | 673 | private float getMotionEventY(MotionEvent ev, int activePointerId) { 674 | final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); 675 | if (index < 0) { 676 | return -1; 677 | } 678 | return MotionEventCompat.getY(ev, index); 679 | } 680 | 681 | @Override 682 | public void requestDisallowInterceptTouchEvent(boolean b) { 683 | // Nope. 684 | } 685 | 686 | private boolean isAnimationRunning(Animation animation) { 687 | return animation != null && animation.hasStarted() && !animation.hasEnded(); 688 | } 689 | 690 | @Override 691 | public boolean onTouchEvent(MotionEvent ev) { 692 | final int action = MotionEventCompat.getActionMasked(ev); 693 | 694 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 695 | mReturningToStart = false; 696 | } 697 | 698 | if (!isEnabled() || mReturningToStart || canChildScrollUp()) { 699 | // Fail fast if we're not in a state where a swipe is possible 700 | return false; 701 | } 702 | 703 | switch (action) { 704 | case MotionEvent.ACTION_DOWN: 705 | 706 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 707 | mIsBeingDragged = false; 708 | break; 709 | 710 | case MotionEvent.ACTION_MOVE: { 711 | 712 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 713 | if (pointerIndex < 0) { 714 | Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 715 | return false; 716 | } 717 | 718 | final float y = MotionEventCompat.getY(ev, pointerIndex); 719 | final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 720 | if (mIsBeingDragged) { 721 | mProgress.showArrow(true); 722 | float originalDragPercent = overscrollTop / mTotalDragDistance; 723 | if (originalDragPercent < 0) { 724 | return false; 725 | } 726 | float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); 727 | float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; 728 | float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; 729 | float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset 730 | - mOriginalOffsetTop : mSpinnerFinalOffset; 731 | float tensionSlingshotPercent = Math.max(0, 732 | Math.min(extraOS, slingshotDist * 2) / slingshotDist); 733 | float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( 734 | (tensionSlingshotPercent / 4), 2)) * 2f; 735 | float extraMove = (slingshotDist) * tensionPercent * 2; 736 | 737 | int targetY = mOriginalOffsetTop 738 | + (int) ((slingshotDist * dragPercent) + extraMove); 739 | // where 1.0f is a full circle 740 | if (mCircleView.getVisibility() != View.VISIBLE) { 741 | mCircleView.setVisibility(View.VISIBLE); 742 | } 743 | if (!mScale) { 744 | ViewCompat.setScaleX(mCircleView, 1f); 745 | ViewCompat.setScaleY(mCircleView, 1f); 746 | } 747 | if (overscrollTop < mTotalDragDistance) { 748 | if (mScale) { 749 | setAnimationProgress(overscrollTop / mTotalDragDistance); 750 | } 751 | if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA 752 | && !isAnimationRunning(mAlphaStartAnimation)) { 753 | // Animate the alpha 754 | startProgressAlphaStartAnimation(); 755 | } 756 | float strokeStart = (float) (adjustedPercent * .8f); 757 | mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); 758 | mProgress.setArrowScale(Math.min(1f, adjustedPercent)); 759 | } else { 760 | if (mProgress.getAlpha() < MAX_ALPHA 761 | && !isAnimationRunning(mAlphaMaxAnimation)) { 762 | // Animate the alpha 763 | startProgressAlphaMaxAnimation(); 764 | } 765 | } 766 | float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; 767 | mProgress.setProgressRotation(rotation); 768 | setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, 769 | true /* requires update */); 770 | } 771 | break; 772 | } 773 | case MotionEventCompat.ACTION_POINTER_DOWN: { 774 | 775 | final int index = MotionEventCompat.getActionIndex(ev); 776 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 777 | break; 778 | } 779 | 780 | case MotionEventCompat.ACTION_POINTER_UP: 781 | 782 | onSecondaryPointerUp(ev); 783 | break; 784 | 785 | case MotionEvent.ACTION_UP: 786 | case MotionEvent.ACTION_CANCEL: { 787 | 788 | if (mActivePointerId == INVALID_POINTER) { 789 | if (action == MotionEvent.ACTION_UP) { 790 | Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); 791 | } 792 | return false; 793 | } 794 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 795 | final float y = MotionEventCompat.getY(ev, pointerIndex); 796 | final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 797 | mIsBeingDragged = false; 798 | if (overscrollTop > mTotalDragDistance) { 799 | setRefreshing(true, true /* notify */); 800 | } else { 801 | // cancel refresh 802 | mRefreshing = false; 803 | mProgress.setStartEndTrim(0f, 0f); 804 | Animation.AnimationListener listener = null; 805 | if (!mScale) { 806 | listener = new Animation.AnimationListener() { 807 | 808 | @Override 809 | public void onAnimationStart(Animation animation) { 810 | } 811 | 812 | @Override 813 | public void onAnimationEnd(Animation animation) { 814 | if (!mScale) { 815 | startScaleDownAnimation(null); 816 | } 817 | } 818 | 819 | @Override 820 | public void onAnimationRepeat(Animation animation) { 821 | } 822 | 823 | }; 824 | } 825 | animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); 826 | mProgress.showArrow(false); 827 | } 828 | mActivePointerId = INVALID_POINTER; 829 | return false; 830 | } 831 | 832 | } 833 | 834 | return true; 835 | } 836 | 837 | private void animateOffsetToCorrectPosition(int from, Animation.AnimationListener listener) { 838 | mFrom = from; 839 | mAnimateToCorrectPosition.reset(); 840 | mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); 841 | mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); 842 | if (listener != null) { 843 | mCircleView.setAnimationListener(listener); 844 | } 845 | mCircleView.clearAnimation(); 846 | mCircleView.startAnimation(mAnimateToCorrectPosition); 847 | } 848 | 849 | private void animateOffsetToStartPosition(int from, Animation.AnimationListener listener) { 850 | if (mScale) { 851 | // Scale the item back down 852 | startScaleDownReturnToStartAnimation(from, listener); 853 | } else { 854 | mFrom = from; 855 | mAnimateToStartPosition.reset(); 856 | mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); 857 | mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); 858 | if (listener != null) { 859 | mCircleView.setAnimationListener(listener); 860 | } 861 | mCircleView.clearAnimation(); 862 | mCircleView.startAnimation(mAnimateToStartPosition); 863 | } 864 | } 865 | 866 | private final Animation mAnimateToCorrectPosition = new Animation() { 867 | @Override 868 | public void applyTransformation(float interpolatedTime, Transformation t) { 869 | int targetTop = 0; 870 | int endTarget = 0; 871 | if (!mUsingCustomStart) { 872 | endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); 873 | } else { 874 | endTarget = (int) mSpinnerFinalOffset; 875 | } 876 | targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); 877 | int offset = targetTop - mCircleView.getTop(); 878 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 879 | } 880 | }; 881 | 882 | private void moveToStart(float interpolatedTime) { 883 | int targetTop = 0; 884 | targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); 885 | int offset = targetTop - mCircleView.getTop(); 886 | setTargetOffsetTopAndBottom(offset, false /* requires update */); 887 | } 888 | 889 | private final Animation mAnimateToStartPosition = new Animation() { 890 | @Override 891 | public void applyTransformation(float interpolatedTime, Transformation t) { 892 | moveToStart(interpolatedTime); 893 | } 894 | }; 895 | 896 | private void startScaleDownReturnToStartAnimation(int from, 897 | Animation.AnimationListener listener) { 898 | mFrom = from; 899 | if (isAlphaUsedForScale()) { 900 | mStartingScale = mProgress.getAlpha(); 901 | } else { 902 | mStartingScale = ViewCompat.getScaleX(mCircleView); 903 | } 904 | mScaleDownToStartAnimation = new Animation() { 905 | @Override 906 | public void applyTransformation(float interpolatedTime, Transformation t) { 907 | float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); 908 | setAnimationProgress(targetScale); 909 | moveToStart(interpolatedTime); 910 | } 911 | }; 912 | mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); 913 | if (listener != null) { 914 | mCircleView.setAnimationListener(listener); 915 | } 916 | mCircleView.clearAnimation(); 917 | mCircleView.startAnimation(mScaleDownToStartAnimation); 918 | } 919 | 920 | private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { 921 | mCircleView.bringToFront(); 922 | mCircleView.offsetTopAndBottom(offset); 923 | mCurrentTargetOffsetTop = mCircleView.getTop(); 924 | if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { 925 | invalidate(); 926 | } 927 | } 928 | 929 | private void onSecondaryPointerUp(MotionEvent ev) { 930 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 931 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 932 | if (pointerId == mActivePointerId) { 933 | // This was our active pointer going up. Choose a new 934 | // active pointer and adjust accordingly. 935 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 936 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 937 | } 938 | int test = mActivePointerId; 939 | } 940 | 941 | /** 942 | * Classes that wish to be notified when the swipe gesture correctly 943 | * triggers a refresh should implement this interface. 944 | */ 945 | public interface OnRefreshListener { 946 | public void onRefresh(); 947 | } 948 | 949 | /** 950 | * @author zfeng 951 | * Classes that wish to be notified when the swipe gesture correctly 952 | * triggers a load should implement this interface. 953 | */ 954 | public interface LoadMoreListener{ 955 | public void loadMore(); 956 | } 957 | } 958 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_recycler.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyZhaofeng/SwipeRefreshLoadDemo/4a8f496a2a52af88a5c055f379072499cf114284/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyZhaofeng/SwipeRefreshLoadDemo/4a8f496a2a52af88a5c055f379072499cf114284/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyZhaofeng/SwipeRefreshLoadDemo/4a8f496a2a52af88a5c055f379072499cf114284/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyZhaofeng/SwipeRefreshLoadDemo/4a8f496a2a52af88a5c055f379072499cf114284/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #28a9ea 4 | #212121 5 | #7FFF00 6 | #8A2BE2 7 | #7FFFD4 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TestSwipeRefreshLayout 3 | 4 | Hello world! 5 | Settings 6 | 7 | 8 | a 9 | b 10 | c 11 | d 12 | e 13 | f 14 | g 15 | h 16 | i 17 | j 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 28 | 29 | 30 | 38 | 39 | 40 | 45 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyZhaofeng/SwipeRefreshLoadDemo/4a8f496a2a52af88a5c055f379072499cf114284/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------