├── 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 |
--------------------------------------------------------------------------------