├── DemoAndroid ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── org │ │ │ └── cgsdream │ │ │ └── demo_android │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── org │ │ │ │ └── cgsdream │ │ │ │ └── demo_android │ │ │ │ ├── MainActivity.java │ │ │ │ ├── WWLoadingView.java │ │ │ │ ├── WWPullRefreshLayout.java │ │ │ │ └── qmui │ │ │ │ ├── QMUIDisplayHelper.java │ │ │ │ ├── QMUIMaterialProgressDrawable.java │ │ │ │ └── QMUIPullRefreshLayout.java │ │ └── res │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ │ └── values │ │ │ ├── attr.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── org │ │ └── cgsdream │ │ └── demo_android │ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── Demo_iOS ├── Demo_iOS.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── cgspine.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── cgspine.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ ├── Demo_iOS.xcscheme │ │ └── xcschememanagement.plist └── Demo_iOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── ResuableView.swift │ ├── SMRefreshComponent.swift │ ├── SMRefreshHeader.swift │ ├── UIScrollView+SM.swift │ ├── UITableView+SM.swift │ ├── UIView+SM.swift │ ├── ViewController.swift │ └── WWRefreshView.swift └── README.md /DemoAndroid/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /DemoAndroid/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /DemoAndroid/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "org.cgsdream.demo_android" 8 | minSdkVersion 16 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 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 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.1.0' 28 | compile 'com.android.support:design:25.1.0' 29 | testCompile 'junit:junit:4.12' 30 | } 31 | -------------------------------------------------------------------------------- /DemoAndroid/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 /Users/cgspine/Library/Android/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 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/androidTest/java/org/cgsdream/demo_android/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("org.cgsdream.demo_android", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | 7 | import java.lang.ref.WeakReference; 8 | 9 | public class MainActivity extends AppCompatActivity { 10 | private static final String TAG = "MainActivity"; 11 | private WWPullRefreshLayout mPullRefreshLayout; 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | mPullRefreshLayout = (WWPullRefreshLayout) findViewById(R.id.pull_to_refresh); 18 | mPullRefreshLayout.setOnPullListener(new WWPullRefreshLayout.OnPullListener() { 19 | @Override 20 | public void onMoveTarget(int offset) { 21 | 22 | } 23 | 24 | @Override 25 | public void onMoveRefreshView(int offset) { 26 | 27 | } 28 | 29 | @Override 30 | public void onRefresh() { 31 | Log.w(TAG, "onPullDownToRefresh start"); 32 | final WeakReference pullToRefreshLayout = new WeakReference<>(mPullRefreshLayout); 33 | if (pullToRefreshLayout.get() != null) { 34 | pullToRefreshLayout.get().postDelayed(new Runnable() { 35 | @Override 36 | public void run() { 37 | pullToRefreshLayout.get().finishRefresh(); 38 | } 39 | }, 3000); 40 | } 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/WWLoadingView.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.content.Context; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Canvas; 8 | import android.graphics.Paint; 9 | import android.graphics.Path; 10 | import android.os.Build; 11 | import android.support.annotation.ColorInt; 12 | import android.support.annotation.ColorRes; 13 | import android.util.AttributeSet; 14 | import android.view.View; 15 | import android.view.animation.AccelerateDecelerateInterpolator; 16 | 17 | import org.cgsdream.demo_android.qmui.QMUIDisplayHelper; 18 | import org.cgsdream.demo_android.qmui.QMUIPullRefreshLayout; 19 | 20 | /** 21 | * Created by cgine on 17/1/18. 22 | */ 23 | 24 | public class WWLoadingView extends View implements QMUIPullRefreshLayout.IRefreshView { 25 | private static final int DURATION = 600; 26 | private Ball[] mBalls = new Ball[4]; 27 | private int mSize; 28 | private ValueAnimator mAnimator; 29 | private float mOriginInset; 30 | private float mCurrentPercent = 0; 31 | 32 | public WWLoadingView(Context context, int size) { 33 | super(context); 34 | mSize = size; 35 | init(context); 36 | } 37 | 38 | public WWLoadingView(Context context, AttributeSet attrs) { 39 | super(context, attrs); 40 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.WWLoadingView); 41 | mSize = array.getDimensionPixelSize(R.styleable.WWLoadingView_loading_size, 0); 42 | array.recycle(); 43 | 44 | init(context); 45 | } 46 | 47 | private void init(Context context) { 48 | mOriginInset = QMUIDisplayHelper.getDensity(context) * 3.5f; 49 | float ballRadius = mOriginInset; 50 | float ballSmallRadius = QMUIDisplayHelper.getDensity(context) * 1.5f; 51 | 52 | OriginPoint op1 = new OriginPoint(mSize / 2, mOriginInset); 53 | OriginPoint op2 = new OriginPoint(mOriginInset, mSize / 2); 54 | OriginPoint op3 = new OriginPoint(mSize / 2, mSize - mOriginInset); 55 | OriginPoint op4 = new OriginPoint(mSize - mOriginInset, mSize / 2); 56 | op1.setNext(op2); 57 | op2.setNext(op3); 58 | op3.setNext(op4); 59 | op4.setNext(op1); 60 | 61 | mBalls[0] = new Ball(ballRadius, ballSmallRadius, 0xFF0082EF, op1); 62 | mBalls[1] = new Ball(ballRadius, ballSmallRadius, 0xFF2DBC00, op2); 63 | mBalls[2] = new Ball(ballRadius, ballSmallRadius, 0xFFFFCC00, op3); 64 | mBalls[3] = new Ball(ballRadius, ballSmallRadius, 0xFFFB6500, op4); 65 | } 66 | 67 | private void startAnim() { 68 | stopAnim(); 69 | mAnimator = ValueAnimator.ofFloat(0, 1); 70 | mAnimator.setDuration(DURATION); 71 | mAnimator.setRepeatMode(ValueAnimator.RESTART); 72 | mAnimator.setRepeatCount(ValueAnimator.INFINITE); 73 | mAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 74 | mAnimator.setCurrentPlayTime((long) (DURATION * mCurrentPercent)); 75 | mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 76 | @Override 77 | public void onAnimationUpdate(ValueAnimator animation) { 78 | mCurrentPercent = (Float) animation.getAnimatedValue(); 79 | for (int i = 0; i < mBalls.length; i++) { 80 | Ball ball = mBalls[i]; 81 | ball.calculate(mCurrentPercent); 82 | } 83 | invalidate(); 84 | } 85 | }); 86 | mAnimator.addListener(new Animator.AnimatorListener() { 87 | @Override 88 | public void onAnimationStart(Animator animation) { 89 | 90 | } 91 | 92 | @Override 93 | public void onAnimationEnd(Animator animation) { 94 | 95 | } 96 | 97 | @Override 98 | public void onAnimationCancel(Animator animation) { 99 | 100 | } 101 | 102 | @Override 103 | public void onAnimationRepeat(Animator animation) { 104 | setToNextPosition(); 105 | } 106 | }); 107 | mAnimator.start(); 108 | } 109 | 110 | private void stopAnim() { 111 | if (mAnimator != null) { 112 | mAnimator.removeAllUpdateListeners(); 113 | if (Build.VERSION.SDK_INT >= 19) { 114 | mAnimator.pause(); 115 | } 116 | mAnimator.end(); 117 | mAnimator.cancel(); 118 | mAnimator = null; 119 | } 120 | } 121 | 122 | private void setToNextPosition() { 123 | for (int i = 0; i < mBalls.length; i++) { 124 | Ball ball = mBalls[i]; 125 | ball.next(); 126 | } 127 | } 128 | 129 | @Override 130 | public void doRefresh() { 131 | startAnim(); 132 | } 133 | 134 | @Override 135 | public void setColorSchemeResources(@ColorRes int... colorResIds) { 136 | 137 | } 138 | 139 | @Override 140 | public void setColorSchemeColors(@ColorInt int... colors) { 141 | 142 | } 143 | 144 | @Override 145 | public void stop() { 146 | stopAnim(); 147 | } 148 | 149 | @Override 150 | public void onPull(int offset, int total, int overPull) { 151 | if (mAnimator != null && mAnimator.isRunning()) { 152 | return; 153 | } 154 | int dis = offset; 155 | if (overPull > 0) { 156 | dis = total + overPull; 157 | } 158 | float useDis = dis * 0.3f; 159 | mCurrentPercent = useDis / total; 160 | for (int i = 0; i < mBalls.length; i++) { 161 | Ball ball = mBalls[i]; 162 | ball.calculate(mCurrentPercent); 163 | } 164 | invalidate(); 165 | } 166 | 167 | @Override 168 | protected void onAttachedToWindow() { 169 | super.onAttachedToWindow(); 170 | } 171 | 172 | @Override 173 | protected void onDetachedFromWindow() { 174 | super.onDetachedFromWindow(); 175 | stopAnim(); 176 | } 177 | 178 | @Override 179 | protected void onDraw(Canvas canvas) { 180 | super.onDraw(canvas); 181 | for (int i = 0; i < mBalls.length; i++) { 182 | mBalls[i].draw(canvas); 183 | } 184 | } 185 | 186 | @Override 187 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 188 | setMeasuredDimension(mSize, mSize); 189 | } 190 | 191 | private static class OriginPoint { 192 | private float mX; 193 | private float mY; 194 | private OriginPoint mNext; 195 | 196 | public OriginPoint(float x, float y) { 197 | mX = x; 198 | mY = y; 199 | } 200 | 201 | public void setNext(OriginPoint next) { 202 | mNext = next; 203 | } 204 | 205 | public OriginPoint getNext() { 206 | return mNext; 207 | } 208 | 209 | public float getX() { 210 | return mX; 211 | } 212 | 213 | public float getY() { 214 | return mY; 215 | } 216 | } 217 | 218 | private static class Ball { 219 | private float mRadius; 220 | private float mX; 221 | private float mY; 222 | private float mSmallRadius; 223 | private float mSmallX; 224 | private float mSmallY; 225 | private Path mPath; 226 | private Paint mPaint; 227 | private OriginPoint mOriginPoint; 228 | 229 | public Ball(float radius, float smallRadius, @ColorInt int color, OriginPoint op) { 230 | mRadius = radius; 231 | mSmallRadius = smallRadius; 232 | mX = mSmallX = op.getX(); 233 | mY = mSmallY = op.getY(); 234 | mPaint = new Paint(); 235 | mPaint.setColor(color); 236 | mPaint.setStyle(Paint.Style.FILL); 237 | mPaint.setAntiAlias(true); 238 | mPath = new Path(); 239 | mOriginPoint = op; 240 | } 241 | 242 | 243 | public void calculate(float percent) { 244 | if (percent > 1f) { 245 | percent = 1f; 246 | } 247 | float v = 1.3f; 248 | float smallChangePoint = 0.5f, smallV1 = 0.3f; 249 | float smallV2 = (1 - smallChangePoint * smallV1) / (1 - smallChangePoint); 250 | float ev = Math.min(1f, v * percent); 251 | float smallEv; 252 | if (percent > smallChangePoint) { 253 | smallEv = smallV2 * (percent - smallChangePoint) + smallChangePoint * smallV1; 254 | } else { 255 | smallEv = smallV1 * percent; 256 | } 257 | 258 | 259 | float startX = mOriginPoint.getX(); 260 | float startY = mOriginPoint.getY(); 261 | OriginPoint next = mOriginPoint.getNext(); 262 | float endX = next.getX(); 263 | float endY = next.getY(); 264 | float f = (endY - startY) * 1f / (endX - startX); 265 | 266 | mX = (int) (startX + (endX - startX) * ev); 267 | mY = (int) (f * (mX - startX) + startY); 268 | mSmallX = (int) (startX + (endX - startX) * smallEv); 269 | mSmallY = (int) (f * (mSmallX - startX) + startY); 270 | } 271 | 272 | public void next() { 273 | mOriginPoint = mOriginPoint.getNext(); 274 | } 275 | 276 | public void draw(Canvas canvas) { 277 | canvas.drawCircle(mX, mY, mRadius, mPaint); 278 | canvas.drawCircle(mSmallX, mSmallY, mSmallRadius, mPaint); 279 | if (mSmallX == mX && mSmallY == mY) { 280 | return; 281 | } 282 | 283 | /* 三角函数求四个点 */ 284 | float angle; 285 | float x1, y1, smallX1, smallY1, x2, y2, smallX2, smallY2; 286 | if (mSmallX == mX) { 287 | double v = (mRadius - mSmallRadius) / (mY - mSmallY); 288 | if (v > 1 || v < -1) { 289 | return; 290 | } 291 | angle = (float) Math.asin(v); 292 | float sin = (float) Math.sin(angle); 293 | float cos = (float) Math.cos(angle); 294 | x1 = mX - mRadius * cos; 295 | y1 = mY - mRadius * sin; 296 | x2 = mX + mRadius * cos; 297 | y2 = y1; 298 | smallX1 = mSmallX - mSmallRadius * cos; 299 | smallY1 = mSmallY - mSmallRadius * sin; 300 | smallX2 = mSmallX + mSmallRadius * cos; 301 | smallY2 = smallY1; 302 | } else if (mSmallY == mY) { 303 | double v = (mRadius - mSmallRadius) / (mX - mSmallX); 304 | if (v > 1 || v < -1) { 305 | return; 306 | } 307 | angle = (float) Math.asin(v); 308 | float sin = (float) Math.sin(angle); 309 | float cos = (float) Math.cos(angle); 310 | x1 = mX - mRadius * sin; 311 | y1 = mY + mRadius * cos; 312 | x2 = x1; 313 | y2 = mY - mRadius * cos; 314 | smallX1 = mSmallX - mSmallRadius * sin; 315 | smallY1 = mSmallY + mSmallRadius * cos; 316 | smallX2 = smallX1; 317 | smallY2 = mSmallY - mSmallRadius * cos; 318 | } else { 319 | double ab = Math.sqrt(Math.pow(mY - mSmallY, 2) + Math.pow(mX - mSmallX, 2)); 320 | double v = (mRadius - mSmallRadius) / ab; 321 | if (v > 1 || v < -1) { 322 | return; 323 | } 324 | double alpha = Math.asin(v); 325 | double b = Math.atan((mSmallY - mY) / (mSmallX - mX)); 326 | angle = (float) (Math.PI / 2 - alpha - b); 327 | float sin = (float) Math.sin(angle); 328 | float cos = (float) Math.cos(angle); 329 | smallX1 = mSmallX - mSmallRadius * cos; 330 | smallY1 = mSmallY + mSmallRadius * sin; 331 | x1 = mX - mRadius * cos; 332 | y1 = mY + mRadius * sin; 333 | 334 | angle = (float) (b - alpha); 335 | sin = (float) Math.sin(angle); 336 | cos = (float) Math.cos(angle); 337 | smallX2 = mSmallX + mSmallRadius * sin; 338 | smallY2 = mSmallY - mSmallRadius * cos; 339 | x2 = mX + mRadius * sin; 340 | y2 = mY - mRadius * cos; 341 | 342 | } 343 | 344 | /* 控制点 */ 345 | float centerX = (mX + mSmallX) / 2, centerY = (mY + mSmallY) / 2; 346 | float center1X = (x1 + smallX1) / 2, center1y = (y1 + smallY1) / 2; 347 | float center2X = (x2 + smallX2) / 2, center2y = (y2 + smallY2) / 2; 348 | float k1 = (center1y - centerY) / (center1X - centerX); 349 | float k2 = (center2y - centerY) / (center2X - centerX); 350 | float ctrlV = 0.08f; 351 | float anchor1X = center1X + (centerX - center1X) * ctrlV, anchor1Y = k1 * (anchor1X - center1X) + center1y; 352 | float anchor2X = center2X + (centerX - center2X) * ctrlV, anchor2Y = k2 * (anchor2X - center2X) + center2y; 353 | 354 | /* 画贝塞尔曲线 */ 355 | mPath.reset(); 356 | mPath.moveTo(x1, y1); 357 | mPath.quadTo(anchor1X, anchor1Y, smallX1, smallY1); 358 | mPath.lineTo(smallX2, smallY2); 359 | mPath.quadTo(anchor2X, anchor2Y, x2, y2); 360 | mPath.lineTo(x1, y1); 361 | canvas.drawPath(mPath, mPaint); 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/WWPullRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | 7 | import org.cgsdream.demo_android.qmui.QMUIPullRefreshLayout; 8 | 9 | /** 10 | * @author cginechen 11 | * @date 2016-12-11 12 | */ 13 | 14 | public class WWPullRefreshLayout extends QMUIPullRefreshLayout { 15 | 16 | public WWPullRefreshLayout(Context context) { 17 | super(context); 18 | } 19 | 20 | public WWPullRefreshLayout(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | @Override 25 | protected View createRefreshView() { 26 | return new WWLoadingView(getContext(), getResources().getDimensionPixelSize(R.dimen.loading_size)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/qmui/QMUIDisplayHelper.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android.qmui; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.WindowManager; 6 | 7 | /** 8 | * @author cginechen 9 | * @date 2017-03-31 10 | */ 11 | 12 | public class QMUIDisplayHelper { 13 | 14 | /** 15 | * DisplayMetrics 16 | * 17 | * @return 18 | */ 19 | public static DisplayMetrics getDisplayMetrics(Context context) { 20 | DisplayMetrics displayMetrics = new DisplayMetrics(); 21 | ((WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)) 22 | .getDefaultDisplay().getMetrics(displayMetrics); 23 | return displayMetrics; 24 | } 25 | 26 | 27 | /** 28 | * 屏幕密度 29 | */ 30 | public static float sDensity = 0f; 31 | 32 | public static float getDensity(Context context) { 33 | if (sDensity == 0f) { 34 | sDensity = getDisplayMetrics(context).density; 35 | } 36 | return sDensity; 37 | } 38 | 39 | /** 40 | * 单位转换: dp -> px 41 | * 42 | * @param dp 43 | * @return 44 | */ 45 | public static int dp2px(Context context, int dp) { 46 | return (int) (getDensity(context) * dp + 0.5); 47 | } 48 | 49 | /** 50 | * 单位转换:px -> dp 51 | * 52 | * @param px 53 | * @return 54 | */ 55 | public static int px2dp(Context context, int px) { 56 | return (int) (px / getDensity(context) + 0.5); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/qmui/QMUIMaterialProgressDrawable.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 org.cgsdream.demo_android.qmui; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.ColorFilter; 24 | import android.graphics.Paint; 25 | import android.graphics.Paint.Style; 26 | import android.graphics.Path; 27 | import android.graphics.PixelFormat; 28 | import android.graphics.Rect; 29 | import android.graphics.RectF; 30 | import android.graphics.drawable.Animatable; 31 | import android.graphics.drawable.Drawable; 32 | import android.support.annotation.IntDef; 33 | import android.support.annotation.NonNull; 34 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 35 | import android.util.DisplayMetrics; 36 | import android.view.View; 37 | import android.view.animation.AccelerateDecelerateInterpolator; 38 | import android.view.animation.Animation; 39 | import android.view.animation.Interpolator; 40 | import android.view.animation.LinearInterpolator; 41 | import android.view.animation.Transformation; 42 | 43 | import java.lang.annotation.Retention; 44 | import java.lang.annotation.RetentionPolicy; 45 | import java.util.ArrayList; 46 | 47 | /** 48 | * Fancy progress indicator for Material theme. 49 | * 50 | * Copied from android.support.v4.widget.MaterialProgressDrawable 51 | */ 52 | public class QMUIMaterialProgressDrawable extends Drawable implements Animatable { 53 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 54 | static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); 55 | 56 | private static final float FULL_ROTATION = 1080.0f; 57 | 58 | @Retention(RetentionPolicy.SOURCE) 59 | @IntDef({LARGE, DEFAULT}) 60 | public @interface ProgressDrawableSize {} 61 | 62 | // Maps to ProgressBar.Large style 63 | public static final int LARGE = 0; 64 | // Maps to ProgressBar default style 65 | public static final int DEFAULT = 1; 66 | 67 | // Maps to ProgressBar default style 68 | private static final int CIRCLE_DIAMETER = 40; 69 | private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 70 | private static final float STROKE_WIDTH = 2.5f; 71 | 72 | // Maps to ProgressBar.Large style 73 | private static final int CIRCLE_DIAMETER_LARGE = 56; 74 | private static final float CENTER_RADIUS_LARGE = 12.5f; 75 | private static final float STROKE_WIDTH_LARGE = 3f; 76 | 77 | private static final int[] COLORS = new int[] { 78 | Color.BLACK 79 | }; 80 | 81 | /** 82 | * The value in the linear interpolator for animating the drawable at which 83 | * the color transition should start 84 | */ 85 | private static final float COLOR_START_DELAY_OFFSET = 0.75f; 86 | private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; 87 | private static final float START_TRIM_DURATION_OFFSET = 0.5f; 88 | 89 | /** The duration of a single progress spin in milliseconds. */ 90 | private static final int ANIMATION_DURATION = 1332; 91 | 92 | /** The number of points in the progress "star". */ 93 | private static final float NUM_POINTS = 5f; 94 | /** The list of animators operating on this drawable. */ 95 | private final ArrayList mAnimators = new ArrayList(); 96 | 97 | /** The indicator ring, used to manage animation state. */ 98 | private final Ring mRing; 99 | 100 | /** Canvas rotation in degrees. */ 101 | private float mRotation; 102 | 103 | /** Layout info for the arrowhead in dp */ 104 | private static final int ARROW_WIDTH = 10; 105 | private static final int ARROW_HEIGHT = 5; 106 | private static final float ARROW_OFFSET_ANGLE = 5; 107 | 108 | /** Layout info for the arrowhead for the large spinner in dp */ 109 | private static final int ARROW_WIDTH_LARGE = 12; 110 | private static final int ARROW_HEIGHT_LARGE = 6; 111 | private static final float MAX_PROGRESS_ARC = .8f; 112 | 113 | private Resources mResources; 114 | private View mParent; 115 | private Animation mAnimation; 116 | float mRotationCount; 117 | private double mWidth; 118 | private double mHeight; 119 | boolean mFinishing; 120 | 121 | public QMUIMaterialProgressDrawable(Context context, View parent) { 122 | mParent = parent; 123 | mResources = context.getResources(); 124 | 125 | mRing = new Ring(mCallback); 126 | mRing.setColors(COLORS); 127 | 128 | updateSizes(DEFAULT); 129 | setupAnimators(); 130 | } 131 | 132 | private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 133 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 134 | final Ring ring = mRing; 135 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 136 | final float screenDensity = metrics.density; 137 | 138 | mWidth = progressCircleWidth * screenDensity; 139 | mHeight = progressCircleHeight * screenDensity; 140 | ring.setStrokeWidth((float) strokeWidth * screenDensity); 141 | ring.setCenterRadius(centerRadius * screenDensity); 142 | ring.setColorIndex(0); 143 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 144 | ring.setInsets((int) mWidth, (int) mHeight); 145 | } 146 | 147 | /** 148 | * Set the overall size for the progress spinner. This updates the radius 149 | * and stroke width of the ring. 150 | * 151 | * @param size One of {@link #LARGE} or 152 | * {@link #DEFAULT} 153 | */ 154 | public void updateSizes(@ProgressDrawableSize int size) { 155 | if (size == LARGE) { 156 | setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 157 | STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 158 | } else { 159 | setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 160 | ARROW_WIDTH, ARROW_HEIGHT); 161 | } 162 | } 163 | 164 | /** 165 | * @param show Set to true to display the arrowhead on the progress spinner. 166 | */ 167 | public void showArrow(boolean show) { 168 | mRing.setShowArrow(show); 169 | } 170 | 171 | /** 172 | * @param scale Set the scale of the arrowhead for the spinner. 173 | */ 174 | public void setArrowScale(float scale) { 175 | mRing.setArrowScale(scale); 176 | } 177 | 178 | /** 179 | * Set the start and end trim for the progress spinner arc. 180 | * 181 | * @param startAngle start angle 182 | * @param endAngle end angle 183 | */ 184 | public void setStartEndTrim(float startAngle, float endAngle) { 185 | mRing.setStartTrim(startAngle); 186 | mRing.setEndTrim(endAngle); 187 | } 188 | 189 | /** 190 | * Set the amount of rotation to apply to the progress spinner. 191 | * 192 | * @param rotation Rotation is from [0..1] 193 | */ 194 | public void setProgressRotation(float rotation) { 195 | mRing.setRotation(rotation); 196 | } 197 | 198 | /** 199 | * Update the background color of the circle image view. 200 | */ 201 | public void setBackgroundColor(int color) { 202 | mRing.setBackgroundColor(color); 203 | } 204 | 205 | /** 206 | * Set the colors used in the progress animation from color resources. 207 | * The first color will also be the color of the bar that grows in response 208 | * to a user swipe gesture. 209 | * 210 | * @param colors 211 | */ 212 | public void setColorSchemeColors(int... colors) { 213 | mRing.setColors(colors); 214 | mRing.setColorIndex(0); 215 | } 216 | 217 | @Override 218 | public int getIntrinsicHeight() { 219 | return (int) mHeight; 220 | } 221 | 222 | @Override 223 | public int getIntrinsicWidth() { 224 | return (int) mWidth; 225 | } 226 | 227 | @Override 228 | public void draw(Canvas c) { 229 | final Rect bounds = getBounds(); 230 | final int saveCount = c.save(); 231 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 232 | mRing.draw(c, bounds); 233 | c.restoreToCount(saveCount); 234 | } 235 | 236 | @Override 237 | public void setAlpha(int alpha) { 238 | mRing.setAlpha(alpha); 239 | } 240 | 241 | public int getAlpha() { 242 | return mRing.getAlpha(); 243 | } 244 | 245 | @Override 246 | public void setColorFilter(ColorFilter colorFilter) { 247 | mRing.setColorFilter(colorFilter); 248 | } 249 | 250 | @SuppressWarnings("unused") 251 | void setRotation(float rotation) { 252 | mRotation = rotation; 253 | invalidateSelf(); 254 | } 255 | 256 | @SuppressWarnings("unused") 257 | private float getRotation() { 258 | return mRotation; 259 | } 260 | 261 | @Override 262 | public int getOpacity() { 263 | return PixelFormat.TRANSLUCENT; 264 | } 265 | 266 | @Override 267 | public boolean isRunning() { 268 | final ArrayList animators = mAnimators; 269 | final int N = animators.size(); 270 | for (int i = 0; i < N; i++) { 271 | final Animation animator = animators.get(i); 272 | if (animator.hasStarted() && !animator.hasEnded()) { 273 | return true; 274 | } 275 | } 276 | return false; 277 | } 278 | 279 | @Override 280 | public void start() { 281 | mAnimation.reset(); 282 | mRing.storeOriginals(); 283 | // Already showing some part of the ring 284 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 285 | mFinishing = true; 286 | mAnimation.setDuration(ANIMATION_DURATION / 2); 287 | mParent.startAnimation(mAnimation); 288 | } else { 289 | mRing.setColorIndex(0); 290 | mRing.resetOriginals(); 291 | mAnimation.setDuration(ANIMATION_DURATION); 292 | mParent.startAnimation(mAnimation); 293 | } 294 | } 295 | 296 | @Override 297 | public void stop() { 298 | mParent.clearAnimation(); 299 | setRotation(0); 300 | mRing.setShowArrow(false); 301 | mRing.setColorIndex(0); 302 | mRing.resetOriginals(); 303 | } 304 | 305 | float getMinProgressArc(Ring ring) { 306 | return (float) Math.toRadians( 307 | ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); 308 | } 309 | 310 | // Adapted from ArgbEvaluator.java 311 | private int evaluateColorChange(float fraction, int startValue, int endValue) { 312 | int startInt = (Integer) startValue; 313 | int startA = (startInt >> 24) & 0xff; 314 | int startR = (startInt >> 16) & 0xff; 315 | int startG = (startInt >> 8) & 0xff; 316 | int startB = startInt & 0xff; 317 | 318 | int endInt = (Integer) endValue; 319 | int endA = (endInt >> 24) & 0xff; 320 | int endR = (endInt >> 16) & 0xff; 321 | int endG = (endInt >> 8) & 0xff; 322 | int endB = endInt & 0xff; 323 | 324 | return (int) ((startA + (int) (fraction * (endA - startA))) << 24) 325 | | (int) ((startR + (int) (fraction * (endR - startR))) << 16) 326 | | (int) ((startG + (int) (fraction * (endG - startG))) << 8) 327 | | (int) ((startB + (int) (fraction * (endB - startB)))); 328 | } 329 | 330 | /** 331 | * Update the ring color if this is within the last 25% of the animation. 332 | * The new ring color will be a translation from the starting ring color to 333 | * the next color. 334 | */ 335 | void updateRingColor(float interpolatedTime, Ring ring) { 336 | if (interpolatedTime > COLOR_START_DELAY_OFFSET) { 337 | // scale the interpolatedTime so that the full 338 | // transformation from 0 - 1 takes place in the 339 | // remaining time 340 | ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) 341 | / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), 342 | ring.getNextColor())); 343 | } 344 | } 345 | 346 | void applyFinishTranslation(float interpolatedTime, Ring ring) { 347 | // shrink back down and complete a full rotation before 348 | // starting other circles 349 | // Rotation goes between [0..1]. 350 | updateRingColor(interpolatedTime, ring); 351 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 352 | + 1f); 353 | final float minProgressArc = getMinProgressArc(ring); 354 | final float startTrim = ring.getStartingStartTrim() 355 | + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) 356 | * interpolatedTime; 357 | ring.setStartTrim(startTrim); 358 | ring.setEndTrim(ring.getStartingEndTrim()); 359 | final float rotation = ring.getStartingRotation() 360 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 361 | ring.setRotation(rotation); 362 | } 363 | 364 | private void setupAnimators() { 365 | final Ring ring = mRing; 366 | final Animation animation = new Animation() { 367 | @Override 368 | public void applyTransformation(float interpolatedTime, Transformation t) { 369 | if (mFinishing) { 370 | applyFinishTranslation(interpolatedTime, ring); 371 | } else { 372 | // The minProgressArc is calculated from 0 to create an 373 | // angle that matches the stroke width. 374 | final float minProgressArc = getMinProgressArc(ring); 375 | final float startingEndTrim = ring.getStartingEndTrim(); 376 | final float startingTrim = ring.getStartingStartTrim(); 377 | final float startingRotation = ring.getStartingRotation(); 378 | 379 | updateRingColor(interpolatedTime, ring); 380 | 381 | // Moving the start trim only occurs in the first 50% of a 382 | // single ring animation 383 | if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { 384 | // scale the interpolatedTime so that the full 385 | // transformation from 0 - 1 takes place in the 386 | // remaining time 387 | final float scaledTime = (interpolatedTime) 388 | / (1.0f - START_TRIM_DURATION_OFFSET); 389 | final float startTrim = startingTrim 390 | + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR 391 | .getInterpolation(scaledTime)); 392 | ring.setStartTrim(startTrim); 393 | } 394 | 395 | // Moving the end trim starts after 50% of a single ring 396 | // animation completes 397 | if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { 398 | // scale the interpolatedTime so that the full 399 | // transformation from 0 - 1 takes place in the 400 | // remaining time 401 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 402 | float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) 403 | / (1.0f - START_TRIM_DURATION_OFFSET); 404 | final float endTrim = startingEndTrim 405 | + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); 406 | ring.setEndTrim(endTrim); 407 | } 408 | 409 | final float rotation = startingRotation + (0.25f * interpolatedTime); 410 | ring.setRotation(rotation); 411 | 412 | float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) 413 | + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); 414 | setRotation(groupRotation); 415 | } 416 | } 417 | }; 418 | animation.setRepeatCount(Animation.INFINITE); 419 | animation.setRepeatMode(Animation.RESTART); 420 | animation.setInterpolator(LINEAR_INTERPOLATOR); 421 | animation.setAnimationListener(new Animation.AnimationListener() { 422 | 423 | @Override 424 | public void onAnimationStart(Animation animation) { 425 | mRotationCount = 0; 426 | } 427 | 428 | @Override 429 | public void onAnimationEnd(Animation animation) { 430 | // do nothing 431 | } 432 | 433 | @Override 434 | public void onAnimationRepeat(Animation animation) { 435 | ring.storeOriginals(); 436 | ring.goToNextColor(); 437 | ring.setStartTrim(ring.getEndTrim()); 438 | if (mFinishing) { 439 | // finished closing the last ring from the swipe gesture; go 440 | // into progress mode 441 | mFinishing = false; 442 | animation.setDuration(ANIMATION_DURATION); 443 | ring.setShowArrow(false); 444 | } else { 445 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 446 | } 447 | } 448 | }); 449 | mAnimation = animation; 450 | } 451 | 452 | private final Callback mCallback = new Callback() { 453 | @Override 454 | public void invalidateDrawable(Drawable d) { 455 | invalidateSelf(); 456 | } 457 | 458 | @Override 459 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 460 | scheduleSelf(what, when); 461 | } 462 | 463 | @Override 464 | public void unscheduleDrawable(Drawable d, Runnable what) { 465 | unscheduleSelf(what); 466 | } 467 | }; 468 | 469 | private static class Ring { 470 | private final RectF mTempBounds = new RectF(); 471 | private final Paint mPaint = new Paint(); 472 | private final Paint mArrowPaint = new Paint(); 473 | 474 | private final Callback mCallback; 475 | 476 | private float mStartTrim = 0.0f; 477 | private float mEndTrim = 0.0f; 478 | private float mRotation = 0.0f; 479 | private float mStrokeWidth = 5.0f; 480 | private float mStrokeInset = 2.5f; 481 | 482 | private int[] mColors; 483 | // mColorIndex represents the offset into the available mColors that the 484 | // progress circle should currently display. As the progress circle is 485 | // animating, the mColorIndex moves by one to the next available color. 486 | private int mColorIndex; 487 | private float mStartingStartTrim; 488 | private float mStartingEndTrim; 489 | private float mStartingRotation; 490 | private boolean mShowArrow; 491 | private Path mArrow; 492 | private float mArrowScale; 493 | private double mRingCenterRadius; 494 | private int mArrowWidth; 495 | private int mArrowHeight; 496 | private int mAlpha; 497 | private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 498 | private int mBackgroundColor; 499 | private int mCurrentColor; 500 | 501 | Ring(Callback callback) { 502 | mCallback = callback; 503 | 504 | mPaint.setStrokeCap(Paint.Cap.SQUARE); 505 | mPaint.setAntiAlias(true); 506 | mPaint.setStyle(Style.STROKE); 507 | 508 | mArrowPaint.setStyle(Style.FILL); 509 | mArrowPaint.setAntiAlias(true); 510 | } 511 | 512 | public void setBackgroundColor(int color) { 513 | mBackgroundColor = color; 514 | } 515 | 516 | /** 517 | * Set the dimensions of the arrowhead. 518 | * 519 | * @param width Width of the hypotenuse of the arrow head 520 | * @param height Height of the arrow point 521 | */ 522 | public void setArrowDimensions(float width, float height) { 523 | mArrowWidth = (int) width; 524 | mArrowHeight = (int) height; 525 | } 526 | 527 | /** 528 | * Draw the progress spinner 529 | */ 530 | public void draw(Canvas c, Rect bounds) { 531 | final RectF arcBounds = mTempBounds; 532 | arcBounds.set(bounds); 533 | arcBounds.inset(mStrokeInset, mStrokeInset); 534 | 535 | final float startAngle = (mStartTrim + mRotation) * 360; 536 | final float endAngle = (mEndTrim + mRotation) * 360; 537 | float sweepAngle = endAngle - startAngle; 538 | 539 | mPaint.setColor(mCurrentColor); 540 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 541 | 542 | drawTriangle(c, startAngle, sweepAngle, bounds); 543 | 544 | if (mAlpha < 255) { 545 | mCirclePaint.setColor(mBackgroundColor); 546 | mCirclePaint.setAlpha(255 - mAlpha); 547 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 548 | mCirclePaint); 549 | } 550 | } 551 | 552 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 553 | if (mShowArrow) { 554 | if (mArrow == null) { 555 | mArrow = new Path(); 556 | mArrow.setFillType(Path.FillType.EVEN_ODD); 557 | } else { 558 | mArrow.reset(); 559 | } 560 | 561 | // Adjust the position of the triangle so that it is inset as 562 | // much as the arc, but also centered on the arc. 563 | float inset = (int) mStrokeInset / 2 * mArrowScale; 564 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 565 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 566 | 567 | // Update the path each time. This works around an issue in SKIA 568 | // where concatenating a rotation matrix to a scale matrix 569 | // ignored a starting negative rotation. This appears to have 570 | // been fixed as of API 21. 571 | mArrow.moveTo(0, 0); 572 | mArrow.lineTo(mArrowWidth * mArrowScale, 0); 573 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 574 | * mArrowScale)); 575 | mArrow.offset(x - inset, y); 576 | mArrow.close(); 577 | // draw a triangle 578 | mArrowPaint.setColor(mCurrentColor); 579 | c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 580 | bounds.exactCenterY()); 581 | c.drawPath(mArrow, mArrowPaint); 582 | } 583 | } 584 | 585 | /** 586 | * Set the colors the progress spinner alternates between. 587 | * 588 | * @param colors Array of integers describing the colors. Must be non-null. 589 | */ 590 | public void setColors(@NonNull int[] colors) { 591 | mColors = colors; 592 | // if colors are reset, make sure to reset the color index as well 593 | setColorIndex(0); 594 | } 595 | 596 | /** 597 | * Set the absolute color of the progress spinner. This is should only 598 | * be used when animating between current and next color when the 599 | * spinner is rotating. 600 | * 601 | * @param color int describing the color. 602 | */ 603 | public void setColor(int color) { 604 | mCurrentColor = color; 605 | } 606 | 607 | /** 608 | * @param index Index into the color array of the color to display in 609 | * the progress spinner. 610 | */ 611 | public void setColorIndex(int index) { 612 | mColorIndex = index; 613 | mCurrentColor = mColors[mColorIndex]; 614 | } 615 | 616 | /** 617 | * @return int describing the next color the progress spinner should use when drawing. 618 | */ 619 | public int getNextColor() { 620 | return mColors[getNextColorIndex()]; 621 | } 622 | 623 | private int getNextColorIndex() { 624 | return (mColorIndex + 1) % (mColors.length); 625 | } 626 | 627 | /** 628 | * Proceed to the next available ring color. This will automatically 629 | * wrap back to the beginning of colors. 630 | */ 631 | public void goToNextColor() { 632 | setColorIndex(getNextColorIndex()); 633 | } 634 | 635 | public void setColorFilter(ColorFilter filter) { 636 | mPaint.setColorFilter(filter); 637 | invalidateSelf(); 638 | } 639 | 640 | /** 641 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 642 | */ 643 | public void setAlpha(int alpha) { 644 | mAlpha = alpha; 645 | } 646 | 647 | /** 648 | * @return Current alpha of the progress spinner and arrowhead. 649 | */ 650 | public int getAlpha() { 651 | return mAlpha; 652 | } 653 | 654 | /** 655 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 656 | */ 657 | public void setStrokeWidth(float strokeWidth) { 658 | mStrokeWidth = strokeWidth; 659 | mPaint.setStrokeWidth(strokeWidth); 660 | invalidateSelf(); 661 | } 662 | 663 | @SuppressWarnings("unused") 664 | public float getStrokeWidth() { 665 | return mStrokeWidth; 666 | } 667 | 668 | @SuppressWarnings("unused") 669 | public void setStartTrim(float startTrim) { 670 | mStartTrim = startTrim; 671 | invalidateSelf(); 672 | } 673 | 674 | @SuppressWarnings("unused") 675 | public float getStartTrim() { 676 | return mStartTrim; 677 | } 678 | 679 | public float getStartingStartTrim() { 680 | return mStartingStartTrim; 681 | } 682 | 683 | public float getStartingEndTrim() { 684 | return mStartingEndTrim; 685 | } 686 | 687 | public int getStartingColor() { 688 | return mColors[mColorIndex]; 689 | } 690 | 691 | @SuppressWarnings("unused") 692 | public void setEndTrim(float endTrim) { 693 | mEndTrim = endTrim; 694 | invalidateSelf(); 695 | } 696 | 697 | @SuppressWarnings("unused") 698 | public float getEndTrim() { 699 | return mEndTrim; 700 | } 701 | 702 | @SuppressWarnings("unused") 703 | public void setRotation(float rotation) { 704 | mRotation = rotation; 705 | invalidateSelf(); 706 | } 707 | 708 | @SuppressWarnings("unused") 709 | public float getRotation() { 710 | return mRotation; 711 | } 712 | 713 | public void setInsets(int width, int height) { 714 | final float minEdge = (float) Math.min(width, height); 715 | float insets; 716 | if (mRingCenterRadius <= 0 || minEdge < 0) { 717 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 718 | } else { 719 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 720 | } 721 | mStrokeInset = insets; 722 | } 723 | 724 | @SuppressWarnings("unused") 725 | public float getInsets() { 726 | return mStrokeInset; 727 | } 728 | 729 | /** 730 | * @param centerRadius Inner radius in px of the circle the progress 731 | * spinner arc traces. 732 | */ 733 | public void setCenterRadius(double centerRadius) { 734 | mRingCenterRadius = centerRadius; 735 | } 736 | 737 | public double getCenterRadius() { 738 | return mRingCenterRadius; 739 | } 740 | 741 | /** 742 | * @param show Set to true to show the arrow head on the progress spinner. 743 | */ 744 | public void setShowArrow(boolean show) { 745 | if (mShowArrow != show) { 746 | mShowArrow = show; 747 | invalidateSelf(); 748 | } 749 | } 750 | 751 | /** 752 | * @param scale Set the scale of the arrowhead for the spinner. 753 | */ 754 | public void setArrowScale(float scale) { 755 | if (scale != mArrowScale) { 756 | mArrowScale = scale; 757 | invalidateSelf(); 758 | } 759 | } 760 | 761 | /** 762 | * @return The amount the progress spinner is currently rotated, between [0..1]. 763 | */ 764 | public float getStartingRotation() { 765 | return mStartingRotation; 766 | } 767 | 768 | /** 769 | * If the start / end trim are offset to begin with, store them so that 770 | * animation starts from that offset. 771 | */ 772 | public void storeOriginals() { 773 | mStartingStartTrim = mStartTrim; 774 | mStartingEndTrim = mEndTrim; 775 | mStartingRotation = mRotation; 776 | } 777 | 778 | /** 779 | * Reset the progress spinner to default rotation, start and end angles. 780 | */ 781 | public void resetOriginals() { 782 | mStartingStartTrim = 0; 783 | mStartingEndTrim = 0; 784 | mStartingRotation = 0; 785 | setStartTrim(0); 786 | setEndTrim(0); 787 | setRotation(0); 788 | } 789 | 790 | private void invalidateSelf() { 791 | mCallback.invalidateDrawable(null); 792 | } 793 | } 794 | 795 | /** 796 | * Squishes the interpolation curve into the second half of the animation. 797 | */ 798 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 799 | @Override 800 | public float getInterpolation(float input) { 801 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 802 | } 803 | } 804 | 805 | /** 806 | * Squishes the interpolation curve into the first half of the animation. 807 | */ 808 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 809 | @Override 810 | public float getInterpolation(float input) { 811 | return super.getInterpolation(Math.min(1, input * 2.0f)); 812 | } 813 | } 814 | } 815 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/java/org/cgsdream/demo_android/qmui/QMUIPullRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android.qmui; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.support.annotation.ColorInt; 6 | import android.support.annotation.ColorRes; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.content.ContextCompat; 9 | import android.support.v4.view.MotionEventCompat; 10 | import android.support.v4.view.NestedScrollingParent; 11 | import android.support.v4.view.NestedScrollingParentHelper; 12 | import android.support.v4.view.ViewCompat; 13 | import android.support.v7.widget.RecyclerView; 14 | import android.util.AttributeSet; 15 | import android.util.Log; 16 | import android.view.MotionEvent; 17 | import android.view.VelocityTracker; 18 | import android.view.View; 19 | import android.view.ViewConfiguration; 20 | import android.view.ViewGroup; 21 | import android.widget.AbsListView; 22 | import android.widget.ImageView; 23 | import android.widget.Scroller; 24 | 25 | import org.cgsdream.demo_android.R; 26 | 27 | /** 28 | * @author cginechen 29 | * @date 2016-12-11 30 | */ 31 | 32 | public class QMUIPullRefreshLayout extends ViewGroup implements NestedScrollingParent { 33 | private static final String TAG = "QMUIPullRefreshLayout"; 34 | private static final int INVALID_POINTER = -1; 35 | 36 | private View mTargetView; 37 | private IRefreshView mIRefreshView; 38 | private View mRefreshView; 39 | private int mRefreshZIndex = -1; 40 | private int mTouchSlop; 41 | private boolean mIsRefreshing = false; 42 | private OnPullListener mListener; 43 | private OnChildScrollUpCallback mChildScrollUpCallback; 44 | 45 | private int mRefreshInitOffset; 46 | private int mRefreshCurrentOffset; 47 | private int mRefreshEndOffset; 48 | 49 | private int mTargetInitOffset; 50 | private int mTargetCurrentOffset; 51 | private int mTargetRefreshOffset; 52 | private boolean mAutoTargetRefreshOffset = true; 53 | private boolean mEnableOverPull = true; 54 | 55 | private final NestedScrollingParentHelper mNestedScrollingParentHelper; 56 | private boolean mNestedScrollInProgress; 57 | private int mActivePointerId = INVALID_POINTER; 58 | private boolean mIsDragging; 59 | private float mInitialDownY; 60 | private float mInitialMotionY; 61 | private float mLastMotionY; 62 | 63 | private VelocityTracker mVelocityTracker; 64 | private float mMaxVelocity; 65 | 66 | private Scroller mScroller; 67 | private boolean mNeedScrollToInitPos = false; 68 | private boolean mNeedScrollToRefreshPos = false; 69 | private boolean mNeedDecideDoRefreshOrNot = false; 70 | private boolean mNeedScrollToInitOrRefreshPosDependOnCurrentOffset = false; 71 | 72 | private float mDragRate = 0.65f; 73 | 74 | 75 | public QMUIPullRefreshLayout(Context context) { 76 | this(context, null); 77 | } 78 | 79 | public QMUIPullRefreshLayout(Context context, AttributeSet attrs) { 80 | this(context, attrs, R.attr.QMUIPullRefreshLayoutStyle); 81 | } 82 | 83 | public QMUIPullRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { 84 | super(context, attrs, defStyleAttr); 85 | setWillNotDraw(false); 86 | 87 | final ViewConfiguration vc = ViewConfiguration.get(getContext()); 88 | mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 89 | mTouchSlop = QMUIDisplayHelper.px2dp(context, vc.getScaledTouchSlop()); //系统的值是8dp,如何配置? 90 | 91 | mScroller = new Scroller(getContext()); 92 | mScroller.setFriction(0.98f); 93 | 94 | addRefreshView(); 95 | ViewCompat.setChildrenDrawingOrderEnabled(this, true); 96 | 97 | mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); 98 | 99 | TypedArray array = context.obtainStyledAttributes(attrs, 100 | R.styleable.QMUIPullRefreshLayout, defStyleAttr, 0); 101 | 102 | try { 103 | mRefreshInitOffset = array.getDimensionPixelSize( 104 | R.styleable.QMUIPullRefreshLayout_qmui_refresh_init_offset, Integer.MIN_VALUE); 105 | mRefreshEndOffset = array.getDimensionPixelSize( 106 | R.styleable.QMUIPullRefreshLayout_qmui_refresh_end_offset, QMUIDisplayHelper.dp2px(getContext(), 20) 107 | ); 108 | mTargetInitOffset = array.getDimensionPixelSize( 109 | R.styleable.QMUIPullRefreshLayout_qmui_target_init_offset, 0 110 | ); 111 | mTargetRefreshOffset = array.getDimensionPixelSize( 112 | R.styleable.QMUIPullRefreshLayout_qmui_target_refresh_offset, Integer.MIN_VALUE 113 | ); 114 | mAutoTargetRefreshOffset = array.getBoolean( 115 | R.styleable.QMUIPullRefreshLayout_qmui_auto_target_refresh_offset, true 116 | ); 117 | if (!mAutoTargetRefreshOffset && mTargetRefreshOffset == Integer.MIN_VALUE) { 118 | mAutoTargetRefreshOffset = true; 119 | } 120 | 121 | } finally { 122 | array.recycle(); 123 | } 124 | mRefreshCurrentOffset = mRefreshInitOffset; 125 | mTargetCurrentOffset = mTargetInitOffset; 126 | } 127 | 128 | public void setOnPullListener(OnPullListener listener) { 129 | mListener = listener; 130 | } 131 | 132 | public void setChildScrollUpCallback(OnChildScrollUpCallback childScrollUpCallback) { 133 | mChildScrollUpCallback = childScrollUpCallback; 134 | } 135 | 136 | protected View createRefreshView() { 137 | return new RefreshView(getContext()); 138 | } 139 | 140 | private void addRefreshView() { 141 | if (mRefreshView == null) { 142 | mRefreshView = createRefreshView(); 143 | } 144 | if (!(mRefreshView instanceof IRefreshView)) { 145 | throw new RuntimeException("refreshView must be a view"); 146 | } 147 | mIRefreshView = (IRefreshView) mRefreshView; 148 | if (mRefreshView.getLayoutParams() == null) { 149 | mRefreshView.setLayoutParams(new LayoutParams( 150 | LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 151 | } 152 | addView(mRefreshView); 153 | } 154 | 155 | @Override 156 | protected int getChildDrawingOrder(int childCount, int i) { 157 | if (mRefreshZIndex < 0) { 158 | return i; 159 | } 160 | // 最后才绘制mRefreshView 161 | if (i == mRefreshZIndex) { 162 | return childCount - 1; 163 | } 164 | if (i > mRefreshZIndex) { 165 | return i - 1; 166 | } 167 | return i; 168 | } 169 | 170 | @Override 171 | public void requestDisallowInterceptTouchEvent(boolean b) { 172 | // if this is a List < L or another view that doesn't support nested 173 | // scrolling, ignore this request so that the vertical scroll event 174 | // isn't stolen 175 | if ((android.os.Build.VERSION.SDK_INT < 21 && mTargetView instanceof AbsListView) 176 | || (mTargetView != null && !ViewCompat.isNestedScrollingEnabled(mTargetView))) { 177 | // Nope. 178 | } else { 179 | super.requestDisallowInterceptTouchEvent(b); 180 | } 181 | } 182 | 183 | @Override 184 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 185 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 186 | ensureTargetView(); 187 | if (mTargetView == null) { 188 | Log.d(TAG, "onMeasure: mTargetView == null"); 189 | return; 190 | } 191 | int targetMeasureWidthSpec = MeasureSpec.makeMeasureSpec( 192 | getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); 193 | int targetMeasureHeightSpec = MeasureSpec.makeMeasureSpec( 194 | getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); 195 | mTargetView.measure(targetMeasureWidthSpec, targetMeasureHeightSpec); 196 | measureChild(mRefreshView, widthMeasureSpec, heightMeasureSpec); 197 | mRefreshZIndex = -1; 198 | for (int i = 0; i < getChildCount(); i++) { 199 | if (getChildAt(i) == mRefreshView) { 200 | mRefreshZIndex = i; 201 | break; 202 | } 203 | } 204 | if (mRefreshInitOffset == Integer.MIN_VALUE) { 205 | mRefreshInitOffset = -mRefreshView.getMeasuredHeight(); 206 | mRefreshCurrentOffset = 0; 207 | } 208 | if (mAutoTargetRefreshOffset) { 209 | mTargetRefreshOffset = mRefreshEndOffset * 2 + mRefreshView.getMeasuredHeight(); 210 | } 211 | } 212 | 213 | @Override 214 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 215 | final int width = getMeasuredWidth(); 216 | final int height = getMeasuredHeight(); 217 | if (getChildCount() == 0) { 218 | return; 219 | } 220 | ensureTargetView(); 221 | if (mTargetView == null) { 222 | Log.d(TAG, "onLayout: mTargetView == null"); 223 | return; 224 | } 225 | 226 | final int childLeft = getPaddingLeft(); 227 | final int childTop = getPaddingTop(); 228 | final int childWidth = width - getPaddingLeft() - getPaddingRight(); 229 | final int childHeight = height - getPaddingTop() - getPaddingBottom(); 230 | mTargetView.layout(childLeft, childTop + mTargetCurrentOffset, 231 | childLeft + childWidth, childTop + childHeight + mTargetCurrentOffset); 232 | int refreshViewWidth = mRefreshView.getMeasuredWidth(); 233 | int refreshViewHeight = mRefreshView.getMeasuredHeight(); 234 | mRefreshView.layout((width / 2 - refreshViewWidth / 2), mRefreshCurrentOffset, 235 | (width / 2 + refreshViewWidth / 2), mRefreshCurrentOffset + refreshViewHeight); 236 | } 237 | 238 | @Override 239 | public boolean onInterceptTouchEvent(MotionEvent ev) { 240 | ensureTargetView(); 241 | 242 | final int action = MotionEventCompat.getActionMasked(ev); 243 | int pointerIndex; 244 | 245 | if (!isEnabled() || canChildScrollUp() || mNestedScrollInProgress) { 246 | Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = " 247 | + canChildScrollUp() + " ; mNestedScrollInProgress = " + mNestedScrollInProgress); 248 | return false; 249 | } 250 | switch (action) { 251 | case MotionEvent.ACTION_DOWN: 252 | mActivePointerId = ev.getPointerId(0); 253 | mIsDragging = false; 254 | pointerIndex = ev.findPointerIndex(mActivePointerId); 255 | if (pointerIndex < 0) { 256 | return false; 257 | } 258 | mInitialDownY = ev.getY(pointerIndex); 259 | break; 260 | 261 | case MotionEvent.ACTION_MOVE: 262 | pointerIndex = ev.findPointerIndex(mActivePointerId); 263 | if (pointerIndex < 0) { 264 | Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 265 | return false; 266 | } 267 | 268 | final float y = ev.getY(pointerIndex); 269 | startDragging(y); 270 | break; 271 | 272 | case MotionEventCompat.ACTION_POINTER_UP: 273 | onSecondaryPointerUp(ev); 274 | break; 275 | 276 | case MotionEvent.ACTION_UP: 277 | case MotionEvent.ACTION_CANCEL: 278 | mIsDragging = false; 279 | mActivePointerId = INVALID_POINTER; 280 | break; 281 | } 282 | 283 | return mIsDragging; 284 | } 285 | 286 | @Override 287 | public boolean onTouchEvent(MotionEvent ev) { 288 | final int action = MotionEventCompat.getActionMasked(ev); 289 | int pointerIndex; 290 | 291 | if (!isEnabled() || canChildScrollUp() || mNestedScrollInProgress) { 292 | Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = " 293 | + canChildScrollUp() + " ; mNestedScrollInProgress = " + mNestedScrollInProgress); 294 | return false; 295 | } 296 | 297 | acquireVelocityTracker(ev); 298 | 299 | switch (action) { 300 | case MotionEvent.ACTION_DOWN: 301 | mActivePointerId = ev.getPointerId(0); 302 | mIsDragging = false; 303 | resetTag(); 304 | if (!mScroller.isFinished()) { 305 | mScroller.abortAnimation(); 306 | } 307 | break; 308 | 309 | case MotionEvent.ACTION_MOVE: { 310 | pointerIndex = ev.findPointerIndex(mActivePointerId); 311 | if (pointerIndex < 0) { 312 | Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 313 | return false; 314 | } 315 | final float y = ev.getY(pointerIndex); 316 | startDragging(y); 317 | 318 | if (mIsDragging) { 319 | float dy = y - mLastMotionY; 320 | if (dy >= 0) { 321 | moveTargetView(dy, true); 322 | } else { 323 | if (mTargetCurrentOffset + dy <= mTargetInitOffset) { 324 | moveTargetView(dy, true); 325 | // 重新dispatch一次down事件,使得列表可以继续滚动 326 | int oldAction = ev.getAction(); 327 | ev.setAction(MotionEvent.ACTION_DOWN); 328 | dispatchTouchEvent(ev); 329 | ev.setAction(oldAction); 330 | } else { 331 | moveTargetView(dy, true); 332 | } 333 | } 334 | mLastMotionY = y; 335 | } 336 | break; 337 | } 338 | case MotionEventCompat.ACTION_POINTER_DOWN: { 339 | pointerIndex = MotionEventCompat.getActionIndex(ev); 340 | if (pointerIndex < 0) { 341 | Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); 342 | return false; 343 | } 344 | mActivePointerId = ev.getPointerId(pointerIndex); 345 | break; 346 | } 347 | 348 | case MotionEventCompat.ACTION_POINTER_UP: 349 | onSecondaryPointerUp(ev); 350 | break; 351 | 352 | case MotionEvent.ACTION_UP: { 353 | pointerIndex = ev.findPointerIndex(mActivePointerId); 354 | if (pointerIndex < 0) { 355 | Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id."); 356 | return false; 357 | } 358 | 359 | if (mIsDragging) { 360 | mIsDragging = false; 361 | mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 362 | final float vy = mVelocityTracker.getYVelocity(mActivePointerId); 363 | finishPull((int) vy); 364 | } 365 | mActivePointerId = INVALID_POINTER; 366 | releaseVelocityTracker(); 367 | return false; 368 | } 369 | case MotionEvent.ACTION_CANCEL: 370 | releaseVelocityTracker(); 371 | return false; 372 | } 373 | 374 | return true; 375 | } 376 | 377 | 378 | private void ensureTargetView() { 379 | if (mTargetView == null) { 380 | for (int i = 0; i < getChildCount(); i++) { 381 | View view = getChildAt(i); 382 | if (!view.equals(mRefreshView)) { 383 | mTargetView = view; 384 | } 385 | } 386 | } 387 | } 388 | 389 | private void finishPull(int vy) { 390 | if (mTargetCurrentOffset >= mTargetRefreshOffset) { 391 | onRefresh(); 392 | if (vy > 0) { 393 | mNeedScrollToRefreshPos = true; 394 | mScroller.fling(0, mTargetCurrentOffset, 0, vy, 395 | 0, 0, mTargetInitOffset, Integer.MAX_VALUE); 396 | invalidate(); 397 | } else if (vy < 0) { 398 | mNeedScrollToInitOrRefreshPosDependOnCurrentOffset = true; 399 | mScroller.fling(0, mTargetCurrentOffset, 0, vy, 400 | 0, 0, mTargetInitOffset, Integer.MAX_VALUE); 401 | invalidate(); 402 | } else { 403 | mNeedScrollToRefreshPos = true; 404 | invalidate(); 405 | } 406 | } else { 407 | if (vy > 0) { 408 | mNeedDecideDoRefreshOrNot = true; 409 | mScroller.fling(0, mTargetCurrentOffset, 0, vy, 410 | 0, 0, mTargetInitOffset, Integer.MAX_VALUE); 411 | invalidate(); 412 | } else if (vy < 0) { 413 | mNeedScrollToInitPos = true; 414 | mScroller.fling(0, mTargetCurrentOffset, 0, vy, 415 | 0, 0, mTargetInitOffset, Integer.MAX_VALUE); 416 | invalidate(); 417 | } else { 418 | mNeedScrollToInitPos = true; 419 | invalidate(); 420 | } 421 | } 422 | } 423 | 424 | 425 | protected void onRefresh() { 426 | if (mIsRefreshing) { 427 | return; 428 | } 429 | mIRefreshView.doRefresh(); 430 | if (mListener != null) { 431 | mListener.onRefresh(); 432 | } 433 | } 434 | 435 | public void doRefresh() { 436 | if (mRefreshCurrentOffset == mRefreshEndOffset) { 437 | onRefresh(); 438 | return; 439 | } 440 | ensureTargetView(); 441 | onRefresh(); 442 | mNeedScrollToRefreshPos = true; 443 | invalidate(); 444 | } 445 | 446 | public void finishRefresh() { 447 | mIsRefreshing = false; 448 | mIRefreshView.stop(); 449 | mNeedScrollToRefreshPos = false; 450 | mNeedDecideDoRefreshOrNot = false; 451 | mNeedScrollToInitPos = true; 452 | invalidate(); 453 | } 454 | 455 | 456 | public void setEnableOverPull(boolean enableOverPull) { 457 | mEnableOverPull = enableOverPull; 458 | } 459 | 460 | 461 | private void onSecondaryPointerUp(MotionEvent ev) { 462 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 463 | final int pointerId = ev.getPointerId(pointerIndex); 464 | if (pointerId == mActivePointerId) { 465 | // This was our active pointer going up. Choose a new 466 | // active pointer and adjust accordingly. 467 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 468 | mActivePointerId = ev.getPointerId(newPointerIndex); 469 | } 470 | } 471 | 472 | private void reset() { 473 | moveTargetViewTo(mTargetInitOffset, false); 474 | mIRefreshView.stop(); 475 | resetTag(); 476 | } 477 | 478 | private void resetTag() { 479 | mNeedScrollToRefreshPos = false; 480 | mNeedDecideDoRefreshOrNot = false; 481 | mNeedScrollToInitPos = false; 482 | mNeedScrollToInitOrRefreshPosDependOnCurrentOffset = false; 483 | } 484 | 485 | private void startDragging(float y) { 486 | final float yDiff = y - mInitialDownY; 487 | if ((yDiff > mTouchSlop || (yDiff < -mTouchSlop && mTargetCurrentOffset > mTargetInitOffset)) && !mIsDragging) { 488 | mInitialMotionY = mInitialDownY + mTouchSlop; 489 | mLastMotionY = mInitialMotionY; 490 | mIsDragging = true; 491 | } 492 | } 493 | 494 | @Override 495 | protected void onDetachedFromWindow() { 496 | super.onDetachedFromWindow(); 497 | reset(); 498 | } 499 | 500 | @Override 501 | public void setEnabled(boolean enabled) { 502 | super.setEnabled(enabled); 503 | if (!enabled) { 504 | reset(); 505 | invalidate(); 506 | } 507 | } 508 | 509 | public boolean canChildScrollUp() { 510 | if (mChildScrollUpCallback != null) { 511 | return mChildScrollUpCallback.canChildScrollUp(this, mTargetView); 512 | } 513 | if (android.os.Build.VERSION.SDK_INT < 14) { 514 | if (mTargetView instanceof AbsListView) { 515 | final AbsListView absListView = (AbsListView) mTargetView; 516 | return absListView.getChildCount() > 0 517 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 518 | .getTop() < absListView.getPaddingTop()); 519 | } else { 520 | return ViewCompat.canScrollVertically(mTargetView, -1) || mTargetView.getScrollY() > 0; 521 | } 522 | } else { 523 | return ViewCompat.canScrollVertically(mTargetView, -1); 524 | } 525 | } 526 | 527 | 528 | @Override 529 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 530 | Log.i(TAG, "onStartNestedScroll: nestedScrollAxes = " + nestedScrollAxes); 531 | return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 532 | } 533 | 534 | @Override 535 | public void onNestedScrollAccepted(View child, View target, int axes) { 536 | Log.i(TAG, "onNestedScrollAccepted: axes = " + axes); 537 | mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); 538 | mNestedScrollInProgress = true; 539 | } 540 | 541 | @Override 542 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 543 | Log.i(TAG, "onNestedPreScroll: dx = " + dx + " ; dy = " + dy); 544 | int parentCanConsume = mTargetCurrentOffset - mTargetInitOffset; 545 | if (dy > 0 && parentCanConsume > 0) { 546 | if (dy >= parentCanConsume) { 547 | consumed[1] = parentCanConsume; 548 | moveTargetViewTo(mTargetInitOffset, true); 549 | } else { 550 | consumed[1] = dy; 551 | moveTargetView(-dy, true); 552 | } 553 | } 554 | } 555 | 556 | @Override 557 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { 558 | Log.i(TAG, "onNestedScroll: dxConsumed = " + dxConsumed + " ; dyConsumed = " + dyConsumed + 559 | " ; dxUnconsumed = " + dxUnconsumed + " ; dyUnconsumed = " + dyUnconsumed); 560 | if (dyUnconsumed < 0 && !canChildScrollUp()) { 561 | moveTargetView(-dyUnconsumed, true); 562 | } 563 | } 564 | 565 | @Override 566 | public int getNestedScrollAxes() { 567 | return mNestedScrollingParentHelper.getNestedScrollAxes(); 568 | } 569 | 570 | @Override 571 | public void onStopNestedScroll(View child) { 572 | Log.i(TAG, "onStopNestedScroll"); 573 | mNestedScrollingParentHelper.onStopNestedScroll(child); 574 | mNestedScrollInProgress = false; 575 | finishPull(0); 576 | } 577 | 578 | @Override 579 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 580 | Log.i(TAG, "onNestedPreFling: mTargetCurrentOffset = " + mTargetCurrentOffset + 581 | " ; velocityX = " + velocityX + " ; velocityY = " + velocityY); 582 | if (mTargetCurrentOffset > mTargetInitOffset) { 583 | return true; 584 | } 585 | return false; 586 | } 587 | 588 | private void moveTargetView(float dy, boolean isDragging) { 589 | int target = (int) (mTargetCurrentOffset + dy * mDragRate); 590 | moveTargetViewTo(target, isDragging); 591 | } 592 | 593 | private void moveTargetViewTo(int target, boolean isDragging) { 594 | target = Math.max(target, mTargetInitOffset); 595 | if (!mEnableOverPull) { 596 | target = Math.min(target, mTargetRefreshOffset); 597 | } 598 | if (target != mTargetCurrentOffset) { 599 | ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrentOffset); 600 | mTargetCurrentOffset = target; 601 | int total = mTargetRefreshOffset - mTargetInitOffset; 602 | if (isDragging) { 603 | mIRefreshView.onPull(Math.min(mTargetCurrentOffset - mTargetInitOffset, total), total, 604 | mTargetCurrentOffset - mTargetRefreshOffset); 605 | } 606 | if (mListener != null) { 607 | mListener.onMoveTarget(mTargetCurrentOffset); 608 | } 609 | 610 | int refreshOffset; 611 | if (mTargetCurrentOffset >= mTargetRefreshOffset) { 612 | refreshOffset = mRefreshEndOffset; 613 | } else if (mTargetCurrentOffset <= mTargetInitOffset) { 614 | refreshOffset = mRefreshInitOffset; 615 | } else { 616 | float percent = (mTargetCurrentOffset - mTargetInitOffset) * 1.0f / mTargetRefreshOffset - mTargetInitOffset; 617 | refreshOffset = (int) (mRefreshInitOffset + percent * (mRefreshEndOffset - mRefreshInitOffset)); 618 | } 619 | 620 | if (refreshOffset != mRefreshCurrentOffset) { 621 | ViewCompat.offsetTopAndBottom(mRefreshView, refreshOffset - mRefreshCurrentOffset); 622 | mRefreshCurrentOffset = refreshOffset; 623 | if (mListener != null) { 624 | mListener.onMoveRefreshView(mTargetCurrentOffset); 625 | } 626 | } 627 | } 628 | 629 | } 630 | 631 | private void acquireVelocityTracker(final MotionEvent event) { 632 | if (null == mVelocityTracker) { 633 | mVelocityTracker = VelocityTracker.obtain(); 634 | } 635 | mVelocityTracker.addMovement(event); 636 | } 637 | 638 | private void releaseVelocityTracker() { 639 | if (null != mVelocityTracker) { 640 | mVelocityTracker.clear(); 641 | mVelocityTracker.recycle(); 642 | mVelocityTracker = null; 643 | } 644 | } 645 | 646 | @Override 647 | public void computeScroll() { 648 | if (mScroller.computeScrollOffset()) { 649 | int offsetY = mScroller.getCurrY(); 650 | moveTargetViewTo(offsetY, false); 651 | invalidate(); 652 | } else if (mNeedScrollToInitPos) { 653 | mNeedScrollToInitPos = false; 654 | if (mTargetCurrentOffset == mTargetInitOffset) { 655 | if (mScroller.getCurrVelocity() > 0) { 656 | // 如果还有速度,则传递给子view 657 | if (mTargetView instanceof RecyclerView) { 658 | ((RecyclerView) mTargetView).fling(0, (int) mScroller.getCurrVelocity()); 659 | } else if (mTargetView instanceof AbsListView && android.os.Build.VERSION.SDK_INT >= 21) { 660 | ((AbsListView) mTargetView).fling((int) mScroller.getCurrVelocity()); 661 | } 662 | } 663 | return; 664 | } 665 | mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset); 666 | invalidate(); 667 | } else if (mNeedScrollToRefreshPos) { 668 | mNeedScrollToRefreshPos = false; 669 | mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetRefreshOffset - mTargetCurrentOffset); 670 | invalidate(); 671 | } else if (mNeedDecideDoRefreshOrNot) { 672 | mNeedDecideDoRefreshOrNot = false; 673 | if (mTargetCurrentOffset >= mTargetRefreshOffset) { 674 | doRefresh(); 675 | mNeedScrollToRefreshPos = true; 676 | } else { 677 | mNeedScrollToInitPos = true; 678 | } 679 | invalidate(); 680 | } else if (mNeedScrollToInitOrRefreshPosDependOnCurrentOffset) { 681 | mNeedScrollToInitOrRefreshPosDependOnCurrentOffset = false; 682 | if (mTargetCurrentOffset >= mTargetRefreshOffset) { 683 | mNeedScrollToRefreshPos = true; 684 | } else { 685 | mNeedScrollToInitPos = true; 686 | } 687 | invalidate(); 688 | } 689 | } 690 | 691 | 692 | public interface OnPullListener { 693 | void onMoveTarget(int offset); 694 | 695 | void onMoveRefreshView(int offset); 696 | 697 | void onRefresh(); 698 | } 699 | 700 | 701 | public interface OnChildScrollUpCallback { 702 | boolean canChildScrollUp(QMUIPullRefreshLayout parent, @Nullable View child); 703 | } 704 | 705 | public static class RefreshView extends ImageView implements IRefreshView { 706 | private static final int MAX_ALPHA = 255; 707 | private static final float TRIM_RATE = 0.85f; 708 | private static final float TRIM_OFFSET = 0.4f; 709 | 710 | private QMUIMaterialProgressDrawable mProgress; 711 | 712 | public RefreshView(Context context) { 713 | super(context); 714 | mProgress = new QMUIMaterialProgressDrawable(getContext(), this); 715 | mProgress.setColorSchemeColors(0x00ff00); 716 | mProgress.updateSizes(QMUIMaterialProgressDrawable.LARGE); 717 | mProgress.setAlpha(MAX_ALPHA); 718 | mProgress.setArrowScale(1.1f); 719 | setImageDrawable(mProgress); 720 | } 721 | 722 | @Override 723 | public void onPull(int offset, int total, int overPull) { 724 | float end = TRIM_RATE * offset / total; 725 | float rotate = TRIM_OFFSET * offset / total; 726 | if (overPull > 0) { 727 | rotate += TRIM_OFFSET * overPull / total; 728 | } 729 | mProgress.showArrow(true); 730 | mProgress.setStartEndTrim(0, end); 731 | mProgress.setProgressRotation(rotate); 732 | } 733 | 734 | public void setSize(int size) { 735 | if (size != QMUIMaterialProgressDrawable.LARGE && size != QMUIMaterialProgressDrawable.DEFAULT) { 736 | return; 737 | } 738 | setImageDrawable(null); 739 | mProgress.updateSizes(size); 740 | setImageDrawable(mProgress); 741 | } 742 | 743 | public void stop() { 744 | mProgress.stop(); 745 | } 746 | 747 | public void doRefresh() { 748 | if (!mProgress.isRunning()) { 749 | mProgress.start(); 750 | } 751 | } 752 | 753 | public void setColorSchemeResources(@ColorRes int... colorResIds) { 754 | final Context context = getContext(); 755 | int[] colorRes = new int[colorResIds.length]; 756 | for (int i = 0; i < colorResIds.length; i++) { 757 | colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); 758 | } 759 | setColorSchemeColors(colorRes); 760 | } 761 | 762 | public void setColorSchemeColors(@ColorInt int... colors) { 763 | mProgress.setColorSchemeColors(colors); 764 | } 765 | } 766 | 767 | public interface IRefreshView { 768 | void stop(); 769 | 770 | void doRefresh(); 771 | 772 | void setColorSchemeResources(@ColorRes int... colorResIds); 773 | 774 | void setColorSchemeColors(@ColorInt int... colors); 775 | 776 | void onPull(int offset, int total, int overPull); 777 | } 778 | } -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #F4F5F7 7 | #ffffff 8 | 9 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 30dp 7 | -30dp 8 | 9 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DemoAndroid 3 | 4 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /DemoAndroid/app/src/test/java/org/cgsdream/demo_android/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.cgsdream.demo_android; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /DemoAndroid/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:2.2.2' 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 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /DemoAndroid/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 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /DemoAndroid/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/DemoAndroid/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /DemoAndroid/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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.14.1-all.zip 7 | -------------------------------------------------------------------------------- /DemoAndroid/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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /DemoAndroid/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 | -------------------------------------------------------------------------------- /DemoAndroid/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 96F7D14B1E8E8FDC0006D7D5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D14A1E8E8FDC0006D7D5 /* AppDelegate.swift */; }; 11 | 96F7D14D1E8E8FDC0006D7D5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D14C1E8E8FDC0006D7D5 /* ViewController.swift */; }; 12 | 96F7D1501E8E8FDC0006D7D5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 96F7D14E1E8E8FDC0006D7D5 /* Main.storyboard */; }; 13 | 96F7D1521E8E8FDC0006D7D5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96F7D1511E8E8FDC0006D7D5 /* Assets.xcassets */; }; 14 | 96F7D1551E8E8FDC0006D7D5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 96F7D1531E8E8FDC0006D7D5 /* LaunchScreen.storyboard */; }; 15 | 96F7D1621E8E91C40006D7D5 /* UITableView+SM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D1611E8E91C40006D7D5 /* UITableView+SM.swift */; }; 16 | 96F7D1641E8E91E90006D7D5 /* SMRefreshComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D1631E8E91E90006D7D5 /* SMRefreshComponent.swift */; }; 17 | 96F7D1661E8E92010006D7D5 /* UIView+SM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D1651E8E92010006D7D5 /* UIView+SM.swift */; }; 18 | 96F7D1681E8E92420006D7D5 /* SMRefreshHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D1671E8E92420006D7D5 /* SMRefreshHeader.swift */; }; 19 | 96F7D16A1E8E92720006D7D5 /* UIScrollView+SM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D1691E8E92720006D7D5 /* UIScrollView+SM.swift */; }; 20 | 96F7D16C1E8E93200006D7D5 /* WWRefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D16B1E8E93200006D7D5 /* WWRefreshView.swift */; }; 21 | 96F7D16E1E8EAFB80006D7D5 /* ResuableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F7D16D1E8EAFB80006D7D5 /* ResuableView.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 96F7D1471E8E8FDC0006D7D5 /* Demo_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 96F7D14A1E8E8FDC0006D7D5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 96F7D14C1E8E8FDC0006D7D5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 28 | 96F7D14F1E8E8FDC0006D7D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 96F7D1511E8E8FDC0006D7D5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 96F7D1541E8E8FDC0006D7D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 96F7D1561E8E8FDC0006D7D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 96F7D1611E8E91C40006D7D5 /* UITableView+SM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+SM.swift"; sourceTree = ""; }; 33 | 96F7D1631E8E91E90006D7D5 /* SMRefreshComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMRefreshComponent.swift; sourceTree = ""; }; 34 | 96F7D1651E8E92010006D7D5 /* UIView+SM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SM.swift"; sourceTree = ""; }; 35 | 96F7D1671E8E92420006D7D5 /* SMRefreshHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMRefreshHeader.swift; sourceTree = ""; }; 36 | 96F7D1691E8E92720006D7D5 /* UIScrollView+SM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+SM.swift"; sourceTree = ""; }; 37 | 96F7D16B1E8E93200006D7D5 /* WWRefreshView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WWRefreshView.swift; sourceTree = ""; }; 38 | 96F7D16D1E8EAFB80006D7D5 /* ResuableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResuableView.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 96F7D1441E8E8FDC0006D7D5 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 96F7D13E1E8E8FDC0006D7D5 = { 53 | isa = PBXGroup; 54 | children = ( 55 | 96F7D1491E8E8FDC0006D7D5 /* Demo_iOS */, 56 | 96F7D1481E8E8FDC0006D7D5 /* Products */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | 96F7D1481E8E8FDC0006D7D5 /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 96F7D1471E8E8FDC0006D7D5 /* Demo_iOS.app */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | 96F7D1491E8E8FDC0006D7D5 /* Demo_iOS */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 96F7D15C1E8E91060006D7D5 /* Common */, 72 | 96F7D14A1E8E8FDC0006D7D5 /* AppDelegate.swift */, 73 | 96F7D14C1E8E8FDC0006D7D5 /* ViewController.swift */, 74 | 96F7D14E1E8E8FDC0006D7D5 /* Main.storyboard */, 75 | 96F7D1511E8E8FDC0006D7D5 /* Assets.xcassets */, 76 | 96F7D1531E8E8FDC0006D7D5 /* LaunchScreen.storyboard */, 77 | 96F7D1561E8E8FDC0006D7D5 /* Info.plist */, 78 | 96F7D16B1E8E93200006D7D5 /* WWRefreshView.swift */, 79 | ); 80 | path = Demo_iOS; 81 | sourceTree = ""; 82 | }; 83 | 96F7D15C1E8E91060006D7D5 /* Common */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 96F7D1611E8E91C40006D7D5 /* UITableView+SM.swift */, 87 | 96F7D1631E8E91E90006D7D5 /* SMRefreshComponent.swift */, 88 | 96F7D1651E8E92010006D7D5 /* UIView+SM.swift */, 89 | 96F7D1671E8E92420006D7D5 /* SMRefreshHeader.swift */, 90 | 96F7D1691E8E92720006D7D5 /* UIScrollView+SM.swift */, 91 | 96F7D16D1E8EAFB80006D7D5 /* ResuableView.swift */, 92 | ); 93 | name = Common; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | 96F7D1461E8E8FDC0006D7D5 /* Demo_iOS */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = 96F7D1591E8E8FDC0006D7D5 /* Build configuration list for PBXNativeTarget "Demo_iOS" */; 102 | buildPhases = ( 103 | 96F7D1431E8E8FDC0006D7D5 /* Sources */, 104 | 96F7D1441E8E8FDC0006D7D5 /* Frameworks */, 105 | 96F7D1451E8E8FDC0006D7D5 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = Demo_iOS; 112 | productName = Demo_iOS; 113 | productReference = 96F7D1471E8E8FDC0006D7D5 /* Demo_iOS.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 96F7D13F1E8E8FDC0006D7D5 /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | LastSwiftUpdateCheck = 0820; 123 | LastUpgradeCheck = 0820; 124 | ORGANIZATIONNAME = cgspine; 125 | TargetAttributes = { 126 | 96F7D1461E8E8FDC0006D7D5 = { 127 | CreatedOnToolsVersion = 8.2.1; 128 | DevelopmentTeam = CB5AEAEPW3; 129 | ProvisioningStyle = Automatic; 130 | }; 131 | }; 132 | }; 133 | buildConfigurationList = 96F7D1421E8E8FDC0006D7D5 /* Build configuration list for PBXProject "Demo_iOS" */; 134 | compatibilityVersion = "Xcode 3.2"; 135 | developmentRegion = English; 136 | hasScannedForEncodings = 0; 137 | knownRegions = ( 138 | en, 139 | Base, 140 | ); 141 | mainGroup = 96F7D13E1E8E8FDC0006D7D5; 142 | productRefGroup = 96F7D1481E8E8FDC0006D7D5 /* Products */; 143 | projectDirPath = ""; 144 | projectRoot = ""; 145 | targets = ( 146 | 96F7D1461E8E8FDC0006D7D5 /* Demo_iOS */, 147 | ); 148 | }; 149 | /* End PBXProject section */ 150 | 151 | /* Begin PBXResourcesBuildPhase section */ 152 | 96F7D1451E8E8FDC0006D7D5 /* Resources */ = { 153 | isa = PBXResourcesBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | 96F7D1551E8E8FDC0006D7D5 /* LaunchScreen.storyboard in Resources */, 157 | 96F7D1521E8E8FDC0006D7D5 /* Assets.xcassets in Resources */, 158 | 96F7D1501E8E8FDC0006D7D5 /* Main.storyboard in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | 96F7D1431E8E8FDC0006D7D5 /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 96F7D1681E8E92420006D7D5 /* SMRefreshHeader.swift in Sources */, 170 | 96F7D14D1E8E8FDC0006D7D5 /* ViewController.swift in Sources */, 171 | 96F7D1641E8E91E90006D7D5 /* SMRefreshComponent.swift in Sources */, 172 | 96F7D14B1E8E8FDC0006D7D5 /* AppDelegate.swift in Sources */, 173 | 96F7D1621E8E91C40006D7D5 /* UITableView+SM.swift in Sources */, 174 | 96F7D1661E8E92010006D7D5 /* UIView+SM.swift in Sources */, 175 | 96F7D16C1E8E93200006D7D5 /* WWRefreshView.swift in Sources */, 176 | 96F7D16A1E8E92720006D7D5 /* UIScrollView+SM.swift in Sources */, 177 | 96F7D16E1E8EAFB80006D7D5 /* ResuableView.swift in Sources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXSourcesBuildPhase section */ 182 | 183 | /* Begin PBXVariantGroup section */ 184 | 96F7D14E1E8E8FDC0006D7D5 /* Main.storyboard */ = { 185 | isa = PBXVariantGroup; 186 | children = ( 187 | 96F7D14F1E8E8FDC0006D7D5 /* Base */, 188 | ); 189 | name = Main.storyboard; 190 | sourceTree = ""; 191 | }; 192 | 96F7D1531E8E8FDC0006D7D5 /* LaunchScreen.storyboard */ = { 193 | isa = PBXVariantGroup; 194 | children = ( 195 | 96F7D1541E8E8FDC0006D7D5 /* Base */, 196 | ); 197 | name = LaunchScreen.storyboard; 198 | sourceTree = ""; 199 | }; 200 | /* End PBXVariantGroup section */ 201 | 202 | /* Begin XCBuildConfiguration section */ 203 | 96F7D1571E8E8FDC0006D7D5 /* Debug */ = { 204 | isa = XCBuildConfiguration; 205 | buildSettings = { 206 | ALWAYS_SEARCH_USER_PATHS = NO; 207 | CLANG_ANALYZER_NONNULL = YES; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 209 | CLANG_CXX_LIBRARY = "libc++"; 210 | CLANG_ENABLE_MODULES = YES; 211 | CLANG_ENABLE_OBJC_ARC = YES; 212 | CLANG_WARN_BOOL_CONVERSION = YES; 213 | CLANG_WARN_CONSTANT_CONVERSION = YES; 214 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 215 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 216 | CLANG_WARN_EMPTY_BODY = YES; 217 | CLANG_WARN_ENUM_CONVERSION = YES; 218 | CLANG_WARN_INFINITE_RECURSION = YES; 219 | CLANG_WARN_INT_CONVERSION = YES; 220 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = dwarf; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_TESTABILITY = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu99; 230 | GCC_DYNAMIC_NO_PIC = NO; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_OPTIMIZATION_LEVEL = 0; 233 | GCC_PREPROCESSOR_DEFINITIONS = ( 234 | "DEBUG=1", 235 | "$(inherited)", 236 | ); 237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 239 | GCC_WARN_UNDECLARED_SELECTOR = YES; 240 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 241 | GCC_WARN_UNUSED_FUNCTION = YES; 242 | GCC_WARN_UNUSED_VARIABLE = YES; 243 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 244 | MTL_ENABLE_DEBUG_INFO = YES; 245 | ONLY_ACTIVE_ARCH = YES; 246 | SDKROOT = iphoneos; 247 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 248 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 249 | }; 250 | name = Debug; 251 | }; 252 | 96F7D1581E8E8FDC0006D7D5 /* Release */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ALWAYS_SEARCH_USER_PATHS = NO; 256 | CLANG_ANALYZER_NONNULL = YES; 257 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 258 | CLANG_CXX_LIBRARY = "libc++"; 259 | CLANG_ENABLE_MODULES = YES; 260 | CLANG_ENABLE_OBJC_ARC = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_CONSTANT_CONVERSION = YES; 263 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 264 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 265 | CLANG_WARN_EMPTY_BODY = YES; 266 | CLANG_WARN_ENUM_CONVERSION = YES; 267 | CLANG_WARN_INFINITE_RECURSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 270 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 274 | COPY_PHASE_STRIP = NO; 275 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 276 | ENABLE_NS_ASSERTIONS = NO; 277 | ENABLE_STRICT_OBJC_MSGSEND = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu99; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 287 | MTL_ENABLE_DEBUG_INFO = NO; 288 | SDKROOT = iphoneos; 289 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 290 | VALIDATE_PRODUCT = YES; 291 | }; 292 | name = Release; 293 | }; 294 | 96F7D15A1E8E8FDC0006D7D5 /* Debug */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | DEVELOPMENT_TEAM = CB5AEAEPW3; 299 | INFOPLIST_FILE = Demo_iOS/Info.plist; 300 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 301 | PRODUCT_BUNDLE_IDENTIFIER = "org.cgsdream.Demo-iOS"; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_VERSION = 3.0; 304 | }; 305 | name = Debug; 306 | }; 307 | 96F7D15B1E8E8FDC0006D7D5 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | DEVELOPMENT_TEAM = CB5AEAEPW3; 312 | INFOPLIST_FILE = Demo_iOS/Info.plist; 313 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 314 | PRODUCT_BUNDLE_IDENTIFIER = "org.cgsdream.Demo-iOS"; 315 | PRODUCT_NAME = "$(TARGET_NAME)"; 316 | SWIFT_VERSION = 3.0; 317 | }; 318 | name = Release; 319 | }; 320 | /* End XCBuildConfiguration section */ 321 | 322 | /* Begin XCConfigurationList section */ 323 | 96F7D1421E8E8FDC0006D7D5 /* Build configuration list for PBXProject "Demo_iOS" */ = { 324 | isa = XCConfigurationList; 325 | buildConfigurations = ( 326 | 96F7D1571E8E8FDC0006D7D5 /* Debug */, 327 | 96F7D1581E8E8FDC0006D7D5 /* Release */, 328 | ); 329 | defaultConfigurationIsVisible = 0; 330 | defaultConfigurationName = Release; 331 | }; 332 | 96F7D1591E8E8FDC0006D7D5 /* Build configuration list for PBXNativeTarget "Demo_iOS" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | 96F7D15A1E8E8FDC0006D7D5 /* Debug */, 336 | 96F7D15B1E8E8FDC0006D7D5 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | }; 340 | /* End XCConfigurationList section */ 341 | }; 342 | rootObject = 96F7D13F1E8E8FDC0006D7D5 /* Project object */; 343 | } 344 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/project.xcworkspace/xcuserdata/cgspine.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgspine/WeworkPullRefreshAnimation/030cfeedb7a8aedb2aea3a21dd403bdc9d83c172/Demo_iOS/Demo_iOS.xcodeproj/project.xcworkspace/xcuserdata/cgspine.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/xcuserdata/cgspine.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/xcuserdata/cgspine.xcuserdatad/xcschemes/Demo_iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS.xcodeproj/xcuserdata/cgspine.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Demo_iOS.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 96F7D1461E8E8FDC0006D7D5 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | window = UIWindow() 20 | window?.frame = UIScreen.main.bounds 21 | window?.rootViewController = ViewController() 22 | 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 42 | } 43 | 44 | func applicationWillTerminate(_ application: UIApplication) { 45 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/ResuableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResuableView.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ReusableView: class {} 12 | 13 | extension ReusableView where Self: UIView { 14 | 15 | static var reuseIdentifier: String { 16 | return String(describing: self) 17 | } 18 | } 19 | 20 | extension UITableViewCell: ReusableView { } 21 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/SMRefreshComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMRefreshComponent.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum RefreshState { 12 | case idle // 默认状态 13 | case pulling // 14 | case overPulling // 松开就可刷新的状态 15 | case refreshing // 刷新中状态 16 | case willRefresh // 即将刷新的状态(这是为了防止view还没有显示出来就调用了BeginRefresh,判断方法是self.window == nil) 17 | case noMoreData // 数据加载完状态 18 | } 19 | 20 | class SMRefreshComponent: UIView { 21 | var state:RefreshState = .idle { 22 | willSet{ 23 | if newValue != state { 24 | self.notifyStateChange(state, newState: newValue) 25 | } 26 | } 27 | } 28 | 29 | fileprivate var panGestureRecognizer: UIPanGestureRecognizer? 30 | 31 | var scrollView: UIScrollView? 32 | 33 | var scrollViewOriginInset: UIEdgeInsets = UIEdgeInsets.zero 34 | 35 | var refreshingCallback: (() -> Void)? 36 | 37 | var pullingPercent:CGFloat = 0 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | prepare() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | } 47 | 48 | 49 | override func willMove(toSuperview newSuperview: UIView?) { 50 | if let newSuperview = newSuperview { 51 | // 确保其父类是UIScrollView 52 | if !newSuperview.isKind(of: UIScrollView.self) { 53 | return 54 | } 55 | // 旧的父控件移除KVO 56 | self.removeObservers() 57 | 58 | self.sm_width = newSuperview.sm_width 59 | self.sm_x = 0 60 | self.scrollView = newSuperview as? UIScrollView 61 | if let scrollView = self.scrollView { 62 | scrollView.alwaysBounceVertical = true 63 | self.scrollViewOriginInset = scrollView.contentInset 64 | } 65 | 66 | self.addObservers() 67 | } 68 | } 69 | 70 | override func draw(_ rect: CGRect) { 71 | super.draw(rect) 72 | 73 | if self.state == .willRefresh { 74 | self.state = .refreshing 75 | } 76 | } 77 | 78 | 79 | func prepare() { 80 | self.backgroundColor = UIColor.clear 81 | } 82 | 83 | func notifyStateChange(_ oldState:RefreshState, newState:RefreshState) { 84 | DispatchQueue.main.async(execute: { 85 | self.setNeedsLayout() 86 | }) 87 | } 88 | 89 | 90 | // MARK: KVO监听 91 | 92 | fileprivate func addObservers(){ 93 | let options:NSKeyValueObservingOptions = [.new, .old] 94 | self.scrollView?.addObserver(self, forKeyPath: "contentOffset", options: options, context: nil) 95 | self.scrollView?.addObserver(self, forKeyPath: "contentSize", options: options, context: nil) 96 | self.panGestureRecognizer = self.scrollView?.panGestureRecognizer 97 | self.panGestureRecognizer?.addObserver(self, forKeyPath: "state", options: options, context: nil) 98 | } 99 | 100 | fileprivate func removeObservers(){ 101 | self.superview?.removeObserver(self, forKeyPath: "contentOffset") 102 | self.superview?.removeObserver(self, forKeyPath: "contentSize") 103 | self.panGestureRecognizer?.removeObserver(self, forKeyPath: "state") 104 | } 105 | 106 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 107 | if !self.isUserInteractionEnabled { 108 | return 109 | } 110 | 111 | 112 | if let keyPath = keyPath { 113 | // 这个就算看不见也需要处理 114 | if keyPath == "contentOffset" { 115 | self.scrollViewContentOffsetDidChange(change) 116 | } 117 | 118 | if self.isHidden { 119 | return 120 | } 121 | 122 | if keyPath == "contentSize" { 123 | self.scrollViewContentSizeDidChange(change) 124 | } else if keyPath == "state" { 125 | self.scrollViewPanStateDidChange(change) 126 | } 127 | } 128 | } 129 | 130 | func scrollViewContentOffsetDidChange(_ change: [NSKeyValueChangeKey : Any]?) {} 131 | func scrollViewContentSizeDidChange(_ change: [NSKeyValueChangeKey : Any]?) {} 132 | func scrollViewPanStateDidChange(_ change: [NSKeyValueChangeKey : Any]?) {} 133 | 134 | // MARK: 刷新状态控制 135 | 136 | func beginRefresh() { 137 | self.pullingPercent = 1.0 138 | 139 | if self.window != nil { 140 | self.state = .refreshing 141 | } else { 142 | if self.state != .refreshing { 143 | self.state = .willRefresh 144 | self.setNeedsLayout() 145 | } 146 | } 147 | } 148 | 149 | func endRefresh(){ 150 | DispatchQueue.main.async(execute: { 151 | self.state = .idle 152 | }) 153 | } 154 | 155 | func isRefreshing() -> Bool{ 156 | return state == .refreshing || state == .willRefresh 157 | } 158 | 159 | // MARK: 执行回调 160 | func executeRefreshingCallback() { 161 | DispatchQueue.main.async(execute: { 162 | if let refreshingCallback = self.refreshingCallback { 163 | refreshingCallback() 164 | } 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/SMRefreshHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMRefreshHeader.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public let SMRefreshHeaderHeight: CGFloat = 54 12 | 13 | class SMRefreshHeader: SMRefreshComponent { 14 | var lastUpdatedTimeKey: String = "last_updated_time_key" 15 | /** 上一次下拉刷新成功的时间 */ 16 | var lastUpdatedTime: Date? { 17 | set{ 18 | if let newValue = newValue { 19 | Foundation.UserDefaults.standard.set(newValue, forKey: self.lastUpdatedTimeKey) 20 | Foundation.UserDefaults.standard.synchronize() 21 | } 22 | } 23 | get{ 24 | return Foundation.UserDefaults.standard.object(forKey: self.lastUpdatedTimeKey) as? Date 25 | } 26 | } 27 | /** 忽略多少scrollView的contentInset的top */ 28 | var ignoreScrollViewContentInsetTop: CGFloat = 0 29 | 30 | fileprivate var insetTDelta: CGFloat = 0 31 | 32 | 33 | override func prepare() { 34 | super.prepare() 35 | self.sm_height = SMRefreshHeaderHeight 36 | } 37 | 38 | override func layoutSubviews() { 39 | super.layoutSubviews() 40 | self.sm_y = -self.sm_height - ignoreScrollViewContentInsetTop 41 | 42 | } 43 | 44 | override func scrollViewContentOffsetDidChange(_ change: [NSKeyValueChangeKey : Any]?) { 45 | super.scrollViewContentOffsetDidChange(change) 46 | if self.state == .refreshing { 47 | guard let _ = self.window, let scrollView = self.scrollView else { 48 | return 49 | } 50 | //如果在刷新过程中发生滚动 51 | var insetTop = -scrollView.sm_offsetY > self.scrollViewOriginInset.top ? -scrollView.sm_offsetY : 52 | scrollViewOriginInset.top 53 | insetTop = insetTop > self.sm_height + scrollViewOriginInset.top ? self.sm_height + scrollViewOriginInset.top : insetTop 54 | scrollView.sm_insetTop = insetTop 55 | self.insetTDelta = scrollViewOriginInset.top - insetTop 56 | return 57 | 58 | } 59 | 60 | guard let scrollView = self.scrollView else { 61 | return 62 | } 63 | 64 | scrollViewOriginInset = scrollView.contentInset 65 | 66 | let offsetY = scrollView.sm_offsetY 67 | let happenOffsetY = -self.scrollViewOriginInset.top 68 | // 如果是向上滚动到看不见头部控件,直接返回 69 | if(offsetY > happenOffsetY) { 70 | return 71 | } 72 | 73 | let fullVisisableOffset = happenOffsetY - self.sm_height 74 | let pullingPercent = (happenOffsetY - offsetY) / self.sm_height 75 | 76 | 77 | if scrollView.isDragging { 78 | // 拖拽中 79 | self.pullingPercent = pullingPercent 80 | if self.state == .idle { 81 | if offsetY < fullVisisableOffset { 82 | self.state = .overPulling 83 | } else if offsetY >= fullVisisableOffset && offsetY < happenOffsetY { 84 | self.state = .pulling 85 | } 86 | 87 | } else if self.state == .pulling { 88 | if offsetY < fullVisisableOffset { 89 | self.state = .overPulling 90 | } else if offsetY >= happenOffsetY { 91 | self.state = .idle 92 | } 93 | } else if self.state == .overPulling { 94 | if offsetY >= happenOffsetY { 95 | self.state = .idle 96 | } else if offsetY > fullVisisableOffset { 97 | self.state = .pulling 98 | } 99 | } 100 | } else if self.state == .overPulling { 101 | // 开始刷新 102 | self.beginRefresh() 103 | } else if pullingPercent < 1 { 104 | self.pullingPercent = pullingPercent 105 | } else if pullingPercent <= 0 { 106 | self.state = .idle 107 | } 108 | } 109 | 110 | override func notifyStateChange(_ oldState:RefreshState, newState:RefreshState) { 111 | if newState == .idle || newState == .pulling { 112 | if(oldState == .refreshing){ 113 | self.lastUpdatedTime = Date() 114 | UIView.animate(withDuration: 0.4, animations: { 115 | self.scrollView?.sm_insetTop += self.insetTDelta 116 | }, completion: { finished in 117 | self.pullingPercent = 0.0 118 | }) 119 | } 120 | 121 | } else if newState == .overPulling { 122 | 123 | } else if newState == .refreshing { 124 | DispatchQueue.main.async(execute: { 125 | UIView.animate(withDuration: 0.4, animations: { 126 | let top = self.scrollViewOriginInset.top + self.sm_height 127 | self.scrollView?.sm_insetTop = top 128 | self.scrollView?.setContentOffset(CGPoint(x: 0, y: -top), animated: false) 129 | }, completion: { finished in 130 | self.executeRefreshingCallback() 131 | }) 132 | }) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/UIScrollView+SM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+SM.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView { 12 | fileprivate struct AssociatedKey { 13 | static var sm_refresh_header_key = "sm_refresh_header_key" 14 | } 15 | 16 | var sm_header: SMRefreshHeader? { 17 | get{ 18 | return objc_getAssociatedObject(self, &AssociatedKey.sm_refresh_header_key) as? SMRefreshHeader 19 | } 20 | set{ 21 | if let newValue = newValue { 22 | if self.sm_header == nil || self.sm_header! != newValue { 23 | self.sm_header?.removeFromSuperview() 24 | self.insertSubview(newValue, at: 0) 25 | 26 | self.willChangeValue(forKey: "sm_header") 27 | objc_setAssociatedObject(self, &AssociatedKey.sm_refresh_header_key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 28 | self.didChangeValue(forKey: "sm_header") 29 | } 30 | } 31 | 32 | } 33 | } 34 | 35 | var sm_insetTop: CGFloat { 36 | set{ 37 | var inset = self.contentInset 38 | inset.top = newValue 39 | self.contentInset = inset 40 | } 41 | get{ 42 | return self.contentInset.top 43 | } 44 | } 45 | 46 | var sm_insetLeft: CGFloat { 47 | set{ 48 | var inset = self.contentInset 49 | inset.left = newValue 50 | self.contentInset = inset 51 | } 52 | get{ 53 | return self.contentInset.left 54 | } 55 | } 56 | 57 | var sm_insetBottom: CGFloat { 58 | set{ 59 | var inset = self.contentInset 60 | inset.bottom = newValue 61 | self.contentInset = inset 62 | } 63 | get{ 64 | return self.contentInset.bottom 65 | } 66 | } 67 | 68 | var sm_insetRight: CGFloat { 69 | set{ 70 | var inset = self.contentInset 71 | inset.right = newValue 72 | self.contentInset = inset 73 | } 74 | get{ 75 | return self.contentInset.right 76 | } 77 | } 78 | 79 | var sm_offsetX: CGFloat { 80 | set{ 81 | var offset = self.contentOffset 82 | offset.x = newValue 83 | self.contentOffset = offset 84 | } 85 | get{ 86 | return self.contentOffset.x 87 | } 88 | } 89 | 90 | var sm_offsetY: CGFloat { 91 | set{ 92 | var offset = self.contentOffset 93 | offset.y = newValue 94 | self.contentOffset = offset 95 | } 96 | get{ 97 | return self.contentOffset.y 98 | } 99 | } 100 | 101 | var sm_contentWidth: CGFloat { 102 | set{ 103 | var content = self.contentSize 104 | content.width = newValue 105 | self.contentSize = content 106 | } 107 | get{ 108 | return self.contentSize.width 109 | } 110 | } 111 | 112 | var sm_contentHeight: CGFloat { 113 | set{ 114 | var content = self.contentSize 115 | content.height = newValue 116 | self.contentSize = content 117 | } 118 | get{ 119 | return self.contentSize.height 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/UITableView+SM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+SM.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UITableView { 13 | 14 | func register(_: T.Type) where T: ReusableView { 15 | register(T.classForCoder(), forCellReuseIdentifier: T.reuseIdentifier) 16 | } 17 | 18 | func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T where T: ReusableView { 19 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 20 | fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 21 | } 22 | return cell 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/UIView+SM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+SM.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | var sm_width: CGFloat { 13 | get{ 14 | return self.frame.size.width 15 | } 16 | set{ 17 | var frame = self.frame 18 | frame.size.width = newValue 19 | self.frame = frame 20 | } 21 | } 22 | 23 | var sm_height: CGFloat { 24 | get{ 25 | return self.frame.size.height 26 | } 27 | set{ 28 | var frame = self.frame 29 | frame.size.height = newValue 30 | self.frame = frame 31 | } 32 | } 33 | 34 | var sm_x: CGFloat { 35 | get{ 36 | return self.frame.origin.x 37 | } 38 | set{ 39 | var frame = self.frame 40 | frame.origin.x = newValue 41 | self.frame = frame 42 | } 43 | } 44 | 45 | var sm_y: CGFloat { 46 | get{ 47 | return self.frame.origin.y 48 | } 49 | set{ 50 | var frame = self.frame 51 | frame.origin.y = newValue 52 | self.frame = frame 53 | } 54 | } 55 | 56 | var sm_size: CGSize { 57 | get{ 58 | return self.frame.size 59 | } 60 | set{ 61 | var frame = self.frame 62 | frame.size = newValue 63 | self.frame = frame 64 | } 65 | } 66 | 67 | var sm_origin: CGPoint { 68 | get{ 69 | return self.frame.origin 70 | } 71 | set{ 72 | var frame = self.frame 73 | frame.origin = newValue 74 | self.frame = frame 75 | } 76 | } 77 | 78 | var sm_maxY: CGFloat { 79 | get{ 80 | return self.frame.maxY 81 | } 82 | } 83 | 84 | var sm_maxX: CGFloat { 85 | get{ 86 | return self.frame.maxX 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | lazy var tableView:UITableView = { 13 | let tableView = UITableView() 14 | tableView.delegate = self 15 | tableView.dataSource = self 16 | tableView.register(UITableViewCell.self) 17 | let refreashHeader = WWRefreshView() 18 | refreashHeader.refreshingCallback = { 19 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3), execute: {[weak self] in 20 | self?.tableView.sm_header?.endRefresh() 21 | }) 22 | } 23 | tableView.sm_header = refreashHeader 24 | return tableView 25 | }() 26 | 27 | override func loadView() { 28 | super.loadView() 29 | view.backgroundColor = UIColor.white 30 | self.view.addSubview(tableView) 31 | } 32 | 33 | override func viewDidLayoutSubviews() { 34 | super.viewDidLayoutSubviews() 35 | tableView.frame = self.view.bounds 36 | tableView.sm_y = 20 37 | if let header = tableView.sm_header{ 38 | // TODO why header.sm_width == 0? 39 | //header.sm_width = SMRefreshHeaderHeight 40 | header.sm_x = (tableView.sm_width - SMRefreshHeaderHeight)/2 41 | } 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | 47 | } 48 | 49 | override func didReceiveMemoryWarning() { 50 | super.didReceiveMemoryWarning() 51 | // Dispose of any resources that can be recreated. 52 | } 53 | 54 | } 55 | 56 | extension ViewController: UITableViewDelegate{ 57 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat{ 58 | return 48 59 | } 60 | } 61 | 62 | extension ViewController: UITableViewDataSource{ 63 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ 64 | return 20 65 | } 66 | 67 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{ 68 | let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) 69 | cell.textLabel?.text = "item \(indexPath.row)" 70 | return cell 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Demo_iOS/Demo_iOS/WWRefreshView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWRefreshView.swift 3 | // Demo_iOS 4 | // 5 | // Created by 陈古松 on 2017/3/31. 6 | // Copyright © 2017年 cgspine. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | let ORIGIN_INSET: CGFloat = 12 13 | let ANIMATION_DURATION: CFTimeInterval = 0.8 14 | class WWRefreshView: SMRefreshHeader { 15 | var balls = [Ball]() 16 | var displayLink: CADisplayLink? 17 | var startAnimateTime: CFTimeInterval = 0 18 | override func prepare() { 19 | super.prepare() 20 | sm_width = SMRefreshHeaderHeight 21 | let size = min(sm_width, sm_height) 22 | let op1 = OriginPoint(sm_width/2, sm_height/2 - size / 2 + ORIGIN_INSET) 23 | let op2 = OriginPoint(center.x - size / 2 + ORIGIN_INSET, center.y) 24 | let op3 = OriginPoint(center.x, center.y + size / 2 - ORIGIN_INSET) 25 | let op4 = OriginPoint(center.x + size / 2 - ORIGIN_INSET, center.y) 26 | op1.next = op2 27 | op2.next = op3 28 | op3.next = op4 29 | op4.next = op1 30 | 31 | 32 | let ballRadius: CGFloat = 3.5 33 | let ballSmallRadius: CGFloat = 1.5 34 | 35 | balls.append(Ball(ballRadius, ballSmallRadius, UIColor.red, op1)) 36 | balls.append(Ball(ballRadius, ballSmallRadius, UIColor.blue, op2)) 37 | balls.append(Ball(ballRadius, ballSmallRadius, UIColor.brown, op3)) 38 | balls.append(Ball(ballRadius, ballSmallRadius, UIColor.darkGray, op4)) 39 | 40 | balls.forEach{ 41 | self.layer.addSublayer($0.layer) 42 | $0.draw() 43 | } 44 | displayLink = CADisplayLink(target: self, selector: #selector(displayLinkSelector)) 45 | displayLink?.isPaused = true 46 | displayLink?.add(to: RunLoop.current, forMode: .commonModes) 47 | 48 | } 49 | 50 | func displayLinkSelector(){ 51 | let now = CACurrentMediaTime() 52 | let percent: CGFloat = CGFloat((now - startAnimateTime) / ANIMATION_DURATION) 53 | balls.forEach{ 54 | $0.calculate(percent: percent) 55 | $0.draw() 56 | } 57 | print("displayLinkSelector: \(percent)") 58 | setNeedsDisplay() 59 | if(percent > 1){ 60 | startAnimateTime = now 61 | balls.forEach{ 62 | $0.next() 63 | } 64 | } 65 | } 66 | 67 | 68 | override func notifyStateChange(_ oldState:RefreshState, newState:RefreshState) { 69 | if newState == .refreshing { 70 | startRefreshAnimation() 71 | } 72 | setNeedsDisplay() 73 | super.notifyStateChange(oldState, newState: newState) 74 | } 75 | 76 | override func scrollViewContentOffsetDidChange(_ change: [NSKeyValueChangeKey : Any]?){ 77 | super.scrollViewContentOffsetDidChange(change) 78 | if self.state == .idle || self.state == .pulling || self.state == .overPulling { 79 | balls.forEach{ 80 | $0.calculate(percent: self.pullingPercent) 81 | $0.draw() 82 | } 83 | setNeedsDisplay() 84 | } 85 | } 86 | 87 | func startRefreshAnimation(){ 88 | print("start refresh animation") 89 | startAnimateTime = CACurrentMediaTime() 90 | self.displayLink?.isPaused = false 91 | } 92 | 93 | override func endRefresh() { 94 | super.endRefresh() 95 | self.displayLink?.isPaused = true 96 | } 97 | 98 | } 99 | 100 | class OriginPoint { 101 | var x: CGFloat 102 | var y: CGFloat 103 | var next: OriginPoint? 104 | 105 | init(_ x: CGFloat, _ y: CGFloat){ 106 | self.x = x 107 | self.y = y 108 | } 109 | } 110 | 111 | class Ball{ 112 | var radius:CGFloat 113 | var smallRadius: CGFloat 114 | var op: OriginPoint 115 | var color: UIColor 116 | var layer: CAShapeLayer 117 | 118 | var smallX: CGFloat = 0 119 | var smallY: CGFloat = 0 120 | var x: CGFloat = 0 121 | var y: CGFloat = 0 122 | 123 | init(_ radius: CGFloat, _ smallRadius: CGFloat, _ color: UIColor, _ op:OriginPoint){ 124 | self.radius = radius 125 | self.smallRadius = smallRadius 126 | self.color = color 127 | self.op = op 128 | self.layer = CAShapeLayer() 129 | self.smallX = op.x 130 | self.x = op.x 131 | self.smallY = op.y 132 | self.y = op.y 133 | } 134 | 135 | func next() { 136 | self.op = self.op.next! 137 | } 138 | 139 | 140 | func drawOverCircle() { 141 | if(smallRadius > radius){ 142 | self.layer.path = UIBezierPath(ovalIn: CGRect(x: self.smallX - self.smallRadius, y: self.smallY-self.smallRadius, width: 2 * self.smallRadius, height: 2 * self.smallRadius)).cgPath 143 | }else{ 144 | self.layer.path = UIBezierPath(ovalIn: CGRect(x: self.x - self.radius, y: self.y-self.radius, width: 2 * self.radius, height: 2 * self.radius)).cgPath 145 | } 146 | } 147 | 148 | func calculate(percent: CGFloat){ 149 | let innerPercent = percent > 1 ? 1 : percent 150 | let v: CGFloat = 1.3 151 | let smallChangePoint: CGFloat = 0.5, smallV1: CGFloat = 0.3 152 | let smallV2 = (1 - smallChangePoint * smallV1) / (1 - smallChangePoint) 153 | let ev = min(1, v * innerPercent); 154 | var smallEv: CGFloat 155 | if (innerPercent > smallChangePoint) { 156 | smallEv = smallV2 * (innerPercent - smallChangePoint) + smallChangePoint * smallV1 157 | } else { 158 | smallEv = smallV1 * innerPercent 159 | } 160 | 161 | 162 | let startX = op.x 163 | let startY = op.y 164 | let next = op.next! 165 | let endX = next.x 166 | let endY = next.y 167 | let f = (endY - startY) / (endX - startX) 168 | 169 | self.x = startX + (endX - startX) * ev 170 | self.y = f * (self.x - startX) + startY 171 | self.smallX = startX + (endX - startX) * smallEv 172 | self.smallY = f * (self.smallX - startX) + startY 173 | } 174 | 175 | func draw(){ 176 | if smallX == x && smallY == y { 177 | drawOverCircle() 178 | return 179 | } 180 | 181 | /* 三角函数求四个点 */ 182 | var angle: CGFloat 183 | var x1, y1, smallX1, smallY1, x2, y2, smallX2, smallY2: CGFloat 184 | if (self.smallX == self.x) { 185 | let v = (self.radius - self.smallRadius) / (self.y - self.smallY) 186 | if (v > 1 || v < -1) { 187 | drawOverCircle() 188 | return 189 | } 190 | angle = asin(v) 191 | let sinValue = sin(angle) 192 | let cosValue = cos(angle) 193 | x1 = self.x - radius * cosValue 194 | y1 = self.y - radius * sinValue 195 | x2 = self.x + radius * cosValue 196 | y2 = y1 197 | smallX1 = self.smallX - self.smallRadius * cosValue 198 | smallY1 = self.smallY - self.smallRadius * sinValue 199 | smallX2 = self.smallX + self.smallRadius * cosValue 200 | smallY2 = self.smallY 201 | } else if (self.smallY == self.y) { 202 | let v = (self.radius - self.smallRadius) / (self.x - self.smallX); 203 | if (v > 1 || v < -1) { 204 | drawOverCircle() 205 | return 206 | } 207 | angle = asin(v) 208 | let sinValue = sin(angle) 209 | let cosValue = cos(angle) 210 | x1 = self.x - radius * sinValue 211 | y1 = self.y + radius * cosValue 212 | x2 = x1 213 | y2 = self.y - radius * cosValue 214 | smallX1 = self.smallX - self.smallRadius * sinValue 215 | smallY1 = self.smallY + self.smallRadius * cosValue 216 | smallX2 = smallX1 217 | smallY2 = self.smallY - self.smallRadius * cosValue 218 | } else { 219 | let ab = sqrt(pow(y - smallY, 2) + pow(x - smallX, 2)) 220 | let v = (radius - smallRadius) / ab 221 | if (v > 1 || v < -1) { 222 | drawOverCircle() 223 | return 224 | } 225 | let alpha = asin(v) 226 | let b = atan((smallY - y) / (smallX - x)) 227 | angle = CGFloat(M_PI / 2) - alpha - b 228 | var sinValue = sin(angle) 229 | var cosValue = cos(angle) 230 | smallX1 = smallX - smallRadius * cosValue 231 | smallY1 = smallY + smallRadius * sinValue 232 | x1 = x - radius * cosValue 233 | y1 = y + radius * sinValue 234 | 235 | angle = b - alpha 236 | sinValue = sin(angle) 237 | cosValue = cos(angle) 238 | smallX2 = smallX + smallRadius * sinValue 239 | smallY2 = smallY - smallRadius * cosValue 240 | x2 = x + radius * sinValue 241 | y2 = y - radius * cosValue 242 | 243 | } 244 | 245 | /* 控制点 */ 246 | let centerX = (x + smallX) / 2, centerY = (y + smallY) / 2 247 | let center1X = (x1 + smallX1) / 2, center1y = (y1 + smallY1) / 2 248 | let center2X = (x2 + smallX2) / 2, center2y = (y2 + smallY2) / 2 249 | let k1 = (center1y - centerY) / (center1X - centerX) 250 | let k2 = (center2y - centerY) / (center2X - centerX) 251 | let ctrlV: CGFloat = 0.08 252 | let anchor1X = center1X + (centerX - center1X) * ctrlV, anchor1Y = k1 * (anchor1X - center1X) + center1y 253 | let anchor2X = center2X + (centerX - center2X) * ctrlV, anchor2Y = k2 * (anchor2X - center2X) + center2y 254 | 255 | let cutePath = UIBezierPath() 256 | // what a fuck? 257 | // 通过顺时针、逆时针绘制path与appendPath组合会造成重叠区域镂空的效果,这里要避免 258 | if(smallX < x){ 259 | let pointStart = CGPoint(x: smallX1, y: smallY1) 260 | cutePath.move(to: pointStart) 261 | cutePath.addLine(to: CGPoint(x: smallX2, y: smallY2)) 262 | cutePath.addQuadCurve(to: CGPoint(x: x2, y: y2), controlPoint: CGPoint(x: anchor2X, y: anchor2Y)) 263 | cutePath.addLine(to: CGPoint(x: x1, y: y1)) 264 | cutePath.addQuadCurve(to: pointStart, controlPoint: CGPoint(x: anchor1X, y: anchor1Y)) 265 | }else{ 266 | let pointStart = CGPoint(x: smallX2, y: smallY2) 267 | cutePath.move(to: pointStart) 268 | cutePath.addLine(to: CGPoint(x: smallX1, y: smallY1)) 269 | cutePath.addQuadCurve(to: CGPoint(x: x1, y: y1), controlPoint: CGPoint(x: anchor1X, y: anchor1Y)) 270 | cutePath.addLine(to: CGPoint(x: x2, y: y2)) 271 | cutePath.addQuadCurve(to: pointStart, controlPoint: CGPoint(x: anchor2X, y: anchor2Y)) 272 | } 273 | 274 | let circle1 = UIBezierPath(ovalIn: CGRect(x: self.smallX - self.smallRadius, y: self.smallY-self.smallRadius, width: 2 * self.smallRadius, height: 2 * self.smallRadius)) 275 | cutePath.append(circle1) 276 | let circle2 = UIBezierPath(ovalIn: CGRect(x: self.x - self.radius, y: self.y-self.radius, width: 2 * self.radius, height: 2 * self.radius)) 277 | cutePath.append(circle2) 278 | 279 | layer.path = cutePath.cgPath 280 | self.layer.fillColor = color.cgColor 281 | } 282 | } 283 | 284 | extension WWRefreshView: CAAnimationDelegate { 285 | func animationDidStart(_ anim: CAAnimation){ 286 | 287 | } 288 | 289 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool){ 290 | 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 博文:http://blog.cgsdream.org/2017/04/01/wework_pull_refresh_animation/ 2 | --------------------------------------------------------------------------------