├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── README.md ├── __tests__ └── index.android.js ├── android ├── .gitignore ├── app │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── rnandroidpulltorefresh │ │ │ ├── AppReactPackage.java │ │ │ ├── MainActivity.java │ │ │ ├── MainApplication.java │ │ │ └── views │ │ │ └── ptr │ │ │ ├── PtrEvent.java │ │ │ └── ReactPtrAndroidManager.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties ├── library │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── sgb │ │ │ └── library │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── sgb │ │ │ │ └── library │ │ │ │ └── ptr │ │ │ │ ├── PtrClassicDefaultHeader.java │ │ │ │ ├── PtrClassicFrameLayout.java │ │ │ │ ├── PtrDefaultHandler.java │ │ │ │ ├── PtrFrameLayout.java │ │ │ │ ├── PtrHandler.java │ │ │ │ ├── PtrUIHandler.java │ │ │ │ ├── PtrUIHandlerHolder.java │ │ │ │ ├── PtrUIHandlerHook.java │ │ │ │ ├── header │ │ │ │ ├── MaterialHeader.java │ │ │ │ ├── MaterialProgressDrawable.java │ │ │ │ ├── StoreHouseBarItem.java │ │ │ │ ├── StoreHouseHeader.java │ │ │ │ └── StoreHousePath.java │ │ │ │ ├── indicator │ │ │ │ ├── PtrIndicator.java │ │ │ │ └── PtrTensionIndicator.java │ │ │ │ └── util │ │ │ │ ├── PtrCLog.java │ │ │ │ └── PtrLocalDisplay.java │ │ └── res │ │ │ ├── drawable-xhdpi │ │ │ └── ptr_rotate_arrow.png │ │ │ ├── layout │ │ │ ├── cube_ptr_classic_default_header.xml │ │ │ └── cube_ptr_simple_loading.xml │ │ │ └── values │ │ │ ├── cube_ptr_attrs.xml │ │ │ ├── cube_ptr_string.xml │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── sgb │ │ └── library │ │ └── ExampleUnitTest.java └── settings.gradle ├── art └── screen.gif ├── index.android.js ├── package.json ├── src ├── Main.js └── component │ └── PtrComponent.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | module.system=haste 26 | 27 | experimental.strict_type_args=true 28 | 29 | munge_underscores=true 30 | 31 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 32 | 33 | suppress_type=$FlowIssue 34 | suppress_type=$FlowFixMe 35 | suppress_type=$FixMe 36 | 37 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-6]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 38 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-6]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 40 | 41 | unsafe.enable_getters_and_setters=true 42 | 43 | [version] 44 | ^0.36.0 45 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.xcuserstate 22 | project.xcworkspace 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | *.keystore 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 49 | 50 | fastlane/report.xml 51 | fastlane/Preview.html 52 | fastlane/screenshots 53 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RNAndroidPullToRefresh 2 | ![](https://github.com/panda912/RNAndroidPullToRefresh/blob/master/art/screen.gif) 3 | -------------------------------------------------------------------------------- /__tests__/index.android.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import Index from '../index.android.js'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .gradle 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /captures 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # To learn about Buck see [Docs](https://buckbuild.com/). 4 | # To run your application with Buck: 5 | # - install Buck 6 | # - `npm start` - to start the packager 7 | # - `cd android` 8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 10 | # - `buck install -r android/app` - compile, install and run application 11 | # 12 | 13 | lib_deps = [] 14 | for jarfile in glob(['libs/*.jar']): 15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) 16 | lib_deps.append(':' + name) 17 | prebuilt_jar( 18 | name = name, 19 | binary_jar = jarfile, 20 | ) 21 | 22 | for aarfile in glob(['libs/*.aar']): 23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) 24 | lib_deps.append(':' + name) 25 | android_prebuilt_aar( 26 | name = name, 27 | aar = aarfile, 28 | ) 29 | 30 | android_library( 31 | name = 'all-libs', 32 | exported_deps = lib_deps 33 | ) 34 | 35 | android_library( 36 | name = 'app-code', 37 | srcs = glob([ 38 | 'src/main/java/**/*.java', 39 | ]), 40 | deps = [ 41 | ':all-libs', 42 | ':build_config', 43 | ':res', 44 | ], 45 | ) 46 | 47 | android_build_config( 48 | name = 'build_config', 49 | package = 'com.rnandroidpulltorefresh', 50 | ) 51 | 52 | android_resource( 53 | name = 'res', 54 | res = 'src/main/res', 55 | package = 'com.rnandroidpulltorefresh', 56 | ) 57 | 58 | android_binary( 59 | name = 'app', 60 | package_type = 'debug', 61 | manifest = 'src/main/AndroidManifest.xml', 62 | keystore = '//android/keystores:debug', 63 | deps = [ 64 | ':app-code', 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // the root of your project, i.e. where "package.json" lives 37 | * root: "../../", 38 | * 39 | * // where to put the JS bundle asset in debug mode 40 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 41 | * 42 | * // where to put the JS bundle asset in release mode 43 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 44 | * 45 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 46 | * // require('./image.png')), in debug mode 47 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 48 | * 49 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 50 | * // require('./image.png')), in release mode 51 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 52 | * 53 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 54 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 55 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 56 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 57 | * // for example, you might want to remove it from here. 58 | * inputExcludes: ["android/**", "ios/**"], 59 | * 60 | * // override which node gets called and with what additional arguments 61 | * nodeExecutableAndArgs: ["node"] 62 | * 63 | * // supply additional arguments to the packager 64 | * extraPackagerArgs: [] 65 | * ] 66 | */ 67 | 68 | apply from: "../../node_modules/react-native/react.gradle" 69 | 70 | /** 71 | * Set this to true to create two separate APKs instead of one: 72 | * - An APK that only works on ARM devices 73 | * - An APK that only works on x86 devices 74 | * The advantage is the size of the APK is reduced by about 4MB. 75 | * Upload all the APKs to the Play Store and people will download 76 | * the correct one based on the CPU architecture of their device. 77 | */ 78 | def enableSeparateBuildPerCPUArchitecture = false 79 | 80 | /** 81 | * Run Proguard to shrink the Java bytecode in release builds. 82 | */ 83 | def enableProguardInReleaseBuilds = false 84 | 85 | android { 86 | compileSdkVersion 25 87 | buildToolsVersion "25.0.2" 88 | 89 | defaultConfig { 90 | applicationId "com.rnandroidpulltorefresh" 91 | minSdkVersion 16 92 | targetSdkVersion 25 93 | versionCode 1 94 | versionName "1.0" 95 | ndk { 96 | abiFilters "armeabi-v7a", "x86" 97 | } 98 | } 99 | splits { 100 | abi { 101 | reset() 102 | enable enableSeparateBuildPerCPUArchitecture 103 | universalApk false // If true, also generate a universal APK 104 | include "armeabi-v7a", "x86" 105 | } 106 | } 107 | buildTypes { 108 | release { 109 | minifyEnabled enableProguardInReleaseBuilds 110 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 111 | } 112 | } 113 | // applicationVariants are e.g. debug, release 114 | applicationVariants.all { variant -> 115 | variant.outputs.each { output -> 116 | // For each separate APK per architecture, set a unique version code as described here: 117 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 118 | def versionCodes = ["armeabi-v7a":1, "x86":2] 119 | def abi = output.getFilter(OutputFile.ABI) 120 | if (abi != null) { // null for the universal-debug, universal-release variants 121 | output.versionCodeOverride = 122 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 123 | } 124 | } 125 | } 126 | } 127 | 128 | dependencies { 129 | compile fileTree(dir: "libs", include: ["*.jar"]) 130 | compile project(path: ':library') 131 | compile "com.android.support:appcompat-v7:25.1.0" 132 | compile "com.facebook.react:react-native:+" // From node_modules 133 | 134 | } 135 | 136 | // Run this once to be able to run the application with BUCK 137 | // puts all compile dependencies into folder libs for BUCK to use 138 | task copyDownloadableDepsToLibs(type: Copy) { 139 | from configurations.compile 140 | into 'libs' 141 | } 142 | -------------------------------------------------------------------------------- /android/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 /usr/local/Cellar/android-sdk/24.3.3/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 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rnandroidpulltorefresh/AppReactPackage.java: -------------------------------------------------------------------------------- 1 | package com.rnandroidpulltorefresh; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | import com.rnandroidpulltorefresh.views.ptr.ReactPtrAndroidManager; 9 | 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class AppReactPackage implements ReactPackage { 15 | 16 | @Override 17 | public List createNativeModules(ReactApplicationContext reactContext) { 18 | return Collections.emptyList(); 19 | } 20 | 21 | @Override 22 | public List> createJSModules() { 23 | return Collections.emptyList(); 24 | } 25 | 26 | @Override 27 | public List createViewManagers(ReactApplicationContext reactContext) { 28 | return Arrays.asList( 29 | new ReactPtrAndroidManager() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rnandroidpulltorefresh/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.rnandroidpulltorefresh; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "RNAndroidPullToRefresh"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rnandroidpulltorefresh/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.rnandroidpulltorefresh; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.shell.MainReactPackage; 9 | import com.facebook.soloader.SoLoader; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 17 | @Override 18 | protected boolean getUseDeveloperSupport() { 19 | return BuildConfig.DEBUG; 20 | } 21 | 22 | @Override 23 | protected List getPackages() { 24 | return Arrays.asList( 25 | new MainReactPackage(), 26 | new AppReactPackage() 27 | ); 28 | } 29 | }; 30 | 31 | @Override 32 | public ReactNativeHost getReactNativeHost() { 33 | return mReactNativeHost; 34 | } 35 | 36 | @Override 37 | public void onCreate() { 38 | super.onCreate(); 39 | SoLoader.init(this, /* native exopackage */ false); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rnandroidpulltorefresh/views/ptr/PtrEvent.java: -------------------------------------------------------------------------------- 1 | package com.rnandroidpulltorefresh.views.ptr; 2 | 3 | import com.facebook.react.uimanager.events.Event; 4 | import com.facebook.react.uimanager.events.RCTEventEmitter; 5 | 6 | 7 | /** 8 | * Created by panda on 2016/12/9 上午10:12. 9 | */ 10 | public class PtrEvent extends Event { 11 | 12 | public PtrEvent(int viewTag) { 13 | super(viewTag); 14 | } 15 | 16 | @Override 17 | public String getEventName() { 18 | return "ptrRefresh"; 19 | } 20 | 21 | @Override 22 | public void dispatch(RCTEventEmitter rctEventEmitter) { 23 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), null); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rnandroidpulltorefresh/views/ptr/ReactPtrAndroidManager.java: -------------------------------------------------------------------------------- 1 | package com.rnandroidpulltorefresh.views.ptr; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.FrameLayout; 6 | 7 | import com.facebook.react.bridge.ReadableArray; 8 | import com.facebook.react.common.MapBuilder; 9 | import com.facebook.react.uimanager.ThemedReactContext; 10 | import com.facebook.react.uimanager.UIManagerModule; 11 | import com.facebook.react.uimanager.ViewGroupManager; 12 | import com.facebook.react.uimanager.annotations.ReactProp; 13 | import com.sgb.library.ptr.PtrClassicFrameLayout; 14 | import com.sgb.library.ptr.PtrDefaultHandler; 15 | import com.sgb.library.ptr.PtrFrameLayout; 16 | 17 | import java.util.Map; 18 | 19 | import javax.annotation.Nullable; 20 | 21 | 22 | /** 23 | * Created by panda on 2016/12/7 下午4:50. 24 | */ 25 | public class ReactPtrAndroidManager extends ViewGroupManager { 26 | 27 | private static final int REFRESH_COMPLETE = 0; 28 | private static final int AUTO_REFRESH = 1; 29 | 30 | @Override 31 | public String getName() { 32 | return "RCTPtrAndroid"; 33 | } 34 | 35 | @Override 36 | protected PtrClassicFrameLayout createViewInstance(final ThemedReactContext reactContext) { 37 | PtrClassicFrameLayout layout = new PtrClassicFrameLayout(reactContext); 38 | layout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 39 | return layout; 40 | } 41 | 42 | @ReactProp(name = "resistance", defaultFloat = 1.7f) 43 | public void setResistance(PtrClassicFrameLayout ptr, float resistance) { 44 | ptr.setResistance(resistance); 45 | } 46 | 47 | @ReactProp(name = "durationToCloseHeader", defaultInt = 200) 48 | public void setDurationToCloseHeader(PtrClassicFrameLayout ptr, int duration) { 49 | ptr.setDurationToCloseHeader(duration); 50 | } 51 | 52 | @ReactProp(name = "durationToClose", defaultInt = 300) 53 | public void setDurationToClose(PtrClassicFrameLayout ptr, int duration) { 54 | ptr.setDurationToClose(duration); 55 | } 56 | 57 | @ReactProp(name = "ratioOfHeaderHeightToRefresh", defaultFloat = 1.2f) 58 | public void setRatioOfHeaderHeightToRefresh(PtrClassicFrameLayout ptr, float ratio) { 59 | ptr.setRatioOfHeaderHeightToRefresh(ratio); 60 | } 61 | 62 | @ReactProp(name = "pullToRefresh", defaultBoolean = false) 63 | public void setPullToRefresh(PtrClassicFrameLayout ptr, boolean pullToRefresh) { 64 | ptr.setPullToRefresh(pullToRefresh); 65 | } 66 | 67 | @ReactProp(name = "keepHeaderWhenRefresh", defaultBoolean = false) 68 | public void setKeepHeaderWhenRefresh(PtrClassicFrameLayout ptr, boolean keep) { 69 | ptr.setKeepHeaderWhenRefresh(keep); 70 | } 71 | 72 | @ReactProp(name = "pinContent", defaultBoolean = false) 73 | public void setPinContent(PtrClassicFrameLayout ptr, boolean pinContent) { 74 | ptr.setPinContent(pinContent); 75 | } 76 | 77 | 78 | @Nullable 79 | @Override 80 | public Map getCommandsMap() { 81 | return MapBuilder.of("autoRefresh", AUTO_REFRESH, "refreshComplete", REFRESH_COMPLETE); 82 | } 83 | 84 | @Override 85 | public void receiveCommand(PtrClassicFrameLayout root, int commandId, @Nullable ReadableArray args) { 86 | switch (commandId) { 87 | case AUTO_REFRESH: 88 | root.autoRefresh(); 89 | break; 90 | case REFRESH_COMPLETE: 91 | root.refreshComplete(); 92 | break; 93 | default: 94 | break; 95 | } 96 | } 97 | 98 | @Override 99 | protected void addEventEmitters(final ThemedReactContext reactContext, final PtrClassicFrameLayout view) { 100 | view.setLastUpdateTimeRelateObject(this); 101 | view.setPtrHandler(new PtrDefaultHandler() { 102 | @Override 103 | public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) { 104 | return checkContentCanBePulledDown(frame, content, header); 105 | } 106 | 107 | @Override 108 | public void onRefreshBegin(PtrFrameLayout frame) { 109 | reactContext 110 | .getNativeModule(UIManagerModule.class) 111 | .getEventDispatcher() 112 | .dispatchEvent(new PtrEvent(view.getId())); 113 | } 114 | }); 115 | } 116 | 117 | @Nullable 118 | @Override 119 | public Map getExportedCustomDirectEventTypeConstants() { 120 | return MapBuilder.of( 121 | "ptrRefresh", MapBuilder.of("registrationName", "onPtrRefresh")); 122 | } 123 | 124 | @Override 125 | public void addView(PtrClassicFrameLayout parent, View child, int index) { 126 | super.addView(parent, child, 1); 127 | parent.finishInflateRN(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RNAndroidPullToRefresh 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/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.3' 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 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /android/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 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 27 | exclude group: 'com.android.support', module: 'support-annotations' 28 | }) 29 | compile 'com.android.support:appcompat-v7:25.1.0' 30 | testCompile 'junit:junit:4.12' 31 | } 32 | -------------------------------------------------------------------------------- /android/library/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/panda/Documents/adt-bundle-mac-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /android/library/src/androidTest/java/com/sgb/library/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library; 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("com.sgb.library.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrClassicDefaultHeader.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.res.TypedArray; 6 | import android.text.TextUtils; 7 | import android.util.AttributeSet; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.animation.LinearInterpolator; 11 | import android.view.animation.RotateAnimation; 12 | import android.widget.FrameLayout; 13 | import android.widget.TextView; 14 | 15 | import com.sgb.library.R; 16 | import com.sgb.library.ptr.indicator.PtrIndicator; 17 | 18 | import java.text.SimpleDateFormat; 19 | import java.util.Date; 20 | 21 | 22 | public class PtrClassicDefaultHeader extends FrameLayout implements PtrUIHandler { 23 | 24 | private final static String KEY_SharedPreferences = "cube_ptr_classic_last_update"; 25 | private static SimpleDateFormat sDataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 26 | private int mRotateAniTime = 150; 27 | private RotateAnimation mFlipAnimation; 28 | private RotateAnimation mReverseFlipAnimation; 29 | private TextView mTitleTextView; 30 | private View mRotateView; 31 | private View mProgressBar; 32 | private long mLastUpdateTime = -1; 33 | private TextView mLastUpdateTextView; 34 | private String mLastUpdateTimeKey; 35 | private boolean mShouldShowLastUpdate; 36 | 37 | private LastUpdateTimeUpdater mLastUpdateTimeUpdater = new LastUpdateTimeUpdater(); 38 | 39 | public PtrClassicDefaultHeader(Context context) { 40 | super(context); 41 | initViews(null); 42 | } 43 | 44 | public PtrClassicDefaultHeader(Context context, AttributeSet attrs) { 45 | super(context, attrs); 46 | initViews(attrs); 47 | } 48 | 49 | public PtrClassicDefaultHeader(Context context, AttributeSet attrs, int defStyle) { 50 | super(context, attrs, defStyle); 51 | initViews(attrs); 52 | } 53 | 54 | protected void initViews(AttributeSet attrs) { 55 | TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.PtrClassicHeader, 0, 0); 56 | if (arr != null) { 57 | mRotateAniTime = arr.getInt(R.styleable.PtrClassicHeader_ptr_rotate_ani_time, mRotateAniTime); 58 | } 59 | buildAnimation(); 60 | View header = LayoutInflater.from(getContext()).inflate(R.layout.cube_ptr_classic_default_header, this); 61 | 62 | mRotateView = header.findViewById(R.id.ptr_classic_header_rotate_view); 63 | 64 | mTitleTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_title); 65 | mLastUpdateTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_last_update); 66 | mProgressBar = header.findViewById(R.id.ptr_classic_header_rotate_view_progressbar); 67 | 68 | resetView(); 69 | } 70 | 71 | @Override 72 | protected void onDetachedFromWindow() { 73 | super.onDetachedFromWindow(); 74 | if (mLastUpdateTimeUpdater != null) { 75 | mLastUpdateTimeUpdater.stop(); 76 | } 77 | } 78 | 79 | public void setRotateAniTime(int time) { 80 | if (time == mRotateAniTime || time == 0) { 81 | return; 82 | } 83 | mRotateAniTime = time; 84 | buildAnimation(); 85 | } 86 | 87 | /** 88 | * Specify the last update time by this key string 89 | * 90 | * @param key 91 | */ 92 | public void setLastUpdateTimeKey(String key) { 93 | if (TextUtils.isEmpty(key)) { 94 | return; 95 | } 96 | mLastUpdateTimeKey = key; 97 | } 98 | 99 | /** 100 | * Using an object to specify the last update time. 101 | * 102 | * @param object 103 | */ 104 | public void setLastUpdateTimeRelateObject(Object object) { 105 | setLastUpdateTimeKey(object.getClass().getName()); 106 | } 107 | 108 | private void buildAnimation() { 109 | mFlipAnimation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); 110 | mFlipAnimation.setInterpolator(new LinearInterpolator()); 111 | mFlipAnimation.setDuration(mRotateAniTime); 112 | mFlipAnimation.setFillAfter(true); 113 | 114 | mReverseFlipAnimation = new RotateAnimation(-180, 0, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); 115 | mReverseFlipAnimation.setInterpolator(new LinearInterpolator()); 116 | mReverseFlipAnimation.setDuration(mRotateAniTime); 117 | mReverseFlipAnimation.setFillAfter(true); 118 | } 119 | 120 | private void resetView() { 121 | hideRotateView(); 122 | mProgressBar.setVisibility(INVISIBLE); 123 | } 124 | 125 | private void hideRotateView() { 126 | mRotateView.clearAnimation(); 127 | mRotateView.setVisibility(INVISIBLE); 128 | } 129 | 130 | @Override 131 | public void onUIReset(PtrFrameLayout frame) { 132 | resetView(); 133 | mShouldShowLastUpdate = true; 134 | tryUpdateLastUpdateTime(); 135 | } 136 | 137 | @Override 138 | public void onUIRefreshPrepare(PtrFrameLayout frame) { 139 | 140 | mShouldShowLastUpdate = true; 141 | tryUpdateLastUpdateTime(); 142 | mLastUpdateTimeUpdater.start(); 143 | 144 | mProgressBar.setVisibility(INVISIBLE); 145 | 146 | mRotateView.setVisibility(VISIBLE); 147 | mTitleTextView.setVisibility(VISIBLE); 148 | if (frame.isPullToRefresh()) { 149 | mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down_to_refresh)); 150 | } else { 151 | mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down)); 152 | } 153 | } 154 | 155 | @Override 156 | public void onUIRefreshBegin(PtrFrameLayout frame) { 157 | mShouldShowLastUpdate = false; 158 | hideRotateView(); 159 | mProgressBar.setVisibility(VISIBLE); 160 | mTitleTextView.setVisibility(VISIBLE); 161 | mTitleTextView.setText(R.string.cube_ptr_refreshing); 162 | 163 | tryUpdateLastUpdateTime(); 164 | mLastUpdateTimeUpdater.stop(); 165 | } 166 | 167 | @Override 168 | public void onUIRefreshComplete(PtrFrameLayout frame) { 169 | 170 | hideRotateView(); 171 | mProgressBar.setVisibility(INVISIBLE); 172 | 173 | mTitleTextView.setVisibility(VISIBLE); 174 | mTitleTextView.setText(getResources().getString(R.string.cube_ptr_refresh_complete)); 175 | 176 | // update last update time 177 | SharedPreferences sharedPreferences = getContext().getSharedPreferences(KEY_SharedPreferences, 0); 178 | if (!TextUtils.isEmpty(mLastUpdateTimeKey)) { 179 | mLastUpdateTime = new Date().getTime(); 180 | sharedPreferences.edit().putLong(mLastUpdateTimeKey, mLastUpdateTime).commit(); 181 | } 182 | } 183 | 184 | private void tryUpdateLastUpdateTime() { 185 | if (TextUtils.isEmpty(mLastUpdateTimeKey) || !mShouldShowLastUpdate) { 186 | mLastUpdateTextView.setVisibility(GONE); 187 | } else { 188 | String time = getLastUpdateTime(); 189 | if (TextUtils.isEmpty(time)) { 190 | mLastUpdateTextView.setVisibility(GONE); 191 | } else { 192 | mLastUpdateTextView.setVisibility(VISIBLE); 193 | mLastUpdateTextView.setText(time); 194 | } 195 | } 196 | } 197 | 198 | private String getLastUpdateTime() { 199 | 200 | if (mLastUpdateTime == -1 && !TextUtils.isEmpty(mLastUpdateTimeKey)) { 201 | mLastUpdateTime = getContext().getSharedPreferences(KEY_SharedPreferences, 0).getLong(mLastUpdateTimeKey, -1); 202 | } 203 | if (mLastUpdateTime == -1) { 204 | return null; 205 | } 206 | long diffTime = new Date().getTime() - mLastUpdateTime; 207 | int seconds = (int) (diffTime / 1000); 208 | if (diffTime < 0) { 209 | return null; 210 | } 211 | if (seconds <= 0) { 212 | return null; 213 | } 214 | StringBuilder sb = new StringBuilder(); 215 | sb.append(getContext().getString(R.string.cube_ptr_last_update)); 216 | 217 | if (seconds < 60) { 218 | sb.append(seconds + getContext().getString(R.string.cube_ptr_seconds_ago)); 219 | } else { 220 | int minutes = (seconds / 60); 221 | if (minutes > 60) { 222 | int hours = minutes / 60; 223 | if (hours > 24) { 224 | Date date = new Date(mLastUpdateTime); 225 | sb.append(sDataFormat.format(date)); 226 | } else { 227 | sb.append(hours + getContext().getString(R.string.cube_ptr_hours_ago)); 228 | } 229 | 230 | } else { 231 | sb.append(minutes + getContext().getString(R.string.cube_ptr_minutes_ago)); 232 | } 233 | } 234 | return sb.toString(); 235 | } 236 | 237 | @Override 238 | public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) { 239 | 240 | final int mOffsetToRefresh = frame.getOffsetToRefresh(); 241 | final int currentPos = ptrIndicator.getCurrentPosY(); 242 | final int lastPos = ptrIndicator.getLastPosY(); 243 | 244 | if (currentPos < mOffsetToRefresh && lastPos >= mOffsetToRefresh) { 245 | if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) { 246 | crossRotateLineFromBottomUnderTouch(frame); 247 | if (mRotateView != null) { 248 | mRotateView.clearAnimation(); 249 | mRotateView.startAnimation(mReverseFlipAnimation); 250 | } 251 | } 252 | } else if (currentPos > mOffsetToRefresh && lastPos <= mOffsetToRefresh) { 253 | if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) { 254 | crossRotateLineFromTopUnderTouch(frame); 255 | if (mRotateView != null) { 256 | mRotateView.clearAnimation(); 257 | mRotateView.startAnimation(mFlipAnimation); 258 | } 259 | } 260 | } 261 | } 262 | 263 | private void crossRotateLineFromTopUnderTouch(PtrFrameLayout frame) { 264 | if (!frame.isPullToRefresh()) { 265 | mTitleTextView.setVisibility(VISIBLE); 266 | mTitleTextView.setText(R.string.cube_ptr_release_to_refresh); 267 | } 268 | } 269 | 270 | private void crossRotateLineFromBottomUnderTouch(PtrFrameLayout frame) { 271 | mTitleTextView.setVisibility(VISIBLE); 272 | if (frame.isPullToRefresh()) { 273 | mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down_to_refresh)); 274 | } else { 275 | mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down)); 276 | } 277 | } 278 | 279 | private class LastUpdateTimeUpdater implements Runnable { 280 | 281 | private boolean mRunning = false; 282 | 283 | private void start() { 284 | if (TextUtils.isEmpty(mLastUpdateTimeKey)) { 285 | return; 286 | } 287 | mRunning = true; 288 | run(); 289 | } 290 | 291 | private void stop() { 292 | mRunning = false; 293 | removeCallbacks(this); 294 | } 295 | 296 | @Override 297 | public void run() { 298 | tryUpdateLastUpdateTime(); 299 | if (mRunning) { 300 | postDelayed(this, 1000); 301 | } 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrClassicFrameLayout.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | public class PtrClassicFrameLayout extends PtrFrameLayout { 7 | 8 | private PtrClassicDefaultHeader mPtrClassicHeader; 9 | 10 | public PtrClassicFrameLayout(Context context) { 11 | super(context); 12 | initViews(); 13 | } 14 | 15 | public PtrClassicFrameLayout(Context context, AttributeSet attrs) { 16 | super(context, attrs); 17 | initViews(); 18 | } 19 | 20 | public PtrClassicFrameLayout(Context context, AttributeSet attrs, int defStyle) { 21 | super(context, attrs, defStyle); 22 | initViews(); 23 | } 24 | 25 | private void initViews() { 26 | mPtrClassicHeader = new PtrClassicDefaultHeader(getContext()); 27 | setHeaderView(mPtrClassicHeader); 28 | addPtrUIHandler(mPtrClassicHeader); 29 | } 30 | 31 | public PtrClassicDefaultHeader getHeader() { 32 | return mPtrClassicHeader; 33 | } 34 | 35 | /** 36 | * Specify the last update time by this key string 37 | * 38 | * @param key 39 | */ 40 | public void setLastUpdateTimeKey(String key) { 41 | if (mPtrClassicHeader != null) { 42 | mPtrClassicHeader.setLastUpdateTimeKey(key); 43 | } 44 | } 45 | 46 | /** 47 | * Using an object to specify the last update time. 48 | * 49 | * @param object 50 | */ 51 | public void setLastUpdateTimeRelateObject(Object object) { 52 | if (mPtrClassicHeader != null) { 53 | mPtrClassicHeader.setLastUpdateTimeRelateObject(object); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrDefaultHandler.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | import android.view.View; 4 | import android.widget.AbsListView; 5 | 6 | public abstract class PtrDefaultHandler implements PtrHandler { 7 | 8 | public static boolean canChildScrollUp(View view) { 9 | if (android.os.Build.VERSION.SDK_INT < 14) { 10 | if (view instanceof AbsListView) { 11 | final AbsListView absListView = (AbsListView) view; 12 | return absListView.getChildCount() > 0 13 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 14 | .getTop() < absListView.getPaddingTop()); 15 | } else { 16 | return view.getScrollY() > 0; 17 | } 18 | } else { 19 | return view.canScrollVertically(-1); 20 | } 21 | } 22 | 23 | /** 24 | * Default implement for check can perform pull to refresh 25 | * 26 | * @param frame 27 | * @param content 28 | * @param header 29 | * @return 30 | */ 31 | public static boolean checkContentCanBePulledDown(PtrFrameLayout frame, View content, View header) { 32 | return !canChildScrollUp(content); 33 | } 34 | 35 | @Override 36 | public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) { 37 | return checkContentCanBePulledDown(frame, content, header); 38 | } 39 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrFrameLayout.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.util.Log; 7 | import android.view.Gravity; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.view.ViewConfiguration; 11 | import android.view.ViewGroup; 12 | import android.widget.Scroller; 13 | import android.widget.TextView; 14 | 15 | import com.sgb.library.R; 16 | import com.sgb.library.ptr.indicator.PtrIndicator; 17 | import com.sgb.library.ptr.util.PtrCLog; 18 | 19 | 20 | /** 21 | * This layout view for "Pull to Refresh(Ptr)" support all of the view, you can contain everything you want. 22 | * support: pull to refresh / release to refresh / auto refresh / keep header view while refreshing / hide header view while refreshing 23 | * It defines {@link com.sgb.library.ptr.PtrUIHandler}, which allows you customize the UI easily. 24 | */ 25 | public class PtrFrameLayout extends ViewGroup { 26 | 27 | // status enum 28 | public final static byte PTR_STATUS_INIT = 1; 29 | private byte mStatus = PTR_STATUS_INIT; 30 | public final static byte PTR_STATUS_PREPARE = 2; 31 | public final static byte PTR_STATUS_LOADING = 3; 32 | public final static byte PTR_STATUS_COMPLETE = 4; 33 | private static final boolean DEBUG_LAYOUT = true; 34 | public static boolean DEBUG = true; 35 | private static int ID = 1; 36 | protected final String LOG_TAG = "ptr-frame-" + ++ID; 37 | // auto refresh status 38 | private final static byte FLAG_AUTO_REFRESH_AT_ONCE = 0x01; 39 | private final static byte FLAG_AUTO_REFRESH_BUT_LATER = 0x01 << 1; 40 | private final static byte FLAG_ENABLE_NEXT_PTR_AT_ONCE = 0x01 << 2; 41 | private final static byte FLAG_PIN_CONTENT = 0x01 << 3; 42 | private final static byte MASK_AUTO_REFRESH = 0x03; 43 | protected View mContent; 44 | // optional config for define header and content in xml file 45 | private int mHeaderId = 0; 46 | private int mContainerId = 0; 47 | // config 48 | private int mDurationToClose = 200; 49 | private int mDurationToCloseHeader = 1000; 50 | private boolean mKeepHeaderWhenRefresh = true; 51 | private boolean mPullToRefresh = false; 52 | private View mHeaderView; 53 | private PtrUIHandlerHolder mPtrUIHandlerHolder = PtrUIHandlerHolder.create(); 54 | private PtrHandler mPtrHandler; 55 | // working parameters 56 | private ScrollChecker mScrollChecker; 57 | private int mPagingTouchSlop; 58 | private int mHeaderHeight; 59 | private boolean mDisableWhenHorizontalMove = false; 60 | private int mFlag = 0x00; 61 | 62 | // disable when detect moving horizontally 63 | private boolean mPreventForHorizontal = false; 64 | 65 | private MotionEvent mLastMoveEvent; 66 | 67 | private PtrUIHandlerHook mRefreshCompleteHook; 68 | 69 | private int mLoadingMinTime = 500; 70 | private long mLoadingStartTime = 0; 71 | private PtrIndicator mPtrIndicator; 72 | private boolean mHasSendCancelEvent = false; 73 | private Runnable mPerformRefreshCompleteDelay = new Runnable() { 74 | @Override 75 | public void run() { 76 | performRefreshComplete(); 77 | } 78 | }; 79 | 80 | public PtrFrameLayout(Context context) { 81 | this(context, null); 82 | } 83 | 84 | public PtrFrameLayout(Context context, AttributeSet attrs) { 85 | this(context, attrs, 0); 86 | } 87 | 88 | public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) { 89 | super(context, attrs, defStyle); 90 | 91 | mPtrIndicator = new PtrIndicator(); 92 | 93 | TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout, 0, 0); 94 | if (arr != null) { 95 | 96 | mHeaderId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_header, mHeaderId); 97 | mContainerId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_content, mContainerId); 98 | 99 | mPtrIndicator.setResistance(arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance())); 100 | 101 | mDurationToClose = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close, mDurationToClose); 102 | mDurationToCloseHeader = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close_header, mDurationToCloseHeader); 103 | 104 | float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh(); 105 | ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio); 106 | mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio); 107 | 108 | mKeepHeaderWhenRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_keep_header_when_refresh, mKeepHeaderWhenRefresh); 109 | 110 | mPullToRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_pull_to_fresh, mPullToRefresh); 111 | arr.recycle(); 112 | } 113 | 114 | mScrollChecker = new ScrollChecker(); 115 | 116 | final ViewConfiguration conf = ViewConfiguration.get(getContext()); 117 | mPagingTouchSlop = conf.getScaledTouchSlop() * 2; 118 | } 119 | 120 | @Override 121 | protected void onFinishInflate() { 122 | Log.e("onFinishInflate", "onFinishInflate"); 123 | final int childCount = getChildCount(); 124 | if (childCount > 2) { 125 | throw new IllegalStateException("PtrFrameLayout can only contains 2 children"); 126 | } else if (childCount == 2) { 127 | if (mHeaderId != 0 && mHeaderView == null) { 128 | mHeaderView = findViewById(mHeaderId); 129 | } 130 | if (mContainerId != 0 && mContent == null) { 131 | mContent = findViewById(mContainerId); 132 | } 133 | 134 | // not specify header or content 135 | if (mContent == null || mHeaderView == null) { 136 | 137 | View child1 = getChildAt(0); 138 | View child2 = getChildAt(1); 139 | if (child1 instanceof PtrUIHandler) { 140 | mHeaderView = child1; 141 | mContent = child2; 142 | } else if (child2 instanceof PtrUIHandler) { 143 | mHeaderView = child2; 144 | mContent = child1; 145 | } else { 146 | // both are not specified 147 | if (mContent == null && mHeaderView == null) { 148 | mHeaderView = child1; 149 | mContent = child2; 150 | } 151 | // only one is specified 152 | else { 153 | if (mHeaderView == null) { 154 | mHeaderView = mContent == child1 ? child2 : child1; 155 | } else { 156 | mContent = mHeaderView == child1 ? child2 : child1; 157 | } 158 | } 159 | } 160 | } 161 | } else if (childCount == 1) { 162 | mContent = getChildAt(0); 163 | } else { 164 | TextView errorView = new TextView(getContext()); 165 | errorView.setClickable(true); 166 | errorView.setTextColor(0xffff6600); 167 | errorView.setGravity(Gravity.CENTER); 168 | errorView.setTextSize(20); 169 | errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?"); 170 | mContent = errorView; 171 | addView(mContent); 172 | } 173 | if (mHeaderView != null) { 174 | mHeaderView.bringToFront(); 175 | } 176 | 177 | super.onFinishInflate(); 178 | } 179 | 180 | /** 181 | * apply to RN 182 | */ 183 | public void finishInflateRN() { 184 | final int childCount = getChildCount(); 185 | if (childCount > 2) { 186 | throw new IllegalStateException("PtrFrameLayout can only contains 2 children"); 187 | } else if (childCount == 2) { 188 | if (mHeaderId != 0 && mHeaderView == null) { 189 | mHeaderView = findViewById(mHeaderId); 190 | } 191 | if (mContainerId != 0 && mContent == null) { 192 | mContent = findViewById(mContainerId); 193 | } 194 | 195 | // not specify header or content 196 | if (mContent == null || mHeaderView == null) { 197 | 198 | View child1 = getChildAt(0); 199 | View child2 = getChildAt(1); 200 | if (child1 instanceof PtrUIHandler) { 201 | mHeaderView = child1; 202 | mContent = child2; 203 | } else if (child2 instanceof PtrUIHandler) { 204 | mHeaderView = child2; 205 | mContent = child1; 206 | } else { 207 | // both are not specified 208 | if (mContent == null && mHeaderView == null) { 209 | mHeaderView = child1; 210 | mContent = child2; 211 | } 212 | // only one is specified 213 | else { 214 | if (mHeaderView == null) { 215 | mHeaderView = mContent == child1 ? child2 : child1; 216 | } else { 217 | mContent = mHeaderView == child1 ? child2 : child1; 218 | } 219 | } 220 | } 221 | } else { 222 | mContent = getChildAt(1); 223 | } 224 | } else if (childCount == 1) { 225 | mHeaderView = getChildAt(0); 226 | } else { 227 | TextView errorView = new TextView(getContext()); 228 | errorView.setClickable(true); 229 | errorView.setTextColor(0xffff6600); 230 | errorView.setGravity(Gravity.CENTER); 231 | errorView.setTextSize(20); 232 | errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?"); 233 | mContent = errorView; 234 | addView(mContent); 235 | } 236 | if (mHeaderView != null) { 237 | mHeaderView.bringToFront(); 238 | } 239 | } 240 | 241 | @Override 242 | protected void onDetachedFromWindow() { 243 | super.onDetachedFromWindow(); 244 | if (mScrollChecker != null) { 245 | mScrollChecker.destroy(); 246 | } 247 | 248 | if (mPerformRefreshCompleteDelay != null) { 249 | removeCallbacks(mPerformRefreshCompleteDelay); 250 | } 251 | } 252 | 253 | @Override 254 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 255 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 256 | 257 | if (isDebug()) { 258 | PtrCLog.d(LOG_TAG, "onMeasure frame: width: %s, height: %s, padding: %s %s %s %s", 259 | getMeasuredHeight(), getMeasuredWidth(), 260 | getPaddingLeft(), getPaddingRight(), getPaddingTop(), getPaddingBottom()); 261 | 262 | } 263 | 264 | if (mHeaderView != null) { 265 | measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0); 266 | MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); 267 | mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 268 | mPtrIndicator.setHeaderHeight(mHeaderHeight); 269 | } 270 | 271 | if (mContent != null) { 272 | measureContentView(mContent, widthMeasureSpec, heightMeasureSpec); 273 | if (isDebug()) { 274 | MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); 275 | PtrCLog.d(LOG_TAG, "onMeasure content, width: %s, height: %s, margin: %s %s %s %s", 276 | getMeasuredWidth(), getMeasuredHeight(), 277 | lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); 278 | PtrCLog.d(LOG_TAG, "onMeasure, currentPos: %s, lastPos: %s, top: %s", 279 | mPtrIndicator.getCurrentPosY(), mPtrIndicator.getLastPosY(), mContent.getTop()); 280 | } 281 | } 282 | } 283 | 284 | private void measureContentView(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { 285 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 286 | 287 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); 288 | final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin, lp.height); 289 | 290 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 291 | } 292 | 293 | @Override 294 | protected void onLayout(boolean flag, int i, int j, int k, int l) { 295 | layoutChildren(); 296 | } 297 | 298 | private void layoutChildren() { 299 | int offset = mPtrIndicator.getCurrentPosY(); 300 | int paddingLeft = getPaddingLeft(); 301 | int paddingTop = getPaddingTop(); 302 | 303 | if (mHeaderView != null) { 304 | MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); 305 | final int left = paddingLeft + lp.leftMargin; 306 | // enhance readability(header is layout above screen when first init) 307 | final int top = -(mHeaderHeight - paddingTop - lp.topMargin - offset); 308 | final int right = left + mHeaderView.getMeasuredWidth(); 309 | final int bottom = top + mHeaderView.getMeasuredHeight(); 310 | mHeaderView.layout(left, top, right, bottom); 311 | if (isDebug()) { 312 | PtrCLog.d(LOG_TAG, "onLayout header: %s %s %s %s", left, top, right, bottom); 313 | } 314 | } 315 | if (mContent != null) { 316 | if (isPinContent()) { 317 | offset = 0; 318 | } 319 | MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); 320 | final int left = paddingLeft + lp.leftMargin; 321 | final int top = paddingTop + lp.topMargin + offset; 322 | final int right = left + mContent.getMeasuredWidth(); 323 | final int bottom = top + mContent.getMeasuredHeight(); 324 | if (isDebug()) { 325 | PtrCLog.d(LOG_TAG, "onLayout content: %s %s %s %s", left, top, right, bottom); 326 | } 327 | mContent.layout(left, top, right, bottom); 328 | } 329 | } 330 | 331 | @SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"}) 332 | private boolean isDebug() { 333 | return DEBUG && DEBUG_LAYOUT; 334 | } 335 | 336 | public boolean dispatchTouchEventSupper(MotionEvent e) { 337 | return super.dispatchTouchEvent(e); 338 | } 339 | 340 | @Override 341 | public boolean dispatchTouchEvent(MotionEvent e) { 342 | if (!isEnabled() || mContent == null || mHeaderView == null) { 343 | return dispatchTouchEventSupper(e); 344 | } 345 | int action = e.getAction(); 346 | switch (action) { 347 | case MotionEvent.ACTION_UP: 348 | case MotionEvent.ACTION_CANCEL: 349 | mPtrIndicator.onRelease(); 350 | if (mPtrIndicator.hasLeftStartPosition()) { 351 | if (DEBUG) { 352 | PtrCLog.d(LOG_TAG, "call onRelease when user release"); 353 | } 354 | onRelease(false); 355 | if (mPtrIndicator.hasMovedAfterPressedDown()) { 356 | sendCancelEvent(); 357 | return true; 358 | } 359 | return dispatchTouchEventSupper(e); 360 | } else { 361 | return dispatchTouchEventSupper(e); 362 | } 363 | 364 | case MotionEvent.ACTION_DOWN: 365 | mHasSendCancelEvent = false; 366 | mPtrIndicator.onPressDown(e.getX(), e.getY()); 367 | 368 | mScrollChecker.abortIfWorking(); 369 | 370 | mPreventForHorizontal = false; 371 | // The cancel event will be sent once the position is moved. 372 | // So let the event pass to children. 373 | // fix #93, #102 374 | dispatchTouchEventSupper(e); 375 | return true; 376 | 377 | case MotionEvent.ACTION_MOVE: 378 | mLastMoveEvent = e; 379 | mPtrIndicator.onMove(e.getX(), e.getY()); 380 | float offsetX = mPtrIndicator.getOffsetX(); 381 | float offsetY = mPtrIndicator.getOffsetY(); 382 | 383 | if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) { 384 | if (mPtrIndicator.isInStartPosition()) { 385 | mPreventForHorizontal = true; 386 | } 387 | } 388 | if (mPreventForHorizontal) { 389 | return dispatchTouchEventSupper(e); 390 | } 391 | 392 | boolean moveDown = offsetY > 0; 393 | boolean moveUp = !moveDown; 394 | boolean canMoveUp = mPtrIndicator.hasLeftStartPosition(); 395 | 396 | if (DEBUG) { 397 | boolean canMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView); 398 | PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown); 399 | } 400 | 401 | // disable move when header not reach top 402 | if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) { 403 | return dispatchTouchEventSupper(e); 404 | } 405 | 406 | if ((moveUp && canMoveUp) || moveDown) { 407 | movePos(offsetY); 408 | return true; 409 | } 410 | } 411 | return dispatchTouchEventSupper(e); 412 | } 413 | 414 | /** 415 | * if deltaY > 0, move the content down 416 | * 417 | * @param deltaY 418 | */ 419 | private void movePos(float deltaY) { 420 | // has reached the top 421 | if ((deltaY < 0 && mPtrIndicator.isInStartPosition())) { 422 | if (DEBUG) { 423 | PtrCLog.e(LOG_TAG, String.format("has reached the top")); 424 | } 425 | return; 426 | } 427 | 428 | int to = mPtrIndicator.getCurrentPosY() + (int) deltaY; 429 | 430 | // over top 431 | if (mPtrIndicator.willOverTop(to)) { 432 | if (DEBUG) { 433 | PtrCLog.e(LOG_TAG, String.format("over top")); 434 | } 435 | to = PtrIndicator.POS_START; 436 | } 437 | 438 | mPtrIndicator.setCurrentPos(to); 439 | int change = to - mPtrIndicator.getLastPosY(); 440 | updatePos(change); 441 | } 442 | 443 | private void updatePos(int change) { 444 | if (change == 0) { 445 | return; 446 | } 447 | 448 | boolean isUnderTouch = mPtrIndicator.isUnderTouch(); 449 | 450 | // once moved, cancel event will be sent to child 451 | if (isUnderTouch && !mHasSendCancelEvent && mPtrIndicator.hasMovedAfterPressedDown()) { 452 | mHasSendCancelEvent = true; 453 | sendCancelEvent(); 454 | } 455 | 456 | // leave initiated position or just refresh complete 457 | if ((mPtrIndicator.hasJustLeftStartPosition() && mStatus == PTR_STATUS_INIT) || 458 | (mPtrIndicator.goDownCrossFinishPosition() && mStatus == PTR_STATUS_COMPLETE && isEnabledNextPtrAtOnce())) { 459 | 460 | mStatus = PTR_STATUS_PREPARE; 461 | mPtrUIHandlerHolder.onUIRefreshPrepare(this); 462 | if (DEBUG) { 463 | PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshPrepare, mFlag %s", mFlag); 464 | } 465 | } 466 | 467 | // back to initiated position 468 | if (mPtrIndicator.hasJustBackToStartPosition()) { 469 | tryToNotifyReset(); 470 | 471 | // recover event to children 472 | if (isUnderTouch) { 473 | sendDownEvent(); 474 | } 475 | } 476 | 477 | // Pull to Refresh 478 | if (mStatus == PTR_STATUS_PREPARE) { 479 | // reach fresh height while moving from top to bottom 480 | if (isUnderTouch && !isAutoRefresh() && mPullToRefresh 481 | && mPtrIndicator.crossRefreshLineFromTopToBottom()) { 482 | tryToPerformRefresh(); 483 | } 484 | // reach header height while auto refresh 485 | if (performAutoRefreshButLater() && mPtrIndicator.hasJustReachedHeaderHeightFromTopToBottom()) { 486 | tryToPerformRefresh(); 487 | } 488 | } 489 | 490 | if (DEBUG) { 491 | PtrCLog.v(LOG_TAG, "updatePos: change: %s, current: %s last: %s, top: %s, headerHeight: %s", 492 | change, mPtrIndicator.getCurrentPosY(), mPtrIndicator.getLastPosY(), mContent.getTop(), mHeaderHeight); 493 | } 494 | 495 | mHeaderView.offsetTopAndBottom(change); 496 | if (!isPinContent()) { 497 | mContent.offsetTopAndBottom(change); 498 | } 499 | invalidate(); 500 | 501 | if (mPtrUIHandlerHolder.hasHandler()) { 502 | mPtrUIHandlerHolder.onUIPositionChange(this, isUnderTouch, mStatus, mPtrIndicator); 503 | } 504 | onPositionChange(isUnderTouch, mStatus, mPtrIndicator); 505 | } 506 | 507 | protected void onPositionChange(boolean isInTouching, byte status, PtrIndicator mPtrIndicator) { 508 | } 509 | 510 | @SuppressWarnings("unused") 511 | public int getHeaderHeight() { 512 | return mHeaderHeight; 513 | } 514 | 515 | private void onRelease(boolean stayForLoading) { 516 | 517 | tryToPerformRefresh(); 518 | 519 | if (mStatus == PTR_STATUS_LOADING) { 520 | // keep header for fresh 521 | if (mKeepHeaderWhenRefresh) { 522 | // scroll header back 523 | if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) { 524 | mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose); 525 | } else { 526 | // do nothing 527 | } 528 | } else { 529 | tryScrollBackToTopWhileLoading(); 530 | } 531 | } else { 532 | if (mStatus == PTR_STATUS_COMPLETE) { 533 | notifyUIRefreshComplete(false); 534 | } else { 535 | tryScrollBackToTopAbortRefresh(); 536 | } 537 | } 538 | } 539 | 540 | /** 541 | * please DO REMEMBER resume the hook 542 | * 543 | * @param hook 544 | */ 545 | 546 | public void setRefreshCompleteHook(PtrUIHandlerHook hook) { 547 | mRefreshCompleteHook = hook; 548 | hook.setResumeAction(new Runnable() { 549 | @Override 550 | public void run() { 551 | if (DEBUG) { 552 | PtrCLog.d(LOG_TAG, "mRefreshCompleteHook resume."); 553 | } 554 | notifyUIRefreshComplete(true); 555 | } 556 | }); 557 | } 558 | 559 | /** 560 | * Scroll back to to if is not under touch 561 | */ 562 | private void tryScrollBackToTop() { 563 | if (!mPtrIndicator.isUnderTouch()) { 564 | mScrollChecker.tryToScrollTo(PtrIndicator.POS_START, mDurationToCloseHeader); 565 | } 566 | } 567 | 568 | /** 569 | * just make easier to understand 570 | */ 571 | private void tryScrollBackToTopWhileLoading() { 572 | tryScrollBackToTop(); 573 | } 574 | 575 | /** 576 | * just make easier to understand 577 | */ 578 | private void tryScrollBackToTopAfterComplete() { 579 | tryScrollBackToTop(); 580 | } 581 | 582 | /** 583 | * just make easier to understand 584 | */ 585 | private void tryScrollBackToTopAbortRefresh() { 586 | tryScrollBackToTop(); 587 | } 588 | 589 | private boolean tryToPerformRefresh() { 590 | if (mStatus != PTR_STATUS_PREPARE) { 591 | return false; 592 | } 593 | 594 | // 595 | if ((mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && isAutoRefresh()) || mPtrIndicator.isOverOffsetToRefresh()) { 596 | mStatus = PTR_STATUS_LOADING; 597 | performRefresh(); 598 | } 599 | return false; 600 | } 601 | 602 | private void performRefresh() { 603 | mLoadingStartTime = System.currentTimeMillis(); 604 | if (mPtrUIHandlerHolder.hasHandler()) { 605 | mPtrUIHandlerHolder.onUIRefreshBegin(this); 606 | if (DEBUG) { 607 | PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshBegin"); 608 | } 609 | } 610 | if (mPtrHandler != null) { 611 | mPtrHandler.onRefreshBegin(this); 612 | } 613 | } 614 | 615 | /** 616 | * If at the top and not in loading, reset 617 | */ 618 | private boolean tryToNotifyReset() { 619 | if ((mStatus == PTR_STATUS_COMPLETE || mStatus == PTR_STATUS_PREPARE) && mPtrIndicator.isInStartPosition()) { 620 | if (mPtrUIHandlerHolder.hasHandler()) { 621 | mPtrUIHandlerHolder.onUIReset(this); 622 | if (DEBUG) { 623 | PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIReset"); 624 | } 625 | } 626 | mStatus = PTR_STATUS_INIT; 627 | clearFlag(); 628 | return true; 629 | } 630 | return false; 631 | } 632 | 633 | protected void onPtrScrollAbort() { 634 | if (mPtrIndicator.hasLeftStartPosition() && isAutoRefresh()) { 635 | if (DEBUG) { 636 | PtrCLog.d(LOG_TAG, "call onRelease after scroll abort"); 637 | } 638 | onRelease(true); 639 | } 640 | } 641 | 642 | protected void onPtrScrollFinish() { 643 | if (mPtrIndicator.hasLeftStartPosition() && isAutoRefresh()) { 644 | if (DEBUG) { 645 | PtrCLog.d(LOG_TAG, "call onRelease after scroll finish"); 646 | } 647 | onRelease(true); 648 | } 649 | } 650 | 651 | /** 652 | * Detect whether is refreshing. 653 | * 654 | * @return 655 | */ 656 | public boolean isRefreshing() { 657 | return mStatus == PTR_STATUS_LOADING; 658 | } 659 | 660 | /** 661 | * Call this when data is loaded. 662 | * The UI will perform complete at once or after a delay, depends on the time elapsed is greater then {@link #mLoadingMinTime} or not. 663 | */ 664 | final public void refreshComplete() { 665 | if (DEBUG) { 666 | PtrCLog.i(LOG_TAG, "refreshComplete"); 667 | } 668 | 669 | if (mRefreshCompleteHook != null) { 670 | mRefreshCompleteHook.reset(); 671 | } 672 | 673 | int delay = (int) (mLoadingMinTime - (System.currentTimeMillis() - mLoadingStartTime)); 674 | if (delay <= 0) { 675 | if (DEBUG) { 676 | PtrCLog.d(LOG_TAG, "performRefreshComplete at once"); 677 | } 678 | performRefreshComplete(); 679 | } else { 680 | postDelayed(mPerformRefreshCompleteDelay, delay); 681 | if (DEBUG) { 682 | PtrCLog.d(LOG_TAG, "performRefreshComplete after delay: %s", delay); 683 | } 684 | } 685 | } 686 | 687 | /** 688 | * Do refresh complete work when time elapsed is greater than {@link #mLoadingMinTime} 689 | */ 690 | private void performRefreshComplete() { 691 | mStatus = PTR_STATUS_COMPLETE; 692 | 693 | // if is auto refresh do nothing, wait scroller stop 694 | if (mScrollChecker.mIsRunning && isAutoRefresh()) { 695 | // do nothing 696 | if (DEBUG) { 697 | PtrCLog.d(LOG_TAG, "performRefreshComplete do nothing, scrolling: %s, auto refresh: %s", 698 | mScrollChecker.mIsRunning, mFlag); 699 | } 700 | return; 701 | } 702 | 703 | notifyUIRefreshComplete(false); 704 | } 705 | 706 | /** 707 | * Do real refresh work. If there is a hook, execute the hook first. 708 | * 709 | * @param ignoreHook 710 | */ 711 | private void notifyUIRefreshComplete(boolean ignoreHook) { 712 | /** 713 | * After hook operation is done, {@link #notifyUIRefreshComplete} will be call in resume action to ignore hook. 714 | */ 715 | if (mPtrIndicator.hasLeftStartPosition() && !ignoreHook && mRefreshCompleteHook != null) { 716 | if (DEBUG) { 717 | PtrCLog.d(LOG_TAG, "notifyUIRefreshComplete mRefreshCompleteHook run."); 718 | } 719 | 720 | mRefreshCompleteHook.takeOver(); 721 | return; 722 | } 723 | if (mPtrUIHandlerHolder.hasHandler()) { 724 | if (DEBUG) { 725 | PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshComplete"); 726 | } 727 | mPtrUIHandlerHolder.onUIRefreshComplete(this); 728 | } 729 | mPtrIndicator.onUIRefreshComplete(); 730 | tryScrollBackToTopAfterComplete(); 731 | tryToNotifyReset(); 732 | } 733 | 734 | public void autoRefresh() { 735 | autoRefresh(true, mDurationToCloseHeader); 736 | } 737 | 738 | public void autoRefresh(boolean atOnce) { 739 | autoRefresh(atOnce, mDurationToCloseHeader); 740 | } 741 | 742 | private void clearFlag() { 743 | // remove auto fresh flag 744 | mFlag = mFlag & ~MASK_AUTO_REFRESH; 745 | } 746 | 747 | public void autoRefresh(boolean atOnce, int duration) { 748 | 749 | if (mStatus != PTR_STATUS_INIT) { 750 | return; 751 | } 752 | 753 | mFlag |= atOnce ? FLAG_AUTO_REFRESH_AT_ONCE : FLAG_AUTO_REFRESH_BUT_LATER; 754 | 755 | mStatus = PTR_STATUS_PREPARE; 756 | if (mPtrUIHandlerHolder.hasHandler()) { 757 | mPtrUIHandlerHolder.onUIRefreshPrepare(this); 758 | if (DEBUG) { 759 | PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshPrepare, mFlag %s", mFlag); 760 | } 761 | } 762 | mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToRefresh(), duration); 763 | if (atOnce) { 764 | mStatus = PTR_STATUS_LOADING; 765 | performRefresh(); 766 | } 767 | } 768 | 769 | public boolean isAutoRefresh() { 770 | return (mFlag & MASK_AUTO_REFRESH) > 0; 771 | } 772 | 773 | private boolean performAutoRefreshButLater() { 774 | return (mFlag & MASK_AUTO_REFRESH) == FLAG_AUTO_REFRESH_BUT_LATER; 775 | } 776 | 777 | public boolean isEnabledNextPtrAtOnce() { 778 | return (mFlag & FLAG_ENABLE_NEXT_PTR_AT_ONCE) > 0; 779 | } 780 | 781 | /** 782 | * If @param enable has been set to true. The user can perform next PTR at once. 783 | * 784 | * @param enable 785 | */ 786 | public void setEnabledNextPtrAtOnce(boolean enable) { 787 | if (enable) { 788 | mFlag = mFlag | FLAG_ENABLE_NEXT_PTR_AT_ONCE; 789 | } else { 790 | mFlag = mFlag & ~FLAG_ENABLE_NEXT_PTR_AT_ONCE; 791 | } 792 | } 793 | 794 | public boolean isPinContent() { 795 | return (mFlag & FLAG_PIN_CONTENT) > 0; 796 | } 797 | 798 | /** 799 | * The content view will now move when {@param pinContent} set to true. 800 | * 801 | * @param pinContent 802 | */ 803 | public void setPinContent(boolean pinContent) { 804 | if (pinContent) { 805 | mFlag = mFlag | FLAG_PIN_CONTENT; 806 | } else { 807 | mFlag = mFlag & ~FLAG_PIN_CONTENT; 808 | } 809 | } 810 | 811 | /** 812 | * It's useful when working with viewpager. 813 | * 814 | * @param disable 815 | */ 816 | public void disableWhenHorizontalMove(boolean disable) { 817 | mDisableWhenHorizontalMove = disable; 818 | } 819 | 820 | /** 821 | * loading will last at least for so long 822 | * 823 | * @param time 824 | */ 825 | public void setLoadingMinTime(int time) { 826 | mLoadingMinTime = time; 827 | } 828 | 829 | /** 830 | * Not necessary any longer. Once moved, cancel event will be sent to child. 831 | * 832 | * @param yes 833 | */ 834 | @Deprecated 835 | public void setInterceptEventWhileWorking(boolean yes) { 836 | } 837 | 838 | @SuppressWarnings({"unused"}) 839 | public View getContentView() { 840 | return mContent; 841 | } 842 | 843 | public void setPtrHandler(PtrHandler ptrHandler) { 844 | mPtrHandler = ptrHandler; 845 | } 846 | 847 | public void addPtrUIHandler(PtrUIHandler ptrUIHandler) { 848 | PtrUIHandlerHolder.addHandler(mPtrUIHandlerHolder, ptrUIHandler); 849 | } 850 | 851 | @SuppressWarnings({"unused"}) 852 | public void removePtrUIHandler(PtrUIHandler ptrUIHandler) { 853 | mPtrUIHandlerHolder = PtrUIHandlerHolder.removeHandler(mPtrUIHandlerHolder, ptrUIHandler); 854 | } 855 | 856 | public void setPtrIndicator(PtrIndicator slider) { 857 | if (mPtrIndicator != null && mPtrIndicator != slider) { 858 | slider.convertFrom(mPtrIndicator); 859 | } 860 | mPtrIndicator = slider; 861 | } 862 | 863 | @SuppressWarnings({"unused"}) 864 | public float getResistance() { 865 | return mPtrIndicator.getResistance(); 866 | } 867 | 868 | public void setResistance(float resistance) { 869 | mPtrIndicator.setResistance(resistance); 870 | } 871 | 872 | @SuppressWarnings({"unused"}) 873 | public float getDurationToClose() { 874 | return mDurationToClose; 875 | } 876 | 877 | /** 878 | * The duration to return back to the refresh position 879 | * 880 | * @param duration 881 | */ 882 | public void setDurationToClose(int duration) { 883 | mDurationToClose = duration; 884 | } 885 | 886 | @SuppressWarnings({"unused"}) 887 | public long getDurationToCloseHeader() { 888 | return mDurationToCloseHeader; 889 | } 890 | 891 | /** 892 | * The duration to close time 893 | * 894 | * @param duration 895 | */ 896 | public void setDurationToCloseHeader(int duration) { 897 | mDurationToCloseHeader = duration; 898 | } 899 | 900 | public void setRatioOfHeaderHeightToRefresh(float ratio) { 901 | mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio); 902 | } 903 | 904 | public int getOffsetToRefresh() { 905 | return mPtrIndicator.getOffsetToRefresh(); 906 | } 907 | 908 | @SuppressWarnings({"unused"}) 909 | public void setOffsetToRefresh(int offset) { 910 | mPtrIndicator.setOffsetToRefresh(offset); 911 | } 912 | 913 | @SuppressWarnings({"unused"}) 914 | public float getRatioOfHeaderToHeightRefresh() { 915 | return mPtrIndicator.getRatioOfHeaderToHeightRefresh(); 916 | } 917 | 918 | @SuppressWarnings({"unused"}) 919 | public int getOffsetToKeepHeaderWhileLoading() { 920 | return mPtrIndicator.getOffsetToKeepHeaderWhileLoading(); 921 | } 922 | 923 | @SuppressWarnings({"unused"}) 924 | public void setOffsetToKeepHeaderWhileLoading(int offset) { 925 | mPtrIndicator.setOffsetToKeepHeaderWhileLoading(offset); 926 | } 927 | 928 | @SuppressWarnings({"unused"}) 929 | public boolean isKeepHeaderWhenRefresh() { 930 | return mKeepHeaderWhenRefresh; 931 | } 932 | 933 | public void setKeepHeaderWhenRefresh(boolean keepOrNot) { 934 | mKeepHeaderWhenRefresh = keepOrNot; 935 | } 936 | 937 | public boolean isPullToRefresh() { 938 | return mPullToRefresh; 939 | } 940 | 941 | public void setPullToRefresh(boolean pullToRefresh) { 942 | mPullToRefresh = pullToRefresh; 943 | } 944 | 945 | @SuppressWarnings({"unused"}) 946 | public View getHeaderView() { 947 | return mHeaderView; 948 | } 949 | 950 | public void setHeaderView(View header) { 951 | if (mHeaderView != null && header != null && mHeaderView != header) { 952 | removeView(mHeaderView); 953 | } 954 | ViewGroup.LayoutParams lp = header.getLayoutParams(); 955 | if (lp == null) { 956 | lp = new LayoutParams(-1, -2); 957 | header.setLayoutParams(lp); 958 | } 959 | mHeaderView = header; 960 | addView(header); 961 | } 962 | 963 | @Override 964 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 965 | return p != null && p instanceof LayoutParams; 966 | } 967 | 968 | @Override 969 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 970 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 971 | } 972 | 973 | @Override 974 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 975 | return new LayoutParams(p); 976 | } 977 | 978 | @Override 979 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 980 | return new LayoutParams(getContext(), attrs); 981 | } 982 | 983 | private void sendCancelEvent() { 984 | if (DEBUG) { 985 | PtrCLog.d(LOG_TAG, "send cancel event"); 986 | } 987 | // The ScrollChecker will update position and lead to send cancel event when mLastMoveEvent is null. 988 | // fix #104, #80, #92 989 | if (mLastMoveEvent == null) { 990 | return; 991 | } 992 | MotionEvent last = mLastMoveEvent; 993 | MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, last.getX(), last.getY(), last.getMetaState()); 994 | dispatchTouchEventSupper(e); 995 | } 996 | 997 | private void sendDownEvent() { 998 | if (DEBUG) { 999 | PtrCLog.d(LOG_TAG, "send down event"); 1000 | } 1001 | final MotionEvent last = mLastMoveEvent; 1002 | MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime(), MotionEvent.ACTION_DOWN, last.getX(), last.getY(), last.getMetaState()); 1003 | dispatchTouchEventSupper(e); 1004 | } 1005 | 1006 | public static class LayoutParams extends MarginLayoutParams { 1007 | 1008 | public LayoutParams(Context c, AttributeSet attrs) { 1009 | super(c, attrs); 1010 | } 1011 | 1012 | public LayoutParams(int width, int height) { 1013 | super(width, height); 1014 | } 1015 | 1016 | @SuppressWarnings({"unused"}) 1017 | public LayoutParams(MarginLayoutParams source) { 1018 | super(source); 1019 | } 1020 | 1021 | public LayoutParams(ViewGroup.LayoutParams source) { 1022 | super(source); 1023 | } 1024 | } 1025 | 1026 | class ScrollChecker implements Runnable { 1027 | 1028 | private int mLastFlingY; 1029 | private Scroller mScroller; 1030 | private boolean mIsRunning = false; 1031 | private int mStart; 1032 | private int mTo; 1033 | 1034 | public ScrollChecker() { 1035 | mScroller = new Scroller(getContext()); 1036 | } 1037 | 1038 | public void run() { 1039 | boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished(); 1040 | int curY = mScroller.getCurrY(); 1041 | int deltaY = curY - mLastFlingY; 1042 | if (DEBUG) { 1043 | if (deltaY != 0) { 1044 | PtrCLog.v(LOG_TAG, 1045 | "scroll: %s, start: %s, to: %s, currentPos: %s, current :%s, last: %s, delta: %s", 1046 | finish, mStart, mTo, mPtrIndicator.getCurrentPosY(), curY, mLastFlingY, deltaY); 1047 | } 1048 | } 1049 | if (!finish) { 1050 | mLastFlingY = curY; 1051 | movePos(deltaY); 1052 | post(this); 1053 | } else { 1054 | finish(); 1055 | } 1056 | } 1057 | 1058 | private void finish() { 1059 | if (DEBUG) { 1060 | PtrCLog.v(LOG_TAG, "finish, currentPos:%s", mPtrIndicator.getCurrentPosY()); 1061 | } 1062 | reset(); 1063 | onPtrScrollFinish(); 1064 | } 1065 | 1066 | private void reset() { 1067 | mIsRunning = false; 1068 | mLastFlingY = 0; 1069 | removeCallbacks(this); 1070 | } 1071 | 1072 | private void destroy() { 1073 | reset(); 1074 | if (!mScroller.isFinished()) { 1075 | mScroller.forceFinished(true); 1076 | } 1077 | } 1078 | 1079 | public void abortIfWorking() { 1080 | if (mIsRunning) { 1081 | if (!mScroller.isFinished()) { 1082 | mScroller.forceFinished(true); 1083 | } 1084 | onPtrScrollAbort(); 1085 | reset(); 1086 | } 1087 | } 1088 | 1089 | public void tryToScrollTo(int to, int duration) { 1090 | if (mPtrIndicator.isAlreadyHere(to)) { 1091 | return; 1092 | } 1093 | mStart = mPtrIndicator.getCurrentPosY(); 1094 | mTo = to; 1095 | int distance = to - mStart; 1096 | if (DEBUG) { 1097 | PtrCLog.d(LOG_TAG, "tryToScrollTo: start: %s, distance:%s, to:%s", mStart, distance, to); 1098 | } 1099 | removeCallbacks(this); 1100 | 1101 | mLastFlingY = 0; 1102 | 1103 | // fix #47: Scroller should be reused, https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/issues/47 1104 | if (!mScroller.isFinished()) { 1105 | mScroller.forceFinished(true); 1106 | } 1107 | mScroller.startScroll(0, 0, 0, distance, duration); 1108 | post(this); 1109 | mIsRunning = true; 1110 | } 1111 | } 1112 | } 1113 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrHandler.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | import android.view.View; 4 | 5 | public interface PtrHandler { 6 | 7 | /** 8 | * Check can do refresh or not. For example the content is empty or the first child is in view. 9 | *

10 | * {@link com.sgb.library.ptr.PtrDefaultHandler#checkContentCanBePulledDown} 11 | */ 12 | public boolean checkCanDoRefresh(final PtrFrameLayout frame, final View content, final View header); 13 | 14 | /** 15 | * When refresh begin 16 | * 17 | * @param frame 18 | */ 19 | public void onRefreshBegin(final PtrFrameLayout frame); 20 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrUIHandler.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | 4 | import com.sgb.library.ptr.indicator.PtrIndicator; 5 | 6 | /** 7 | * 8 | */ 9 | public interface PtrUIHandler { 10 | 11 | /** 12 | * When the content view has reached top and refresh has been completed, view will be reset. 13 | * 14 | * @param frame 15 | */ 16 | public void onUIReset(PtrFrameLayout frame); 17 | 18 | /** 19 | * prepare for loading 20 | * 21 | * @param frame 22 | */ 23 | public void onUIRefreshPrepare(PtrFrameLayout frame); 24 | 25 | /** 26 | * perform refreshing UI 27 | */ 28 | public void onUIRefreshBegin(PtrFrameLayout frame); 29 | 30 | /** 31 | * perform UI after refresh 32 | */ 33 | public void onUIRefreshComplete(PtrFrameLayout frame); 34 | 35 | public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator); 36 | } 37 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrUIHandlerHolder.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | 4 | import com.sgb.library.ptr.indicator.PtrIndicator; 5 | 6 | /** 7 | * A single linked list to wrap PtrUIHandler 8 | */ 9 | class PtrUIHandlerHolder implements PtrUIHandler { 10 | 11 | private PtrUIHandler mHandler; 12 | private PtrUIHandlerHolder mNext; 13 | 14 | private boolean contains(PtrUIHandler handler) { 15 | return mHandler != null && mHandler == handler; 16 | } 17 | 18 | private PtrUIHandlerHolder() { 19 | 20 | } 21 | 22 | public boolean hasHandler() { 23 | return mHandler != null; 24 | } 25 | 26 | private PtrUIHandler getHandler() { 27 | return mHandler; 28 | } 29 | 30 | public static void addHandler(PtrUIHandlerHolder head, PtrUIHandler handler) { 31 | 32 | if (null == handler) { 33 | return; 34 | } 35 | if (head == null) { 36 | return; 37 | } 38 | if (null == head.mHandler) { 39 | head.mHandler = handler; 40 | return; 41 | } 42 | 43 | PtrUIHandlerHolder current = head; 44 | for (; ; current = current.mNext) { 45 | 46 | // duplicated 47 | if (current.contains(handler)) { 48 | return; 49 | } 50 | if (current.mNext == null) { 51 | break; 52 | } 53 | } 54 | 55 | PtrUIHandlerHolder newHolder = new PtrUIHandlerHolder(); 56 | newHolder.mHandler = handler; 57 | current.mNext = newHolder; 58 | } 59 | 60 | public static PtrUIHandlerHolder create() { 61 | return new PtrUIHandlerHolder(); 62 | } 63 | 64 | public static PtrUIHandlerHolder removeHandler(PtrUIHandlerHolder head, PtrUIHandler handler) { 65 | if (head == null || handler == null || null == head.mHandler) { 66 | return head; 67 | } 68 | 69 | PtrUIHandlerHolder current = head; 70 | PtrUIHandlerHolder pre = null; 71 | do { 72 | 73 | // delete current: link pre to next, unlink next from current; 74 | // pre will no change, current move to next element; 75 | if (current.contains(handler)) { 76 | 77 | // current is head 78 | if (pre == null) { 79 | 80 | head = current.mNext; 81 | current.mNext = null; 82 | 83 | current = head; 84 | } else { 85 | 86 | pre.mNext = current.mNext; 87 | current.mNext = null; 88 | current = pre.mNext; 89 | } 90 | } else { 91 | pre = current; 92 | current = current.mNext; 93 | } 94 | 95 | } while (current != null); 96 | 97 | if (head == null) { 98 | head = new PtrUIHandlerHolder(); 99 | } 100 | return head; 101 | } 102 | 103 | @Override 104 | public void onUIReset(PtrFrameLayout frame) { 105 | PtrUIHandlerHolder current = this; 106 | do { 107 | final PtrUIHandler handler = current.getHandler(); 108 | if (null != handler) { 109 | handler.onUIReset(frame); 110 | } 111 | } while ((current = current.mNext) != null); 112 | } 113 | 114 | @Override 115 | public void onUIRefreshPrepare(PtrFrameLayout frame) { 116 | if (!hasHandler()) { 117 | return; 118 | } 119 | PtrUIHandlerHolder current = this; 120 | do { 121 | final PtrUIHandler handler = current.getHandler(); 122 | if (null != handler) { 123 | handler.onUIRefreshPrepare(frame); 124 | } 125 | } while ((current = current.mNext) != null); 126 | } 127 | 128 | @Override 129 | public void onUIRefreshBegin(PtrFrameLayout frame) { 130 | PtrUIHandlerHolder current = this; 131 | do { 132 | final PtrUIHandler handler = current.getHandler(); 133 | if (null != handler) { 134 | handler.onUIRefreshBegin(frame); 135 | } 136 | } while ((current = current.mNext) != null); 137 | } 138 | 139 | @Override 140 | public void onUIRefreshComplete(PtrFrameLayout frame) { 141 | PtrUIHandlerHolder current = this; 142 | do { 143 | final PtrUIHandler handler = current.getHandler(); 144 | if (null != handler) { 145 | handler.onUIRefreshComplete(frame); 146 | } 147 | } while ((current = current.mNext) != null); 148 | } 149 | 150 | @Override 151 | public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) { 152 | PtrUIHandlerHolder current = this; 153 | do { 154 | final PtrUIHandler handler = current.getHandler(); 155 | if (null != handler) { 156 | handler.onUIPositionChange(frame, isUnderTouch, status, ptrIndicator); 157 | } 158 | } while ((current = current.mNext) != null); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/PtrUIHandlerHook.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr; 2 | 3 | /** 4 | * Run a hook runnable, the runnable will run only once. 5 | * After the runnable is done, call resume to resume. 6 | * Once run, call takeover will directory call the resume action 7 | */ 8 | public abstract class PtrUIHandlerHook implements Runnable { 9 | 10 | private Runnable mResumeAction; 11 | private static final byte STATUS_PREPARE = 0; 12 | private static final byte STATUS_IN_HOOK = 1; 13 | private static final byte STATUS_RESUMED = 2; 14 | private byte mStatus = STATUS_PREPARE; 15 | 16 | public void takeOver() { 17 | takeOver(null); 18 | } 19 | 20 | public void takeOver(Runnable resumeAction) { 21 | if (resumeAction != null) { 22 | mResumeAction = resumeAction; 23 | } 24 | switch (mStatus) { 25 | case STATUS_PREPARE: 26 | mStatus = STATUS_IN_HOOK; 27 | run(); 28 | break; 29 | case STATUS_IN_HOOK: 30 | break; 31 | case STATUS_RESUMED: 32 | resume(); 33 | break; 34 | } 35 | } 36 | 37 | public void reset() { 38 | mStatus = STATUS_PREPARE; 39 | } 40 | 41 | public void resume() { 42 | if (mResumeAction != null) { 43 | mResumeAction.run(); 44 | } 45 | mStatus = STATUS_RESUMED; 46 | } 47 | 48 | /** 49 | * Hook should always have a resume action, which is hooked by this hook. 50 | * 51 | * @param runnable 52 | */ 53 | public void setResumeAction(Runnable runnable) { 54 | mResumeAction = runnable; 55 | } 56 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/header/MaterialHeader.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.header; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Drawable; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | import android.view.animation.Animation; 11 | import android.view.animation.Transformation; 12 | 13 | import com.sgb.library.ptr.PtrFrameLayout; 14 | import com.sgb.library.ptr.PtrUIHandler; 15 | import com.sgb.library.ptr.PtrUIHandlerHook; 16 | import com.sgb.library.ptr.indicator.PtrIndicator; 17 | 18 | 19 | public class MaterialHeader extends View implements PtrUIHandler { 20 | 21 | private MaterialProgressDrawable mDrawable; 22 | private float mScale = 1f; 23 | private PtrFrameLayout mPtrFrameLayout; 24 | 25 | private Animation mScaleAnimation = new Animation() { 26 | @Override 27 | public void applyTransformation(float interpolatedTime, Transformation t) { 28 | mScale = 1f - interpolatedTime; 29 | mDrawable.setAlpha((int) (255 * mScale)); 30 | invalidate(); 31 | } 32 | }; 33 | 34 | public MaterialHeader(Context context) { 35 | super(context); 36 | initView(); 37 | } 38 | 39 | public MaterialHeader(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | initView(); 42 | } 43 | 44 | public MaterialHeader(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | initView(); 47 | } 48 | 49 | public void setPtrFrameLayout(PtrFrameLayout layout) { 50 | 51 | final PtrUIHandlerHook mPtrUIHandlerHook = new PtrUIHandlerHook() { 52 | @Override 53 | public void run() { 54 | startAnimation(mScaleAnimation); 55 | } 56 | }; 57 | 58 | mScaleAnimation.setDuration(200); 59 | mScaleAnimation.setAnimationListener(new Animation.AnimationListener() { 60 | @Override 61 | public void onAnimationStart(Animation animation) { 62 | 63 | } 64 | 65 | @Override 66 | public void onAnimationEnd(Animation animation) { 67 | mPtrUIHandlerHook.resume(); 68 | } 69 | 70 | @Override 71 | public void onAnimationRepeat(Animation animation) { 72 | 73 | } 74 | }); 75 | 76 | mPtrFrameLayout = layout; 77 | mPtrFrameLayout.setRefreshCompleteHook(mPtrUIHandlerHook); 78 | } 79 | 80 | private void initView() { 81 | mDrawable = new MaterialProgressDrawable(getContext(), this); 82 | mDrawable.setBackgroundColor(Color.WHITE); 83 | mDrawable.setCallback(this); 84 | } 85 | 86 | @Override 87 | public void invalidateDrawable(Drawable dr) { 88 | if (dr == mDrawable) { 89 | invalidate(); 90 | } else { 91 | super.invalidateDrawable(dr); 92 | } 93 | } 94 | 95 | public void setColorSchemeColors(int[] colors) { 96 | mDrawable.setColorSchemeColors(colors); 97 | invalidate(); 98 | } 99 | 100 | @Override 101 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 102 | int height = mDrawable.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom(); 103 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 104 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 105 | } 106 | 107 | @Override 108 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 109 | final int size = mDrawable.getIntrinsicHeight(); 110 | mDrawable.setBounds(0, 0, size, size); 111 | } 112 | 113 | @Override 114 | protected void onDraw(Canvas canvas) { 115 | final int saveCount = canvas.save(); 116 | Rect rect = mDrawable.getBounds(); 117 | int l = getPaddingLeft() + (getMeasuredWidth() - mDrawable.getIntrinsicWidth()) / 2; 118 | canvas.translate(l, getPaddingTop()); 119 | canvas.scale(mScale, mScale, rect.exactCenterX(), rect.exactCenterY()); 120 | mDrawable.draw(canvas); 121 | canvas.restoreToCount(saveCount); 122 | } 123 | 124 | /** 125 | * When the content view has reached top and refresh has been completed, view will be reset. 126 | * 127 | * @param frame 128 | */ 129 | @Override 130 | public void onUIReset(PtrFrameLayout frame) { 131 | mScale = 1f; 132 | mDrawable.stop(); 133 | } 134 | 135 | /** 136 | * prepare for loading 137 | * 138 | * @param frame 139 | */ 140 | @Override 141 | public void onUIRefreshPrepare(PtrFrameLayout frame) { 142 | } 143 | 144 | /** 145 | * perform refreshing UI 146 | * 147 | * @param frame 148 | */ 149 | @Override 150 | public void onUIRefreshBegin(PtrFrameLayout frame) { 151 | mDrawable.setAlpha(255); 152 | mDrawable.start(); 153 | } 154 | 155 | /** 156 | * perform UI after refresh 157 | * 158 | * @param frame 159 | */ 160 | @Override 161 | public void onUIRefreshComplete(PtrFrameLayout frame) { 162 | mDrawable.stop(); 163 | } 164 | 165 | @Override 166 | public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) { 167 | 168 | float percent = Math.min(1f, ptrIndicator.getCurrentPercent()); 169 | 170 | if (status == PtrFrameLayout.PTR_STATUS_PREPARE) { 171 | mDrawable.setAlpha((int) (255 * percent)); 172 | mDrawable.showArrow(true); 173 | 174 | float strokeStart = ((percent) * .8f); 175 | mDrawable.setStartEndTrim(0f, Math.min(0.8f, strokeStart)); 176 | mDrawable.setArrowScale(Math.min(1f, percent)); 177 | 178 | // magic 179 | float rotation = (-0.25f + .4f * percent + percent * 2) * .5f; 180 | mDrawable.setProgressRotation(rotation); 181 | invalidate(); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/header/MaterialProgressDrawable.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 | package com.sgb.library.ptr.header; 17 | 18 | import android.content.Context; 19 | import android.content.res.Resources; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.ColorFilter; 23 | import android.graphics.Paint; 24 | import android.graphics.Paint.Style; 25 | import android.graphics.Path; 26 | import android.graphics.PixelFormat; 27 | import android.graphics.RadialGradient; 28 | import android.graphics.Rect; 29 | import android.graphics.RectF; 30 | import android.graphics.Shader; 31 | import android.graphics.drawable.Animatable; 32 | import android.graphics.drawable.Drawable; 33 | import android.graphics.drawable.ShapeDrawable; 34 | import android.graphics.drawable.shapes.OvalShape; 35 | import android.os.Build; 36 | import android.util.DisplayMetrics; 37 | import android.view.View; 38 | import android.view.animation.AccelerateDecelerateInterpolator; 39 | import android.view.animation.Animation; 40 | import android.view.animation.Interpolator; 41 | import android.view.animation.LinearInterpolator; 42 | import android.view.animation.Transformation; 43 | 44 | import com.sgb.library.ptr.util.PtrLocalDisplay; 45 | 46 | import java.util.ArrayList; 47 | 48 | 49 | /** 50 | * Fancy progress indicator for Material theme. 51 | * It's taken from {@link android.support.v4.widget}. 52 | * I've done some slight changes. 53 | * 54 | * @hide 55 | */ 56 | public class MaterialProgressDrawable extends Drawable implements Animatable { 57 | 58 | // Maps to ProgressBar.Large style 59 | public static final int LARGE = 0; 60 | // Maps to ProgressBar default style 61 | public static final int DEFAULT = 1; 62 | private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 63 | private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); 64 | private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); 65 | private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); 66 | // Maps to ProgressBar default style 67 | private static final int CIRCLE_DIAMETER = 40; 68 | private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 69 | private static final float STROKE_WIDTH = 2.5f; 70 | // Maps to ProgressBar.Large style 71 | private static final int CIRCLE_DIAMETER_LARGE = 56; 72 | private static final float CENTER_RADIUS_LARGE = 12.5f; 73 | private static final float STROKE_WIDTH_LARGE = 3f; 74 | /** 75 | * The duration of a single progress spin in milliseconds. 76 | */ 77 | private static final int ANIMATION_DURATION = 1000 * 80 / 60; 78 | /** 79 | * The number of points in the progress "star". 80 | */ 81 | private static final float NUM_POINTS = 5f; 82 | /** 83 | * Layout info for the arrowhead in dp 84 | */ 85 | private static final int ARROW_WIDTH = 10; 86 | private static final int ARROW_HEIGHT = 5; 87 | private static final float ARROW_OFFSET_ANGLE = 5; 88 | /** 89 | * Layout info for the arrowhead for the large spinner in dp 90 | */ 91 | private static final int ARROW_WIDTH_LARGE = 12; 92 | private static final int ARROW_HEIGHT_LARGE = 6; 93 | private static final float MAX_PROGRESS_ARC = .8f; 94 | private static final int KEY_SHADOW_COLOR = 0x1E000000; 95 | private static final int FILL_SHADOW_COLOR = 0x3D000000; 96 | private static final float SHADOW_RADIUS = 3.5f; 97 | private static final float X_OFFSET = 0f; 98 | private static final float Y_OFFSET = 1.75f; 99 | private final int[] COLORS = new int[]{ 100 | 0xFFC93437, 101 | 0xFF375BF1, 102 | 0xFFF7D23E, 103 | 0xFF34A350 104 | }; 105 | /** 106 | * The list of animators operating on this drawable. 107 | */ 108 | private final ArrayList mAnimators = new ArrayList(); 109 | /** 110 | * The indicator ring, used to manage animation state. 111 | */ 112 | private final Ring mRing; 113 | private final Callback mCallback = new Callback() { 114 | @Override 115 | public void invalidateDrawable(Drawable d) { 116 | invalidateSelf(); 117 | } 118 | 119 | @Override 120 | public void scheduleDrawable(Drawable d, Runnable what, long when) { 121 | scheduleSelf(what, when); 122 | } 123 | 124 | @Override 125 | public void unscheduleDrawable(Drawable d, Runnable what) { 126 | unscheduleSelf(what); 127 | } 128 | }; 129 | /** 130 | * Canvas rotation in degrees. 131 | */ 132 | private float mRotation; 133 | private Resources mResources; 134 | private View mParent; 135 | private Animation mAnimation; 136 | private float mRotationCount; 137 | private double mWidth; 138 | private double mHeight; 139 | private Animation mFinishAnimation; 140 | private int mBackgroundColor; 141 | private ShapeDrawable mShadow; 142 | 143 | public MaterialProgressDrawable(Context context, View parent) { 144 | mParent = parent; 145 | mResources = context.getResources(); 146 | mRing = new Ring(mCallback); 147 | mRing.setColors(COLORS); 148 | updateSizes(DEFAULT); 149 | setupAnimators(); 150 | } 151 | 152 | private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 153 | double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 154 | final Ring ring = mRing; 155 | final DisplayMetrics metrics = mResources.getDisplayMetrics(); 156 | final float screenDensity = metrics.density; 157 | mWidth = progressCircleWidth * screenDensity; 158 | mHeight = progressCircleHeight * screenDensity; 159 | ring.setStrokeWidth((float) strokeWidth * screenDensity); 160 | ring.setCenterRadius(centerRadius * screenDensity); 161 | ring.setColorIndex(0); 162 | ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 163 | ring.setInsets((int) mWidth, (int) mHeight); 164 | setUp(mWidth); 165 | } 166 | 167 | private void setUp(final double diameter) { 168 | PtrLocalDisplay.init(mParent.getContext()); 169 | final int shadowYOffset = PtrLocalDisplay.dp2px(Y_OFFSET); 170 | final int shadowXOffset = PtrLocalDisplay.dp2px(X_OFFSET); 171 | int mShadowRadius = PtrLocalDisplay.dp2px(SHADOW_RADIUS); 172 | OvalShape oval = new OvalShadow(mShadowRadius, (int) diameter); 173 | mShadow = new ShapeDrawable(oval); 174 | if (Build.VERSION.SDK_INT >= 11) { 175 | mParent.setLayerType(View.LAYER_TYPE_SOFTWARE, mShadow.getPaint()); 176 | } 177 | mShadow.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, KEY_SHADOW_COLOR); 178 | } 179 | 180 | /** 181 | * Set the overall size for the progress spinner. This updates the radius 182 | * and stroke width of the ring. 183 | * 184 | * @param size One of {@link MaterialProgressDrawable#LARGE} or 185 | * {@link MaterialProgressDrawable#DEFAULT} 186 | */ 187 | public void updateSizes(int size) { 188 | if (size == LARGE) { 189 | setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 190 | STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 191 | } else { 192 | setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 193 | ARROW_WIDTH, ARROW_HEIGHT); 194 | } 195 | } 196 | 197 | /** 198 | * @param show Set to true to display the arrowhead on the progress spinner. 199 | */ 200 | public void showArrow(boolean show) { 201 | mRing.setShowArrow(show); 202 | } 203 | 204 | /** 205 | * @param scale Set the scale of the arrowhead for the spinner. 206 | */ 207 | public void setArrowScale(float scale) { 208 | mRing.setArrowScale(scale); 209 | } 210 | 211 | /** 212 | * Set the start and end trim for the progress spinner arc. 213 | * 214 | * @param startAngle start angle 215 | * @param endAngle end angle 216 | */ 217 | public void setStartEndTrim(float startAngle, float endAngle) { 218 | mRing.setStartTrim(startAngle); 219 | mRing.setEndTrim(endAngle); 220 | } 221 | 222 | /** 223 | * Set the amount of rotation to apply to the progress spinner. 224 | * 225 | * @param rotation Rotation is from [0..1] 226 | */ 227 | public void setProgressRotation(float rotation) { 228 | mRing.setRotation(rotation); 229 | } 230 | 231 | /** 232 | * Update the background color of the circle image view. 233 | */ 234 | public void setBackgroundColor(int color) { 235 | mBackgroundColor = color; 236 | mRing.setBackgroundColor(color); 237 | } 238 | 239 | /** 240 | * Set the colors used in the progress animation from color resources. 241 | * The first color will also be the color of the bar that grows in response 242 | * to a user swipe gesture. 243 | * 244 | * @param colors 245 | */ 246 | public void setColorSchemeColors(int... colors) { 247 | mRing.setColors(colors); 248 | mRing.setColorIndex(0); 249 | } 250 | 251 | @Override 252 | public int getIntrinsicHeight() { 253 | return (int) mHeight; 254 | } 255 | 256 | @Override 257 | public int getIntrinsicWidth() { 258 | return (int) mWidth; 259 | } 260 | 261 | @Override 262 | public void draw(Canvas c) { 263 | if (mShadow != null) { 264 | mShadow.getPaint().setColor(mBackgroundColor); 265 | mShadow.draw(c); 266 | } 267 | 268 | final Rect bounds = getBounds(); 269 | final int saveCount = c.save(); 270 | c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 271 | mRing.draw(c, bounds); 272 | c.restoreToCount(saveCount); 273 | } 274 | 275 | public int getAlpha() { 276 | return mRing.getAlpha(); 277 | } 278 | 279 | @Override 280 | public void setAlpha(int alpha) { 281 | mRing.setAlpha(alpha); 282 | } 283 | 284 | @Override 285 | public void setColorFilter(ColorFilter colorFilter) { 286 | mRing.setColorFilter(colorFilter); 287 | } 288 | 289 | @SuppressWarnings("unused") 290 | private float getRotation() { 291 | return mRotation; 292 | } 293 | 294 | @SuppressWarnings("unused") 295 | void setRotation(float rotation) { 296 | mRotation = rotation; 297 | invalidateSelf(); 298 | } 299 | 300 | @Override 301 | public int getOpacity() { 302 | return PixelFormat.TRANSLUCENT; 303 | } 304 | 305 | @Override 306 | public boolean isRunning() { 307 | final ArrayList animators = mAnimators; 308 | final int N = animators.size(); 309 | for (int i = 0; i < N; i++) { 310 | final Animation animator = animators.get(i); 311 | if (animator.hasStarted() && !animator.hasEnded()) { 312 | return true; 313 | } 314 | } 315 | return false; 316 | } 317 | 318 | @Override 319 | public void start() { 320 | mAnimation.reset(); 321 | mRing.storeOriginals(); 322 | // Already showing some part of the ring 323 | if (mRing.getEndTrim() != mRing.getStartTrim()) { 324 | mParent.startAnimation(mFinishAnimation); 325 | } else { 326 | mRing.setColorIndex(0); 327 | mRing.resetOriginals(); 328 | mParent.startAnimation(mAnimation); 329 | } 330 | } 331 | 332 | @Override 333 | public void stop() { 334 | mParent.clearAnimation(); 335 | setRotation(0); 336 | mRing.setShowArrow(false); 337 | mRing.setColorIndex(0); 338 | mRing.resetOriginals(); 339 | } 340 | 341 | private void setupAnimators() { 342 | final Ring ring = mRing; 343 | final Animation finishRingAnimation = new Animation() { 344 | public void applyTransformation(float interpolatedTime, Transformation t) { 345 | // shrink back down and complete a full rotation before starting other circles 346 | // Rotation goes between [0..1]. 347 | float targetRotation = (float) (Math.floor(ring.getStartingRotation() 348 | / MAX_PROGRESS_ARC) + 1f); 349 | final float startTrim = ring.getStartingStartTrim() 350 | + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) 351 | * interpolatedTime; 352 | ring.setStartTrim(startTrim); 353 | final float rotation = ring.getStartingRotation() 354 | + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 355 | ring.setRotation(rotation); 356 | ring.setArrowScale(1 - interpolatedTime); 357 | } 358 | }; 359 | finishRingAnimation.setInterpolator(EASE_INTERPOLATOR); 360 | finishRingAnimation.setDuration(ANIMATION_DURATION / 2); 361 | finishRingAnimation.setAnimationListener(new Animation.AnimationListener() { 362 | @Override 363 | public void onAnimationStart(Animation animation) { 364 | } 365 | 366 | @Override 367 | public void onAnimationEnd(Animation animation) { 368 | ring.goToNextColor(); 369 | ring.storeOriginals(); 370 | ring.setShowArrow(false); 371 | mParent.startAnimation(mAnimation); 372 | } 373 | 374 | @Override 375 | public void onAnimationRepeat(Animation animation) { 376 | } 377 | }); 378 | final Animation animation = new Animation() { 379 | @Override 380 | public void applyTransformation(float interpolatedTime, Transformation t) { 381 | // The minProgressArc is calculated from 0 to create an angle that 382 | // matches the stroke width. 383 | final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth() 384 | / (2 * Math.PI * ring.getCenterRadius())); 385 | final float startingEndTrim = ring.getStartingEndTrim(); 386 | final float startingTrim = ring.getStartingStartTrim(); 387 | final float startingRotation = ring.getStartingRotation(); 388 | // Offset the minProgressArc to where the endTrim is located. 389 | final float minArc = MAX_PROGRESS_ARC - minProgressArc; 390 | final float endTrim = startingEndTrim 391 | + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 392 | ring.setEndTrim(endTrim); 393 | final float startTrim = startingTrim 394 | + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR 395 | .getInterpolation(interpolatedTime)); 396 | ring.setStartTrim(startTrim); 397 | final float rotation = startingRotation + (0.25f * interpolatedTime); 398 | ring.setRotation(rotation); 399 | float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 400 | + (720.0f * (mRotationCount / NUM_POINTS)); 401 | setRotation(groupRotation); 402 | } 403 | }; 404 | animation.setRepeatCount(Animation.INFINITE); 405 | animation.setRepeatMode(Animation.RESTART); 406 | animation.setInterpolator(LINEAR_INTERPOLATOR); 407 | animation.setDuration(ANIMATION_DURATION); 408 | animation.setAnimationListener(new Animation.AnimationListener() { 409 | @Override 410 | public void onAnimationStart(Animation animation) { 411 | mRotationCount = 0; 412 | } 413 | 414 | @Override 415 | public void onAnimationEnd(Animation animation) { 416 | // do nothing 417 | } 418 | 419 | @Override 420 | public void onAnimationRepeat(Animation animation) { 421 | ring.storeOriginals(); 422 | ring.goToNextColor(); 423 | ring.setStartTrim(ring.getEndTrim()); 424 | mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 425 | } 426 | }); 427 | mFinishAnimation = finishRingAnimation; 428 | mAnimation = animation; 429 | } 430 | 431 | private static class Ring { 432 | private final RectF mTempBounds = new RectF(); 433 | private final Paint mArcPaint = new Paint(); 434 | private final Paint mArrowPaint = new Paint(); 435 | private final Callback mRingCallback; 436 | private final Paint mCirclePaint = new Paint(); 437 | private float mStartTrim = 0.0f; 438 | private float mEndTrim = 0.0f; 439 | private float mRotation = 0.0f; 440 | private float mStrokeWidth = 5.0f; 441 | private float mStrokeInset = 2.5f; 442 | private int[] mColors; 443 | // mColorIndex represents the offset into the available mColors that the 444 | // progress circle should currently display. As the progress circle is 445 | // animating, the mColorIndex moves by one to the next available color. 446 | private int mColorIndex; 447 | private float mStartingStartTrim; 448 | private float mStartingEndTrim; 449 | private float mStartingRotation; 450 | private boolean mShowArrow; 451 | private Path mArrow; 452 | private float mArrowScale; 453 | private double mRingCenterRadius; 454 | private int mArrowWidth; 455 | private int mArrowHeight; 456 | private int mAlpha; 457 | private int mBackgroundColor; 458 | 459 | public Ring(Callback callback) { 460 | mRingCallback = callback; 461 | mArcPaint.setStrokeCap(Paint.Cap.SQUARE); 462 | mArcPaint.setAntiAlias(true); 463 | mArcPaint.setStyle(Style.STROKE); 464 | mArrowPaint.setStyle(Style.FILL); 465 | mArrowPaint.setAntiAlias(true); 466 | 467 | mCirclePaint.setAntiAlias(true); 468 | } 469 | 470 | public void setBackgroundColor(int color) { 471 | mBackgroundColor = color; 472 | } 473 | 474 | /** 475 | * Set the dimensions of the arrowhead. 476 | * 477 | * @param width Width of the hypotenuse of the arrow head 478 | * @param height Height of the arrow point 479 | */ 480 | public void setArrowDimensions(float width, float height) { 481 | mArrowWidth = (int) width; 482 | mArrowHeight = (int) height; 483 | } 484 | 485 | /** 486 | * Draw the progress spinner 487 | */ 488 | public void draw(Canvas c, Rect bounds) { 489 | 490 | mCirclePaint.setColor(mBackgroundColor); 491 | mCirclePaint.setAlpha(mAlpha); 492 | 493 | c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint); 494 | 495 | final RectF arcBounds = mTempBounds; 496 | arcBounds.set(bounds); 497 | arcBounds.inset(mStrokeInset, mStrokeInset); 498 | final float startAngle = (mStartTrim + mRotation) * 360; 499 | final float endAngle = (mEndTrim + mRotation) * 360; 500 | float sweepAngle = endAngle - startAngle; 501 | mArcPaint.setColor(mColors[mColorIndex]); 502 | mArcPaint.setAlpha(mAlpha); 503 | c.drawArc(arcBounds, startAngle, sweepAngle, false, mArcPaint); 504 | drawTriangle(c, startAngle, sweepAngle, bounds); 505 | } 506 | 507 | private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 508 | if (mShowArrow) { 509 | if (mArrow == null) { 510 | mArrow = new Path(); 511 | mArrow.setFillType(Path.FillType.EVEN_ODD); 512 | } else { 513 | mArrow.reset(); 514 | } 515 | // Adjust the position of the triangle so that it is inset as 516 | // much as the arc, but also centered on the arc. 517 | float inset = (int) mStrokeInset / 2 * mArrowScale; 518 | float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 519 | float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 520 | // Update the path each time. This works around an issue in SKIA 521 | // where concatenating a rotation matrix to a scale matrix 522 | // ignored a starting negative rotation. This appears to have 523 | // been fixed as of API 21. 524 | mArrow.moveTo(0, 0); 525 | mArrow.lineTo(mArrowWidth * mArrowScale, 0); 526 | mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 527 | * mArrowScale)); 528 | mArrow.offset(x - inset, y); 529 | mArrow.close(); 530 | // draw a triangle 531 | mArrowPaint.setColor(mColors[mColorIndex]); 532 | mArrowPaint.setAlpha(mAlpha); 533 | c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 534 | bounds.exactCenterY()); 535 | c.drawPath(mArrow, mArrowPaint); 536 | } 537 | } 538 | 539 | /** 540 | * Set the colors the progress spinner alternates between. 541 | * 542 | * @param colors Array of integers describing the colors. Must be non-null. 543 | */ 544 | public void setColors(int[] colors) { 545 | mColors = colors; 546 | // if colors are reset, make sure to reset the color index as well 547 | setColorIndex(0); 548 | } 549 | 550 | /** 551 | * @param index Index into the color array of the color to display in 552 | * the progress spinner. 553 | */ 554 | public void setColorIndex(int index) { 555 | mColorIndex = index; 556 | } 557 | 558 | /** 559 | * Proceed to the next available ring color. This will automatically 560 | * wrap back to the beginning of colors. 561 | */ 562 | public void goToNextColor() { 563 | mColorIndex = (mColorIndex + 1) % (mColors.length); 564 | } 565 | 566 | public void setColorFilter(ColorFilter filter) { 567 | mArcPaint.setColorFilter(filter); 568 | invalidateSelf(); 569 | } 570 | 571 | /** 572 | * @return Current alpha of the progress spinner and arrowhead. 573 | */ 574 | public int getAlpha() { 575 | return mAlpha; 576 | } 577 | 578 | /** 579 | * @param alpha Set the alpha of the progress spinner and associated arrowhead. 580 | */ 581 | public void setAlpha(int alpha) { 582 | mAlpha = alpha; 583 | } 584 | 585 | @SuppressWarnings("unused") 586 | public float getStrokeWidth() { 587 | return mStrokeWidth; 588 | } 589 | 590 | /** 591 | * @param strokeWidth Set the stroke width of the progress spinner in pixels. 592 | */ 593 | public void setStrokeWidth(float strokeWidth) { 594 | mStrokeWidth = strokeWidth; 595 | mArcPaint.setStrokeWidth(strokeWidth); 596 | invalidateSelf(); 597 | } 598 | 599 | @SuppressWarnings("unused") 600 | public float getStartTrim() { 601 | return mStartTrim; 602 | } 603 | 604 | @SuppressWarnings("unused") 605 | public void setStartTrim(float startTrim) { 606 | mStartTrim = startTrim; 607 | invalidateSelf(); 608 | } 609 | 610 | public float getStartingStartTrim() { 611 | return mStartingStartTrim; 612 | } 613 | 614 | public float getStartingEndTrim() { 615 | return mStartingEndTrim; 616 | } 617 | 618 | @SuppressWarnings("unused") 619 | public float getEndTrim() { 620 | return mEndTrim; 621 | } 622 | 623 | @SuppressWarnings("unused") 624 | public void setEndTrim(float endTrim) { 625 | mEndTrim = endTrim; 626 | invalidateSelf(); 627 | } 628 | 629 | @SuppressWarnings("unused") 630 | public float getRotation() { 631 | return mRotation; 632 | } 633 | 634 | @SuppressWarnings("unused") 635 | public void setRotation(float rotation) { 636 | mRotation = rotation; 637 | invalidateSelf(); 638 | } 639 | 640 | public void setInsets(int width, int height) { 641 | final float minEdge = (float) Math.min(width, height); 642 | float insets; 643 | if (mRingCenterRadius <= 0 || minEdge < 0) { 644 | insets = (float) Math.ceil(mStrokeWidth / 2.0f); 645 | } else { 646 | insets = (float) (minEdge / 2.0f - mRingCenterRadius); 647 | } 648 | mStrokeInset = insets; 649 | } 650 | 651 | @SuppressWarnings("unused") 652 | public float getInsets() { 653 | return mStrokeInset; 654 | } 655 | 656 | public double getCenterRadius() { 657 | return mRingCenterRadius; 658 | } 659 | 660 | /** 661 | * @param centerRadius Inner radius in px of the circle the progress 662 | * spinner arc traces. 663 | */ 664 | public void setCenterRadius(double centerRadius) { 665 | mRingCenterRadius = centerRadius; 666 | } 667 | 668 | /** 669 | * @param show Set to true to show the arrow head on the progress spinner. 670 | */ 671 | public void setShowArrow(boolean show) { 672 | if (mShowArrow != show) { 673 | mShowArrow = show; 674 | invalidateSelf(); 675 | } 676 | } 677 | 678 | /** 679 | * @param scale Set the scale of the arrowhead for the spinner. 680 | */ 681 | public void setArrowScale(float scale) { 682 | if (scale != mArrowScale) { 683 | mArrowScale = scale; 684 | invalidateSelf(); 685 | } 686 | } 687 | 688 | /** 689 | * @return The amount the progress spinner is currently rotated, between [0..1]. 690 | */ 691 | public float getStartingRotation() { 692 | return mStartingRotation; 693 | } 694 | 695 | /** 696 | * If the start / end trim are offset to begin with, store them so that 697 | * animation starts from that offset. 698 | */ 699 | public void storeOriginals() { 700 | mStartingStartTrim = mStartTrim; 701 | mStartingEndTrim = mEndTrim; 702 | mStartingRotation = mRotation; 703 | } 704 | 705 | /** 706 | * Reset the progress spinner to default rotation, start and end angles. 707 | */ 708 | public void resetOriginals() { 709 | mStartingStartTrim = 0; 710 | mStartingEndTrim = 0; 711 | mStartingRotation = 0; 712 | setStartTrim(0); 713 | setEndTrim(0); 714 | setRotation(0); 715 | } 716 | 717 | private void invalidateSelf() { 718 | mRingCallback.invalidateDrawable(null); 719 | } 720 | } 721 | 722 | /** 723 | * Squishes the interpolation curve into the second half of the animation. 724 | */ 725 | private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 726 | @Override 727 | public float getInterpolation(float input) { 728 | return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 729 | } 730 | } 731 | 732 | /** 733 | * Squishes the interpolation curve into the first half of the animation. 734 | */ 735 | private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 736 | @Override 737 | public float getInterpolation(float input) { 738 | return super.getInterpolation(Math.min(1, input * 2.0f)); 739 | } 740 | } 741 | 742 | /** 743 | * Taken from {@link package android.support.v4.widget} 744 | */ 745 | private class OvalShadow extends OvalShape { 746 | private RadialGradient mRadialGradient; 747 | private int mShadowRadius; 748 | private Paint mShadowPaint; 749 | private int mCircleDiameter; 750 | 751 | public OvalShadow(int shadowRadius, int circleDiameter) { 752 | super(); 753 | mShadowPaint = new Paint(); 754 | mShadowRadius = shadowRadius; 755 | mCircleDiameter = circleDiameter; 756 | mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, 757 | mShadowRadius, new int[]{ 758 | FILL_SHADOW_COLOR, Color.TRANSPARENT 759 | }, null, Shader.TileMode.CLAMP); 760 | mShadowPaint.setShader(mRadialGradient); 761 | } 762 | 763 | @Override 764 | public void draw(Canvas canvas, Paint paint) { 765 | final int viewWidth = getBounds().width(); 766 | final int viewHeight = getBounds().height(); 767 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), 768 | mShadowPaint); 769 | canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); 770 | } 771 | } 772 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/header/StoreHouseBarItem.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.header; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.PointF; 6 | import android.view.animation.Animation; 7 | import android.view.animation.Transformation; 8 | 9 | import java.util.Random; 10 | 11 | /** 12 | * Created by srain on 11/6/14. 13 | */ 14 | public class StoreHouseBarItem extends Animation { 15 | 16 | public PointF midPoint; 17 | public float translationX; 18 | public int index; 19 | 20 | private final Paint mPaint = new Paint(); 21 | private float mFromAlpha = 1.0f; 22 | private float mToAlpha = 0.4f; 23 | private PointF mCStartPoint; 24 | private PointF mCEndPoint; 25 | 26 | public StoreHouseBarItem(int index, PointF start, PointF end, int color, int lineWidth) { 27 | this.index = index; 28 | 29 | midPoint = new PointF((start.x + end.x) / 2, (start.y + end.y) / 2); 30 | 31 | mCStartPoint = new PointF(start.x - midPoint.x, start.y - midPoint.y); 32 | mCEndPoint = new PointF(end.x - midPoint.x, end.y - midPoint.y); 33 | 34 | setColor(color); 35 | setLineWidth(lineWidth); 36 | mPaint.setAntiAlias(true); 37 | mPaint.setStyle(Paint.Style.STROKE); 38 | } 39 | 40 | public void setLineWidth(int width) { 41 | mPaint.setStrokeWidth(width); 42 | } 43 | 44 | public void setColor(int color) { 45 | mPaint.setColor(color); 46 | } 47 | 48 | public void resetPosition(int horizontalRandomness) { 49 | Random random = new Random(); 50 | int randomNumber = -random.nextInt(horizontalRandomness) + horizontalRandomness; 51 | translationX = randomNumber; 52 | } 53 | 54 | @Override 55 | protected void applyTransformation(float interpolatedTime, Transformation t) { 56 | float alpha = mFromAlpha; 57 | alpha = alpha + ((mToAlpha - alpha) * interpolatedTime); 58 | setAlpha(alpha); 59 | } 60 | 61 | public void start(float fromAlpha, float toAlpha) { 62 | mFromAlpha = fromAlpha; 63 | mToAlpha = toAlpha; 64 | super.start(); 65 | } 66 | 67 | public void setAlpha(float alpha) { 68 | mPaint.setAlpha((int) (alpha * 255)); 69 | } 70 | 71 | public void draw(Canvas canvas) { 72 | canvas.drawLine(mCStartPoint.x, mCStartPoint.y, mCEndPoint.x, mCEndPoint.y, mPaint); 73 | } 74 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/header/StoreHouseHeader.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.header; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Matrix; 7 | import android.graphics.PointF; 8 | import android.util.AttributeSet; 9 | import android.view.View; 10 | import android.view.animation.Transformation; 11 | 12 | import com.sgb.library.ptr.PtrFrameLayout; 13 | import com.sgb.library.ptr.PtrUIHandler; 14 | import com.sgb.library.ptr.indicator.PtrIndicator; 15 | import com.sgb.library.ptr.util.PtrLocalDisplay; 16 | 17 | import java.util.ArrayList; 18 | 19 | 20 | public class StoreHouseHeader extends View implements PtrUIHandler { 21 | 22 | public ArrayList mItemList = new ArrayList(); 23 | 24 | private int mLineWidth = -1; 25 | private float mScale = 1; 26 | private int mDropHeight = -1; 27 | private float mInternalAnimationFactor = 0.7f; 28 | private int mHorizontalRandomness = -1; 29 | 30 | private float mProgress = 0; 31 | 32 | private int mDrawZoneWidth = 0; 33 | private int mDrawZoneHeight = 0; 34 | private int mOffsetX = 0; 35 | private int mOffsetY = 0; 36 | private float mBarDarkAlpha = 0.4f; 37 | private float mFromAlpha = 1.0f; 38 | private float mToAlpha = 0.4f; 39 | 40 | private int mLoadingAniDuration = 1000; 41 | private int mLoadingAniSegDuration = 1000; 42 | private int mLoadingAniItemDuration = 400; 43 | 44 | private Transformation mTransformation = new Transformation(); 45 | private boolean mIsInLoading = false; 46 | private AniController mAniController = new AniController(); 47 | private int mTextColor = Color.WHITE; 48 | 49 | public StoreHouseHeader(Context context) { 50 | super(context); 51 | initView(); 52 | } 53 | 54 | public StoreHouseHeader(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | initView(); 57 | } 58 | 59 | public StoreHouseHeader(Context context, AttributeSet attrs, int defStyleAttr) { 60 | super(context, attrs, defStyleAttr); 61 | initView(); 62 | } 63 | 64 | private void initView() { 65 | PtrLocalDisplay.init(getContext()); 66 | mLineWidth = PtrLocalDisplay.dp2px(1); 67 | mDropHeight = PtrLocalDisplay.dp2px(40); 68 | mHorizontalRandomness = PtrLocalDisplay.SCREEN_WIDTH_PIXELS / 2; 69 | } 70 | 71 | private void setProgress(float progress) { 72 | mProgress = progress; 73 | } 74 | 75 | public int getLoadingAniDuration() { 76 | return mLoadingAniDuration; 77 | } 78 | 79 | public void setLoadingAniDuration(int duration) { 80 | mLoadingAniDuration = duration; 81 | mLoadingAniSegDuration = duration; 82 | } 83 | 84 | public StoreHouseHeader setLineWidth(int width) { 85 | mLineWidth = width; 86 | for (int i = 0; i < mItemList.size(); i++) { 87 | mItemList.get(i).setLineWidth(width); 88 | } 89 | return this; 90 | } 91 | 92 | public StoreHouseHeader setTextColor(int color) { 93 | mTextColor = color; 94 | for (int i = 0; i < mItemList.size(); i++) { 95 | mItemList.get(i).setColor(color); 96 | } 97 | return this; 98 | } 99 | 100 | public StoreHouseHeader setDropHeight(int height) { 101 | mDropHeight = height; 102 | return this; 103 | } 104 | 105 | @Override 106 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 107 | int height = getTopOffset() + mDrawZoneHeight + getBottomOffset(); 108 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 109 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 110 | 111 | mOffsetX = (getMeasuredWidth() - mDrawZoneWidth) / 2; 112 | mOffsetY = getTopOffset(); 113 | mDropHeight = getTopOffset(); 114 | } 115 | 116 | private int getTopOffset() { 117 | return getPaddingTop() + PtrLocalDisplay.dp2px(10); 118 | } 119 | 120 | private int getBottomOffset() { 121 | return getPaddingBottom() + PtrLocalDisplay.dp2px(10); 122 | } 123 | 124 | public void initWithString(String str) { 125 | initWithString(str, 25); 126 | } 127 | 128 | public void initWithString(String str, int fontSize) { 129 | ArrayList pointList = StoreHousePath.getPath(str, fontSize * 0.01f, 14); 130 | initWithPointList(pointList); 131 | } 132 | 133 | public void initWithStringArray(int id) { 134 | String[] points = getResources().getStringArray(id); 135 | ArrayList pointList = new ArrayList(); 136 | for (int i = 0; i < points.length; i++) { 137 | String[] x = points[i].split(","); 138 | float[] f = new float[4]; 139 | for (int j = 0; j < 4; j++) { 140 | f[j] = Float.parseFloat(x[j]); 141 | } 142 | pointList.add(f); 143 | } 144 | initWithPointList(pointList); 145 | } 146 | 147 | public float getScale() { 148 | return mScale; 149 | } 150 | 151 | public void setScale(float scale) { 152 | mScale = scale; 153 | } 154 | 155 | public void initWithPointList(ArrayList pointList) { 156 | 157 | float drawWidth = 0; 158 | float drawHeight = 0; 159 | boolean shouldLayout = mItemList.size() > 0; 160 | mItemList.clear(); 161 | for (int i = 0; i < pointList.size(); i++) { 162 | float[] line = pointList.get(i); 163 | PointF startPoint = new PointF(PtrLocalDisplay.dp2px(line[0]) * mScale, PtrLocalDisplay.dp2px(line[1]) * mScale); 164 | PointF endPoint = new PointF(PtrLocalDisplay.dp2px(line[2]) * mScale, PtrLocalDisplay.dp2px(line[3]) * mScale); 165 | 166 | drawWidth = Math.max(drawWidth, startPoint.x); 167 | drawWidth = Math.max(drawWidth, endPoint.x); 168 | 169 | drawHeight = Math.max(drawHeight, startPoint.y); 170 | drawHeight = Math.max(drawHeight, endPoint.y); 171 | 172 | StoreHouseBarItem item = new StoreHouseBarItem(i, startPoint, endPoint, mTextColor, mLineWidth); 173 | item.resetPosition(mHorizontalRandomness); 174 | mItemList.add(item); 175 | } 176 | mDrawZoneWidth = (int) Math.ceil(drawWidth); 177 | mDrawZoneHeight = (int) Math.ceil(drawHeight); 178 | if (shouldLayout) { 179 | requestLayout(); 180 | } 181 | } 182 | 183 | private void beginLoading() { 184 | mIsInLoading = true; 185 | mAniController.start(); 186 | invalidate(); 187 | } 188 | 189 | private void loadFinish() { 190 | mIsInLoading = false; 191 | mAniController.stop(); 192 | } 193 | 194 | @Override 195 | public void onDraw(Canvas canvas) { 196 | super.onDraw(canvas); 197 | float progress = mProgress; 198 | int c1 = canvas.save(); 199 | int len = mItemList.size(); 200 | 201 | for (int i = 0; i < len; i++) { 202 | 203 | canvas.save(); 204 | StoreHouseBarItem storeHouseBarItem = mItemList.get(i); 205 | float offsetX = mOffsetX + storeHouseBarItem.midPoint.x; 206 | float offsetY = mOffsetY + storeHouseBarItem.midPoint.y; 207 | 208 | if (mIsInLoading) { 209 | storeHouseBarItem.getTransformation(getDrawingTime(), mTransformation); 210 | canvas.translate(offsetX, offsetY); 211 | } else { 212 | 213 | if (progress == 0) { 214 | storeHouseBarItem.resetPosition(mHorizontalRandomness); 215 | continue; 216 | } 217 | 218 | float startPadding = (1 - mInternalAnimationFactor) * i / len; 219 | float endPadding = 1 - mInternalAnimationFactor - startPadding; 220 | 221 | // done 222 | if (progress == 1 || progress >= 1 - endPadding) { 223 | canvas.translate(offsetX, offsetY); 224 | storeHouseBarItem.setAlpha(mBarDarkAlpha); 225 | } else { 226 | float realProgress; 227 | if (progress <= startPadding) { 228 | realProgress = 0; 229 | } else { 230 | realProgress = Math.min(1, (progress - startPadding) / mInternalAnimationFactor); 231 | } 232 | offsetX += storeHouseBarItem.translationX * (1 - realProgress); 233 | offsetY += -mDropHeight * (1 - realProgress); 234 | Matrix matrix = new Matrix(); 235 | matrix.postRotate(360 * realProgress); 236 | matrix.postScale(realProgress, realProgress); 237 | matrix.postTranslate(offsetX, offsetY); 238 | storeHouseBarItem.setAlpha(mBarDarkAlpha * realProgress); 239 | canvas.concat(matrix); 240 | } 241 | } 242 | storeHouseBarItem.draw(canvas); 243 | canvas.restore(); 244 | } 245 | if (mIsInLoading) { 246 | invalidate(); 247 | } 248 | canvas.restoreToCount(c1); 249 | } 250 | 251 | @Override 252 | public void onUIReset(PtrFrameLayout frame) { 253 | loadFinish(); 254 | for (int i = 0; i < mItemList.size(); i++) { 255 | mItemList.get(i).resetPosition(mHorizontalRandomness); 256 | 257 | } 258 | } 259 | 260 | @Override 261 | public void onUIRefreshPrepare(PtrFrameLayout frame) { 262 | 263 | } 264 | 265 | @Override 266 | public void onUIRefreshBegin(PtrFrameLayout frame) { 267 | beginLoading(); 268 | } 269 | 270 | @Override 271 | public void onUIRefreshComplete(PtrFrameLayout frame) { 272 | loadFinish(); 273 | } 274 | 275 | @Override 276 | public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) { 277 | 278 | float currentPercent = Math.min(1f, ptrIndicator.getCurrentPercent()); 279 | setProgress(currentPercent); 280 | invalidate(); 281 | } 282 | 283 | private class AniController implements Runnable { 284 | 285 | private int mTick = 0; 286 | private int mCountPerSeg = 0; 287 | private int mSegCount = 0; 288 | private int mInterval = 0; 289 | private boolean mRunning = true; 290 | 291 | private void start() { 292 | mRunning = true; 293 | mTick = 0; 294 | 295 | mInterval = mLoadingAniDuration / mItemList.size(); 296 | mCountPerSeg = mLoadingAniSegDuration / mInterval; 297 | mSegCount = mItemList.size() / mCountPerSeg + 1; 298 | run(); 299 | } 300 | 301 | @Override 302 | public void run() { 303 | 304 | int pos = mTick % mCountPerSeg; 305 | for (int i = 0; i < mSegCount; i++) { 306 | 307 | int index = i * mCountPerSeg + pos; 308 | if (index > mTick) { 309 | continue; 310 | } 311 | 312 | index = index % mItemList.size(); 313 | StoreHouseBarItem item = mItemList.get(index); 314 | 315 | item.setFillAfter(false); 316 | item.setFillEnabled(true); 317 | item.setFillBefore(false); 318 | item.setDuration(mLoadingAniItemDuration); 319 | item.start(mFromAlpha, mToAlpha); 320 | } 321 | 322 | mTick++; 323 | if (mRunning) { 324 | postDelayed(this, mInterval); 325 | } 326 | } 327 | 328 | private void stop() { 329 | mRunning = false; 330 | removeCallbacks(this); 331 | } 332 | } 333 | } -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/header/StoreHousePath.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.header; 2 | 3 | import android.util.SparseArray; 4 | 5 | import java.util.ArrayList; 6 | 7 | /** 8 | * Created by srain on 11/7/14. 9 | */ 10 | public class StoreHousePath { 11 | 12 | private static final SparseArray sPointList; 13 | 14 | static { 15 | sPointList = new SparseArray(); 16 | float[][] LETTERS = new float[][]{ 17 | new float[]{ 18 | // A 19 | 24, 0, 1, 22, 20 | 1, 22, 1, 72, 21 | 24, 0, 47, 22, 22 | 47, 22, 47, 72, 23 | 1, 48, 47, 48 24 | }, 25 | 26 | new float[]{ 27 | // B 28 | 0, 0, 0, 72, 29 | 0, 0, 37, 0, 30 | 37, 0, 47, 11, 31 | 47, 11, 47, 26, 32 | 47, 26, 38, 36, 33 | 38, 36, 0, 36, 34 | 38, 36, 47, 46, 35 | 47, 46, 47, 61, 36 | 47, 61, 38, 71, 37 | 37, 72, 0, 72, 38 | }, 39 | 40 | new float[]{ 41 | // C 42 | 47, 0, 0, 0, 43 | 0, 0, 0, 72, 44 | 0, 72, 47, 72, 45 | }, 46 | 47 | new float[]{ 48 | // D 49 | 0, 0, 0, 72, 50 | 0, 0, 24, 0, 51 | 24, 0, 47, 22, 52 | 47, 22, 47, 48, 53 | 47, 48, 23, 72, 54 | 23, 72, 0, 72, 55 | }, 56 | 57 | new float[]{ 58 | // E 59 | 0, 0, 0, 72, 60 | 0, 0, 47, 0, 61 | 0, 36, 37, 36, 62 | 0, 72, 47, 72, 63 | }, 64 | 65 | new float[]{ 66 | // F 67 | 0, 0, 0, 72, 68 | 0, 0, 47, 0, 69 | 0, 36, 37, 36, 70 | }, 71 | 72 | new float[]{ 73 | // G 74 | 47, 23, 47, 0, 75 | 47, 0, 0, 0, 76 | 0, 0, 0, 72, 77 | 0, 72, 47, 72, 78 | 47, 72, 47, 48, 79 | 47, 48, 24, 48, 80 | }, 81 | 82 | new float[]{ 83 | // H 84 | 0, 0, 0, 72, 85 | 0, 36, 47, 36, 86 | 47, 0, 47, 72, 87 | }, 88 | 89 | new float[]{ 90 | // I 91 | 0, 0, 47, 0, 92 | 24, 0, 24, 72, 93 | 0, 72, 47, 72, 94 | }, 95 | 96 | new float[]{ 97 | // J 98 | 47, 0, 47, 72, 99 | 47, 72, 24, 72, 100 | 24, 72, 0, 48, 101 | }, 102 | 103 | new float[]{ 104 | // K 105 | 0, 0, 0, 72, 106 | 47, 0, 3, 33, 107 | 3, 38, 47, 72, 108 | }, 109 | 110 | new float[]{ 111 | // L 112 | 0, 0, 0, 72, 113 | 0, 72, 47, 72, 114 | }, 115 | 116 | new float[]{ 117 | // M 118 | 0, 0, 0, 72, 119 | 0, 0, 24, 23, 120 | 24, 23, 47, 0, 121 | 47, 0, 47, 72, 122 | }, 123 | 124 | new float[]{ 125 | // N 126 | 0, 0, 0, 72, 127 | 0, 0, 47, 72, 128 | 47, 72, 47, 0, 129 | }, 130 | 131 | new float[]{ 132 | // O 133 | 0, 0, 0, 72, 134 | 0, 72, 47, 72, 135 | 47, 72, 47, 0, 136 | 47, 0, 0, 0, 137 | }, 138 | 139 | new float[]{ 140 | // P 141 | 0, 0, 0, 72, 142 | 0, 0, 47, 0, 143 | 47, 0, 47, 36, 144 | 47, 36, 0, 36, 145 | }, 146 | 147 | new float[]{ 148 | // Q 149 | 0, 0, 0, 72, 150 | 0, 72, 23, 72, 151 | 23, 72, 47, 48, 152 | 47, 48, 47, 0, 153 | 47, 0, 0, 0, 154 | 24, 28, 47, 71, 155 | }, 156 | 157 | new float[]{ 158 | // R 159 | 0, 0, 0, 72, 160 | 0, 0, 47, 0, 161 | 47, 0, 47, 36, 162 | 47, 36, 0, 36, 163 | 0, 37, 47, 72, 164 | }, 165 | 166 | new float[]{ 167 | // S 168 | 47, 0, 0, 0, 169 | 0, 0, 0, 36, 170 | 0, 36, 47, 36, 171 | 47, 36, 47, 72, 172 | 47, 72, 0, 72, 173 | }, 174 | 175 | new float[]{ 176 | // T 177 | 0, 0, 47, 0, 178 | 24, 0, 24, 72, 179 | }, 180 | 181 | new float[]{ 182 | // U 183 | 0, 0, 0, 72, 184 | 0, 72, 47, 72, 185 | 47, 72, 47, 0, 186 | }, 187 | 188 | new float[]{ 189 | // V 190 | 0, 0, 24, 72, 191 | 24, 72, 47, 0, 192 | }, 193 | 194 | new float[]{ 195 | // W 196 | 0, 0, 0, 72, 197 | 0, 72, 24, 49, 198 | 24, 49, 47, 72, 199 | 47, 72, 47, 0 200 | }, 201 | 202 | new float[]{ 203 | // X 204 | 0, 0, 47, 72, 205 | 47, 0, 0, 72 206 | }, 207 | 208 | new float[]{ 209 | // Y 210 | 0, 0, 24, 23, 211 | 47, 0, 24, 23, 212 | 24, 23, 24, 72 213 | }, 214 | 215 | new float[]{ 216 | // Z 217 | 0, 0, 47, 0, 218 | 47, 0, 0, 72, 219 | 0, 72, 47, 72 220 | }, 221 | }; 222 | final float[][] NUMBERS = new float[][]{ 223 | new float[]{ 224 | // 0 225 | 0, 0, 0, 72, 226 | 0, 72, 47, 72, 227 | 47, 72, 47, 0, 228 | 47, 0, 0, 0, 229 | }, 230 | new float[]{ 231 | // 1 232 | 24, 0, 24, 72, 233 | }, 234 | 235 | new float[]{ 236 | // 2 237 | 0, 0, 47, 0, 238 | 47, 0, 47, 36, 239 | 47, 36, 0, 36, 240 | 0, 36, 0, 72, 241 | 0, 72, 47, 72 242 | }, 243 | 244 | new float[]{ 245 | // 3 246 | 0, 0, 47, 0, 247 | 47, 0, 47, 36, 248 | 47, 36, 0, 36, 249 | 47, 36, 47, 72, 250 | 47, 72, 0, 72, 251 | }, 252 | 253 | new float[]{ 254 | // 4 255 | 0, 0, 0, 36, 256 | 0, 36, 47, 36, 257 | 47, 0, 47, 72, 258 | }, 259 | 260 | new float[]{ 261 | // 5 262 | 0, 0, 0, 36, 263 | 0, 36, 47, 36, 264 | 47, 36, 47, 72, 265 | 47, 72, 0, 72, 266 | 0, 0, 47, 0 267 | }, 268 | 269 | new float[]{ 270 | // 6 271 | 0, 0, 0, 72, 272 | 0, 72, 47, 72, 273 | 47, 72, 47, 36, 274 | 47, 36, 0, 36 275 | }, 276 | 277 | new float[]{ 278 | // 7 279 | 0, 0, 47, 0, 280 | 47, 0, 47, 72 281 | }, 282 | 283 | new float[]{ 284 | // 8 285 | 0, 0, 0, 72, 286 | 0, 72, 47, 72, 287 | 47, 72, 47, 0, 288 | 47, 0, 0, 0, 289 | 0, 36, 47, 36 290 | }, 291 | 292 | new float[]{ 293 | // 9 294 | 47, 0, 0, 0, 295 | 0, 0, 0, 36, 296 | 0, 36, 47, 36, 297 | 47, 0, 47, 72, 298 | } 299 | }; 300 | // A - Z 301 | for (int i = 0; i < LETTERS.length; i++) { 302 | sPointList.append(i + 65, LETTERS[i]); 303 | } 304 | // a - z 305 | for (int i = 0; i < LETTERS.length; i++) { 306 | sPointList.append(i + 65 + 32, LETTERS[i]); 307 | } 308 | // 0 - 9 309 | for (int i = 0; i < NUMBERS.length; i++) { 310 | sPointList.append(i + 48, NUMBERS[i]); 311 | } 312 | // blank 313 | addChar(' ', new float[]{}); 314 | // - 315 | addChar('-', new float[]{ 316 | 0, 36, 47, 36 317 | }); 318 | // . 319 | addChar('.', new float[]{ 320 | 24, 60, 24, 72 321 | }); 322 | } 323 | 324 | public static void addChar(char c, float[] points) { 325 | sPointList.append(c, points); 326 | } 327 | 328 | public static ArrayList getPath(String str) { 329 | return getPath(str, 1, 14); 330 | } 331 | 332 | /** 333 | * @param str 334 | * @param scale 335 | * @param gapBetweenLetter 336 | * @return ArrayList of float[] {x1, y1, x2, y2} 337 | */ 338 | public static ArrayList getPath(String str, float scale, int gapBetweenLetter) { 339 | ArrayList list = new ArrayList(); 340 | float offsetForWidth = 0; 341 | for (int i = 0; i < str.length(); i++) { 342 | int pos = str.charAt(i); 343 | int key = sPointList.indexOfKey(pos); 344 | if (key == -1) { 345 | continue; 346 | } 347 | float[] points = sPointList.get(pos); 348 | int pointCount = points.length / 4; 349 | 350 | for (int j = 0; j < pointCount; j++) { 351 | float[] line = new float[4]; 352 | for (int k = 0; k < 4; k++) { 353 | float l = points[j * 4 + k]; 354 | // x 355 | if (k % 2 == 0) { 356 | line[k] = (l + offsetForWidth) * scale; 357 | } 358 | // y 359 | else { 360 | line[k] = l * scale; 361 | } 362 | } 363 | list.add(line); 364 | } 365 | offsetForWidth += 57 + gapBetweenLetter; 366 | } 367 | return list; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/indicator/PtrIndicator.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.indicator; 2 | 3 | import android.graphics.PointF; 4 | 5 | public class PtrIndicator { 6 | 7 | public final static int POS_START = 0; 8 | protected int mOffsetToRefresh = 0; 9 | private PointF mPtLastMove = new PointF(); 10 | private float mOffsetX; 11 | private float mOffsetY; 12 | private int mCurrentPos = 0; 13 | private int mLastPos = 0; 14 | private int mHeaderHeight; 15 | private int mPressedPos = 0; 16 | 17 | private float mRatioOfHeaderHeightToRefresh = 1.2f; 18 | private float mResistance = 1.7f; 19 | private boolean mIsUnderTouch = false; 20 | private int mOffsetToKeepHeaderWhileLoading = -1; 21 | // record the refresh complete position 22 | private int mRefreshCompleteY = 0; 23 | 24 | public boolean isUnderTouch() { 25 | return mIsUnderTouch; 26 | } 27 | 28 | public float getResistance() { 29 | return mResistance; 30 | } 31 | 32 | public void setResistance(float resistance) { 33 | mResistance = resistance; 34 | } 35 | 36 | public void onRelease() { 37 | mIsUnderTouch = false; 38 | } 39 | 40 | public void onUIRefreshComplete() { 41 | mRefreshCompleteY = mCurrentPos; 42 | } 43 | 44 | public boolean goDownCrossFinishPosition() { 45 | return mCurrentPos >= mRefreshCompleteY; 46 | } 47 | 48 | protected void processOnMove(float currentX, float currentY, float offsetX, float offsetY) { 49 | setOffset(offsetX, offsetY / mResistance); 50 | } 51 | 52 | public void setRatioOfHeaderHeightToRefresh(float ratio) { 53 | mRatioOfHeaderHeightToRefresh = ratio; 54 | mOffsetToRefresh = (int) (mHeaderHeight * ratio); 55 | } 56 | 57 | public float getRatioOfHeaderToHeightRefresh() { 58 | return mRatioOfHeaderHeightToRefresh; 59 | } 60 | 61 | public int getOffsetToRefresh() { 62 | return mOffsetToRefresh; 63 | } 64 | 65 | public void setOffsetToRefresh(int offset) { 66 | mRatioOfHeaderHeightToRefresh = mHeaderHeight * 1f / offset; 67 | mOffsetToRefresh = offset; 68 | } 69 | 70 | public void onPressDown(float x, float y) { 71 | mIsUnderTouch = true; 72 | mPressedPos = mCurrentPos; 73 | mPtLastMove.set(x, y); 74 | } 75 | 76 | public final void onMove(float x, float y) { 77 | float offsetX = x - mPtLastMove.x; 78 | float offsetY = (y - mPtLastMove.y); 79 | processOnMove(x, y, offsetX, offsetY); 80 | mPtLastMove.set(x, y); 81 | } 82 | 83 | protected void setOffset(float x, float y) { 84 | mOffsetX = x; 85 | mOffsetY = y; 86 | } 87 | 88 | public float getOffsetX() { 89 | return mOffsetX; 90 | } 91 | 92 | public float getOffsetY() { 93 | return mOffsetY; 94 | } 95 | 96 | public int getLastPosY() { 97 | return mLastPos; 98 | } 99 | 100 | public int getCurrentPosY() { 101 | return mCurrentPos; 102 | } 103 | 104 | /** 105 | * Update current position before update the UI 106 | */ 107 | public final void setCurrentPos(int current) { 108 | mLastPos = mCurrentPos; 109 | mCurrentPos = current; 110 | onUpdatePos(current, mLastPos); 111 | } 112 | 113 | protected void onUpdatePos(int current, int last) { 114 | 115 | } 116 | 117 | public int getHeaderHeight() { 118 | return mHeaderHeight; 119 | } 120 | 121 | public void setHeaderHeight(int height) { 122 | mHeaderHeight = height; 123 | updateHeight(); 124 | } 125 | 126 | protected void updateHeight() { 127 | mOffsetToRefresh = (int) (mRatioOfHeaderHeightToRefresh * mHeaderHeight); 128 | } 129 | 130 | public void convertFrom(PtrIndicator ptrSlider) { 131 | mCurrentPos = ptrSlider.mCurrentPos; 132 | mLastPos = ptrSlider.mLastPos; 133 | mHeaderHeight = ptrSlider.mHeaderHeight; 134 | } 135 | 136 | public boolean hasLeftStartPosition() { 137 | return mCurrentPos > POS_START; 138 | } 139 | 140 | public boolean hasJustLeftStartPosition() { 141 | return mLastPos == POS_START && hasLeftStartPosition(); 142 | } 143 | 144 | public boolean hasJustBackToStartPosition() { 145 | return mLastPos != POS_START && isInStartPosition(); 146 | } 147 | 148 | public boolean isOverOffsetToRefresh() { 149 | return mCurrentPos >= getOffsetToRefresh(); 150 | } 151 | 152 | public boolean hasMovedAfterPressedDown() { 153 | return mCurrentPos != mPressedPos; 154 | } 155 | 156 | public boolean isInStartPosition() { 157 | return mCurrentPos == POS_START; 158 | } 159 | 160 | public boolean crossRefreshLineFromTopToBottom() { 161 | return mLastPos < getOffsetToRefresh() && mCurrentPos >= getOffsetToRefresh(); 162 | } 163 | 164 | public boolean hasJustReachedHeaderHeightFromTopToBottom() { 165 | return mLastPos < mHeaderHeight && mCurrentPos >= mHeaderHeight; 166 | } 167 | 168 | public boolean isOverOffsetToKeepHeaderWhileLoading() { 169 | return mCurrentPos > getOffsetToKeepHeaderWhileLoading(); 170 | } 171 | 172 | public void setOffsetToKeepHeaderWhileLoading(int offset) { 173 | mOffsetToKeepHeaderWhileLoading = offset; 174 | } 175 | 176 | public int getOffsetToKeepHeaderWhileLoading() { 177 | return mOffsetToKeepHeaderWhileLoading >= 0 ? mOffsetToKeepHeaderWhileLoading : mHeaderHeight; 178 | } 179 | 180 | public boolean isAlreadyHere(int to) { 181 | return mCurrentPos == to; 182 | } 183 | 184 | public float getLastPercent() { 185 | final float oldPercent = mHeaderHeight == 0 ? 0 : mLastPos * 1f / mHeaderHeight; 186 | return oldPercent; 187 | } 188 | 189 | public float getCurrentPercent() { 190 | final float currentPercent = mHeaderHeight == 0 ? 0 : mCurrentPos * 1f / mHeaderHeight; 191 | return currentPercent; 192 | } 193 | 194 | public boolean willOverTop(int to) { 195 | return to < POS_START; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/indicator/PtrTensionIndicator.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.indicator; 2 | 3 | public class PtrTensionIndicator extends PtrIndicator { 4 | 5 | private float DRAG_RATE = 0.5f; 6 | private float mDownY; 7 | private float mDownPos; 8 | private float mOneHeight = 0; 9 | 10 | private float mCurrentDragPercent; 11 | 12 | private int mReleasePos; 13 | private float mReleasePercent = -1; 14 | 15 | @Override 16 | public void onPressDown(float x, float y) { 17 | super.onPressDown(x, y); 18 | mDownY = y; 19 | mDownPos = getCurrentPosY(); 20 | } 21 | 22 | @Override 23 | public void onRelease() { 24 | super.onRelease(); 25 | mReleasePos = getCurrentPosY(); 26 | mReleasePercent = mCurrentDragPercent; 27 | } 28 | 29 | @Override 30 | public void onUIRefreshComplete() { 31 | mReleasePos = getCurrentPosY(); 32 | mReleasePercent = getOverDragPercent(); 33 | } 34 | 35 | @Override 36 | public void setHeaderHeight(int height) { 37 | super.setHeaderHeight(height); 38 | mOneHeight = height * 4f / 5; 39 | } 40 | 41 | @Override 42 | protected void processOnMove(float currentX, float currentY, float offsetX, float offsetY) { 43 | 44 | if (currentY < mDownY) { 45 | super.processOnMove(currentX, currentY, offsetX, offsetY); 46 | return; 47 | } 48 | 49 | // distance from top 50 | final float scrollTop = (currentY - mDownY) * DRAG_RATE + mDownPos; 51 | final float currentDragPercent = scrollTop / mOneHeight; 52 | 53 | if (currentDragPercent < 0) { 54 | setOffset(offsetX, 0); 55 | return; 56 | } 57 | 58 | mCurrentDragPercent = currentDragPercent; 59 | 60 | // 0 ~ 1 61 | float boundedDragPercent = Math.min(1f, Math.abs(currentDragPercent)); 62 | float extraOS = scrollTop - mOneHeight; 63 | 64 | // 0 ~ 2 65 | // if extraOS lower than 0, which means scrollTop lower than onHeight, tensionSlingshotPercent will be 0. 66 | float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, mOneHeight * 2) / mOneHeight); 67 | 68 | float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; 69 | float extraMove = (mOneHeight) * tensionPercent / 2; 70 | int targetY = (int) ((mOneHeight * boundedDragPercent) + extraMove); 71 | int change = targetY - getCurrentPosY(); 72 | 73 | setOffset(currentX, change); 74 | } 75 | 76 | private float offsetToTarget(float scrollTop) { 77 | 78 | // distance from top 79 | final float currentDragPercent = scrollTop / mOneHeight; 80 | 81 | mCurrentDragPercent = currentDragPercent; 82 | 83 | // 0 ~ 1 84 | float boundedDragPercent = Math.min(1f, Math.abs(currentDragPercent)); 85 | float extraOS = scrollTop - mOneHeight; 86 | 87 | // 0 ~ 2 88 | // if extraOS lower than 0, which means scrollTop lower than mOneHeight, tensionSlingshotPercent will be 0. 89 | float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, mOneHeight * 2) / mOneHeight); 90 | 91 | float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; 92 | float extraMove = (mOneHeight) * tensionPercent / 2; 93 | int targetY = (int) ((mOneHeight * boundedDragPercent) + extraMove); 94 | 95 | return 0; 96 | } 97 | 98 | @Override 99 | public int getOffsetToKeepHeaderWhileLoading() { 100 | return getOffsetToRefresh(); 101 | } 102 | 103 | @Override 104 | public int getOffsetToRefresh() { 105 | return (int) mOneHeight; 106 | } 107 | 108 | public float getOverDragPercent() { 109 | if (isUnderTouch()) { 110 | return mCurrentDragPercent; 111 | } else { 112 | if (mReleasePercent <= 0) { 113 | return 1.0f * getCurrentPosY() / getOffsetToKeepHeaderWhileLoading(); 114 | } 115 | // after release 116 | return mReleasePercent * getCurrentPosY() / mReleasePos; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/util/PtrCLog.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.util; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * An encapsulation of {@link Log}, enable log level and print log with parameters. 7 | * 8 | * @author http://www.liaohuqiu.net/ 9 | */ 10 | public class PtrCLog { 11 | 12 | public static final int LEVEL_VERBOSE = 0; 13 | public static final int LEVEL_DEBUG = 1; 14 | public static final int LEVEL_INFO = 2; 15 | public static final int LEVEL_WARNING = 3; 16 | public static final int LEVEL_ERROR = 4; 17 | public static final int LEVEL_FATAL = 5; 18 | 19 | private static int sLevel = LEVEL_VERBOSE; 20 | 21 | /** 22 | * set log level, the level lower than this level will not be logged 23 | * 24 | * @param level 25 | */ 26 | public static void setLogLevel(int level) { 27 | sLevel = level; 28 | } 29 | 30 | /** 31 | * Send a VERBOSE log message. 32 | * 33 | * @param tag 34 | * @param msg 35 | */ 36 | public static void v(String tag, String msg) { 37 | if (sLevel > LEVEL_VERBOSE) { 38 | return; 39 | } 40 | Log.v(tag, msg); 41 | } 42 | 43 | /** 44 | * Send a VERBOSE log message. 45 | * 46 | * @param tag 47 | * @param msg 48 | * @param throwable 49 | */ 50 | public static void v(String tag, String msg, Throwable throwable) { 51 | if (sLevel > LEVEL_VERBOSE) { 52 | return; 53 | } 54 | Log.v(tag, msg, throwable); 55 | } 56 | 57 | /** 58 | * Send a VERBOSE log message. 59 | * 60 | * @param tag 61 | * @param msg 62 | * @param args 63 | */ 64 | public static void v(String tag, String msg, Object... args) { 65 | if (sLevel > LEVEL_VERBOSE) { 66 | return; 67 | } 68 | if (args.length > 0) { 69 | msg = String.format(msg, args); 70 | } 71 | Log.v(tag, msg); 72 | } 73 | 74 | /** 75 | * Send a DEBUG log message 76 | * 77 | * @param tag 78 | * @param msg 79 | */ 80 | public static void d(String tag, String msg) { 81 | if (sLevel > LEVEL_DEBUG) { 82 | return; 83 | } 84 | Log.d(tag, msg); 85 | } 86 | 87 | /** 88 | * Send a DEBUG log message 89 | * 90 | * @param tag 91 | * @param msg 92 | * @param args 93 | */ 94 | public static void d(String tag, String msg, Object... args) { 95 | if (sLevel > LEVEL_DEBUG) { 96 | return; 97 | } 98 | if (args.length > 0) { 99 | msg = String.format(msg, args); 100 | } 101 | Log.d(tag, msg); 102 | } 103 | 104 | /** 105 | * Send a DEBUG log message 106 | * 107 | * @param tag 108 | * @param msg 109 | * @param throwable 110 | */ 111 | public static void d(String tag, String msg, Throwable throwable) { 112 | if (sLevel > LEVEL_DEBUG) { 113 | return; 114 | } 115 | Log.d(tag, msg, throwable); 116 | } 117 | 118 | /** 119 | * Send an INFO log message 120 | * 121 | * @param tag 122 | * @param msg 123 | */ 124 | public static void i(String tag, String msg) { 125 | if (sLevel > LEVEL_INFO) { 126 | return; 127 | } 128 | Log.i(tag, msg); 129 | } 130 | 131 | /** 132 | * Send an INFO log message 133 | * 134 | * @param tag 135 | * @param msg 136 | * @param args 137 | */ 138 | public static void i(String tag, String msg, Object... args) { 139 | if (sLevel > LEVEL_INFO) { 140 | return; 141 | } 142 | if (args.length > 0) { 143 | msg = String.format(msg, args); 144 | } 145 | Log.i(tag, msg); 146 | } 147 | 148 | /** 149 | * Send an INFO log message 150 | * 151 | * @param tag 152 | * @param msg 153 | */ 154 | public static void i(String tag, String msg, Throwable throwable) { 155 | if (sLevel > LEVEL_INFO) { 156 | return; 157 | } 158 | Log.i(tag, msg, throwable); 159 | } 160 | 161 | /** 162 | * Send a WARNING log message 163 | * 164 | * @param tag 165 | * @param msg 166 | */ 167 | public static void w(String tag, String msg) { 168 | if (sLevel > LEVEL_WARNING) { 169 | return; 170 | } 171 | Log.w(tag, msg); 172 | } 173 | 174 | /** 175 | * Send a WARNING log message 176 | * 177 | * @param tag 178 | * @param msg 179 | * @param args 180 | */ 181 | public static void w(String tag, String msg, Object... args) { 182 | if (sLevel > LEVEL_WARNING) { 183 | return; 184 | } 185 | if (args.length > 0) { 186 | msg = String.format(msg, args); 187 | } 188 | Log.w(tag, msg); 189 | } 190 | 191 | /** 192 | * Send a WARNING log message 193 | * 194 | * @param tag 195 | * @param msg 196 | * @param throwable 197 | */ 198 | public static void w(String tag, String msg, Throwable throwable) { 199 | if (sLevel > LEVEL_WARNING) { 200 | return; 201 | } 202 | Log.w(tag, msg, throwable); 203 | } 204 | 205 | /** 206 | * Send an ERROR log message 207 | * 208 | * @param tag 209 | * @param msg 210 | */ 211 | public static void e(String tag, String msg) { 212 | if (sLevel > LEVEL_ERROR) { 213 | return; 214 | } 215 | Log.e(tag, msg); 216 | } 217 | 218 | /** 219 | * Send an ERROR log message 220 | * 221 | * @param tag 222 | * @param msg 223 | * @param args 224 | */ 225 | public static void e(String tag, String msg, Object... args) { 226 | if (sLevel > LEVEL_ERROR) { 227 | return; 228 | } 229 | if (args.length > 0) { 230 | msg = String.format(msg, args); 231 | } 232 | Log.e(tag, msg); 233 | } 234 | 235 | /** 236 | * Send an ERROR log message 237 | * 238 | * @param tag 239 | * @param msg 240 | * @param throwable 241 | */ 242 | public static void e(String tag, String msg, Throwable throwable) { 243 | if (sLevel > LEVEL_ERROR) { 244 | return; 245 | } 246 | Log.e(tag, msg, throwable); 247 | } 248 | 249 | /** 250 | * Send a FATAL ERROR log message 251 | * 252 | * @param tag 253 | * @param msg 254 | */ 255 | public static void f(String tag, String msg) { 256 | if (sLevel > LEVEL_FATAL) { 257 | return; 258 | } 259 | Log.wtf(tag, msg); 260 | } 261 | 262 | /** 263 | * Send a FATAL ERROR log message 264 | * 265 | * @param tag 266 | * @param msg 267 | * @param args 268 | */ 269 | public static void f(String tag, String msg, Object... args) { 270 | if (sLevel > LEVEL_FATAL) { 271 | return; 272 | } 273 | if (args.length > 0) { 274 | msg = String.format(msg, args); 275 | } 276 | Log.wtf(tag, msg); 277 | } 278 | 279 | /** 280 | * Send a FATAL ERROR log message 281 | * 282 | * @param tag 283 | * @param msg 284 | * @param throwable 285 | */ 286 | public static void f(String tag, String msg, Throwable throwable) { 287 | if (sLevel > LEVEL_FATAL) { 288 | return; 289 | } 290 | Log.wtf(tag, msg, throwable); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /android/library/src/main/java/com/sgb/library/ptr/util/PtrLocalDisplay.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library.ptr.util; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | import android.view.View; 6 | import android.view.WindowManager; 7 | 8 | public class PtrLocalDisplay { 9 | 10 | public static int SCREEN_WIDTH_PIXELS; 11 | public static int SCREEN_HEIGHT_PIXELS; 12 | public static float SCREEN_DENSITY; 13 | public static int SCREEN_WIDTH_DP; 14 | public static int SCREEN_HEIGHT_DP; 15 | 16 | public static void init(Context context) { 17 | if (context == null) { 18 | return; 19 | } 20 | DisplayMetrics dm = new DisplayMetrics(); 21 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 22 | wm.getDefaultDisplay().getMetrics(dm); 23 | SCREEN_WIDTH_PIXELS = dm.widthPixels; 24 | SCREEN_HEIGHT_PIXELS = dm.heightPixels; 25 | SCREEN_DENSITY = dm.density; 26 | SCREEN_WIDTH_DP = (int) (SCREEN_WIDTH_PIXELS / dm.density); 27 | SCREEN_HEIGHT_DP = (int) (SCREEN_HEIGHT_PIXELS / dm.density); 28 | } 29 | 30 | public static int dp2px(float dp) { 31 | final float scale = SCREEN_DENSITY; 32 | return (int) (dp * scale + 0.5f); 33 | } 34 | 35 | public static int designedDP2px(float designedDp) { 36 | if (SCREEN_WIDTH_DP != 320) { 37 | designedDp = designedDp * SCREEN_WIDTH_DP / 320f; 38 | } 39 | return dp2px(designedDp); 40 | } 41 | 42 | public static void setPadding(final View view, float left, float top, float right, float bottom) { 43 | view.setPadding(designedDP2px(left), dp2px(top), designedDP2px(right), dp2px(bottom)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /android/library/src/main/res/drawable-xhdpi/ptr_rotate_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/android/library/src/main/res/drawable-xhdpi/ptr_rotate_arrow.png -------------------------------------------------------------------------------- /android/library/src/main/res/layout/cube_ptr_classic_default_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 21 | 22 | 30 | 31 | 32 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/library/src/main/res/layout/cube_ptr_simple_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /android/library/src/main/res/values/cube_ptr_attrs.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /android/library/src/main/res/values/cube_ptr_string.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pull Down 5 | Pull Down to Refresh 6 | Release To Refresh 7 | Updating... 8 | Updated. 9 | 10 | last update:  11 |  seconds ago 12 |  minutes ago 13 |  hours ago 14 | 15 | -------------------------------------------------------------------------------- /android/library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Library 3 | 4 | -------------------------------------------------------------------------------- /android/library/src/test/java/com/sgb/library/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.sgb.library; 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 | } -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'RNAndroidPullToRefresh' 2 | 3 | include ':app', ':library' 4 | -------------------------------------------------------------------------------- /art/screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panda912/RNAndroidPullToRefresh/14edefa16670e5b4c84f16f6f175d45ae260be27/art/screen.gif -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import {AppRegistry} from 'react-native'; 8 | import RNAndroidPullToRefresh from './src/Main'; 9 | 10 | AppRegistry.registerComponent('RNAndroidPullToRefresh', () => RNAndroidPullToRefresh); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RNAndroidPullToRefresh", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "react": "~15.4.0-rc.4", 11 | "react-native": "0.40.0" 12 | }, 13 | "devDependencies": { 14 | "babel-jest": "18.0.0", 15 | "babel-preset-react-native": "1.9.1", 16 | "jest": "18.1.0", 17 | "react-test-renderer": "~15.4.0-rc.4" 18 | }, 19 | "jest": { 20 | "preset": "react-native" 21 | } 22 | } -------------------------------------------------------------------------------- /src/Main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by panda on 2017/1/18. 3 | */ 4 | import React, {Component} from 'react'; 5 | import { 6 | View, 7 | StatusBar, 8 | ToolbarAndroid, 9 | ScrollView, 10 | ToastAndroid, 11 | } from 'react-native'; 12 | import PtrFrame from './component/PtrComponent'; 13 | 14 | export default class extends Component { 15 | render() { 16 | return ( 17 | 18 | 19 | 23 | this._getData()} 26 | durationToCloseHeader={300} 27 | durationToClose={200} 28 | resistance={2} 29 | pinContent={false} 30 | ratioOfHeaderHeightToRefresh={1.2} 31 | pullToRefresh={false} 32 | keepHeaderWhenRefresh={true} 33 | style={{flex: 1}}> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | _getData() { 48 | ToastAndroid.show("refreshing", ToastAndroid.SHORT); 49 | this.timer = setTimeout(() => { 50 | this.refs.ptr.refreshComplete(); 51 | }, 1500); 52 | }; 53 | 54 | componentWillUnmount() { 55 | this.timer && clearTimeout(this.timer); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/component/PtrComponent.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | PropTypes, 4 | } from 'react'; 5 | import { 6 | requireNativeComponent, 7 | View, 8 | } from 'react-native'; 9 | const UIManager = require('UIManager'); 10 | const ReactNative = require('ReactNative'); 11 | const REF_PTR = "ptr_ref"; 12 | 13 | export default class PtrComponent extends Component { 14 | constructor(props) { 15 | super(props); 16 | this._onRefresh = this._onRefresh.bind(this); 17 | } 18 | 19 | _onRefresh() { 20 | if (!this.props.handleRefresh) { 21 | return; 22 | } 23 | this.props.handleRefresh(); 24 | }; 25 | 26 | /** 27 | * 自动刷新 28 | */ 29 | autoRefresh() { 30 | let self = this; 31 | UIManager.dispatchViewManagerCommand( 32 | ReactNative.findNodeHandle(self.refs[REF_PTR]), 33 | 1, 34 | null 35 | ); 36 | } 37 | 38 | /** 39 | * 刷新完成 40 | */ 41 | refreshComplete() { 42 | UIManager.dispatchViewManagerCommand( 43 | ReactNative.findNodeHandle(this.refs[REF_PTR]), 44 | 0, 45 | null 46 | ); 47 | } 48 | 49 | render() { 50 | // onPtrRefresh 事件对应原生的ptrRefresh事件 51 | return ( 52 | this._onRefresh()}/> 56 | ); 57 | } 58 | } 59 | 60 | PtrComponent.name = "RCTPtrAndroid"; //便于调试时显示(可以设置为任意字符串) 61 | PtrComponent.propTypes = { 62 | handleRefresh: PropTypes.func, 63 | resistance: PropTypes.number, 64 | durationToCloseHeader: PropTypes.number, 65 | durationToClose: PropTypes.number, 66 | ratioOfHeaderHeightToRefresh: PropTypes.number, 67 | pullToRefresh: PropTypes.bool, 68 | keepHeaderWhenRefresh: PropTypes.bool, 69 | pinContent: PropTypes.bool, 70 | ...View.propTypes, 71 | }; 72 | 73 | const RCTPtrAndroid = requireNativeComponent('RCTPtrAndroid', PtrComponent, {nativeOnly: {onPtrRefresh: true}}); --------------------------------------------------------------------------------