├── src ├── prop-types │ ├── index.web.ts │ ├── common.ts │ └── index.ts ├── utils │ ├── index.web.ts │ ├── index.ts │ └── common.ts ├── components │ ├── common.ts │ ├── static-container.ts │ ├── index.ts │ ├── index.web.tsx │ ├── RNScrollView.tsx │ └── agent-scroll-view.tsx ├── index.ts ├── types │ └── index.ts ├── abstract-class │ ├── index.ts │ ├── common.ts │ ├── scroll-paged-abstract.tsx │ └── view-paged-abstract.tsx ├── view-paged │ ├── view-paged.web.tsx │ ├── view-paged.tsx │ └── index.tsx └── scroll-paged │ ├── scroll-paged.web.tsx │ ├── scroll-paged.tsx │ └── index.tsx ├── demo ├── ios.gif ├── tab.gif ├── web.gif ├── android.gif ├── carousel.gif └── horizontal.gif ├── .gitmodules ├── .huskyrc ├── .lintstagedrc ├── .eslintignore ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── taumu │ │ └── rnscrollview │ │ ├── RNScrollViewPackage.java │ │ ├── RNHorizontalScrollViewPackage.java │ │ ├── RNScrollViewCommandHelper.java │ │ ├── RNScrollViewManager.java │ │ ├── RNHorizontalScrollViewManager.java │ │ ├── RNScrollView.java │ │ └── RNHorizontalScrollView.java ├── .project ├── build.gradle └── react-scroll-paged-view.iml ├── .prettierrc ├── .editorconfig ├── jsconfig.json ├── .npmrc ├── commitlint.config.js ├── tsconfig.json ├── .npmignore ├── android-old-version ├── rnscrollview-0.47 │ ├── RNScrollViewPackage.java │ ├── RNScrollViewCommandHelper.java │ ├── RNScrollViewManager.java │ └── RNScrollView.java └── rnscrollview-0.54 │ ├── RNScrollViewPackage.java │ ├── RNScrollViewCommandHelper.java │ ├── RNScrollViewManager.java │ └── RNScrollView.java ├── LICENSE ├── .gitignore ├── .eslintrc.js ├── package.json ├── docs └── Dev_Record.md ├── README_zh-CN.md └── README.md /src/prop-types/index.web.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | -------------------------------------------------------------------------------- /src/utils/index.web.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | 3 | export const isWeb = true 4 | -------------------------------------------------------------------------------- /demo/ios.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/ios.gif -------------------------------------------------------------------------------- /demo/tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/tab.gif -------------------------------------------------------------------------------- /demo/web.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/web.gif -------------------------------------------------------------------------------- /demo/android.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/android.gif -------------------------------------------------------------------------------- /demo/carousel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/carousel.gif -------------------------------------------------------------------------------- /demo/horizontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaumuLu/react-scroll-paged-view/HEAD/demo/horizontal.gif -------------------------------------------------------------------------------- /src/components/common.ts: -------------------------------------------------------------------------------- 1 | import StaticContainer from './static-container' 2 | 3 | export { StaticContainer } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example"] 2 | path = example 3 | url = https://github.com/TaumuLu/react-native-example 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{json,html,md}": [ 3 | "prettier --write" 4 | ], 5 | "*.{js,jsx,ts,tsx}": [ 6 | "eslint --fix" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | public/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | example/node_modules/ 7 | 8 | # eslint ignores hidden files by default 9 | !.* 10 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import ScrollPagedView from './scroll-paged' 2 | import ViewPaged from './view-paged' 3 | 4 | export default ScrollPagedView 5 | 6 | export { ViewPaged } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "singleQuote": true, 4 | "jsxBracketSameLine": false, 5 | "semi": false, 6 | "trailingComma": "none", 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native' 2 | 3 | export * from './common' 4 | 5 | export const isIOS = Platform.OS === 'ios' 6 | 7 | export const isAndroid = Platform.OS === 'android' 8 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Animated } from '../components' 2 | 3 | export interface IViewPagedState { 4 | width: number 5 | height: number 6 | pos: Animated.Value 7 | isReady: boolean 8 | loadIndex: number[] 9 | } 10 | -------------------------------------------------------------------------------- /src/abstract-class/index.ts: -------------------------------------------------------------------------------- 1 | import ViewPagedAbstract from './view-paged-abstract' 2 | import ScrollPagedAbstract, { DirectionValues } from './scroll-paged-abstract' 3 | 4 | export { ScrollPagedAbstract, ViewPagedAbstract, DirectionValues } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 对所有文件生效 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # 对后缀名为 md 的文件生效 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": {} 7 | }, 8 | "exclude": [ 9 | "node_modules", 10 | "web_modules", 11 | "android", 12 | "ios", 13 | "public", 14 | "vendor", 15 | "example-rn", 16 | "lib" 17 | ], 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @ali:registry=https://registry.npm.alibaba-inc.com/ 2 | @alife:registry=https://registry.npm.alibaba-inc.com/ 3 | @terminus:registry=https://registry.npm.terminus.io/ 4 | 5 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 6 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 7 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 8 | 9 | registry=https://registry.npm.taobao.org/ 10 | //registry=https://registry.npmjs.org/ 11 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'feat', 9 | 'fix', 10 | 'docs', 11 | 'style', 12 | 'refactor', 13 | 'test', 14 | 'chore', 15 | 'revert', 16 | 'temp' 17 | ] 18 | ], 19 | 'subject-full-stop': [0, 'never'], 20 | 'subject-case': [0, 'never'] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android_ 4 | Project android_ created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/static-container.ts: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | interface IProps { 4 | children: React.ReactNode 5 | shouldUpdate: boolean 6 | } 7 | 8 | export default class StaticContainer extends Component { 9 | shouldComponentUpdate(nextProps) { 10 | return !!nextProps.shouldUpdate 11 | } 12 | 13 | render() { 14 | const { children } = this.props 15 | if (children === null || children === false) { 16 | return null 17 | } 18 | 19 | return React.Children.only(children) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile 'com.facebook.react:react-native:+' 23 | } 24 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { View, Easing, Animated } from 'react-native' 2 | import AgentScrollView from './agent-scroll-view' 3 | 4 | export * from './common' 5 | 6 | const AnimatedView = Animated.View 7 | 8 | const Style = { 9 | containerStyle: { 10 | flex: 1, 11 | overflow: 'hidden', 12 | position: 'relative' 13 | }, 14 | wrapStyle: { 15 | flex: 1, 16 | overflow: 'hidden', 17 | position: 'relative' 18 | }, 19 | AnimatedStyle: { 20 | flex: 1 21 | }, 22 | pageStyle: {} 23 | } 24 | 25 | export { Style, Animated, AnimatedView, Easing, View, AgentScrollView } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "./lib", 5 | "target": "es5", 6 | "module": "commonjs", 7 | "allowJs": false, 8 | "checkJs": false, 9 | "jsx": "react", 10 | "declaration": true, 11 | "sourceMap": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "esModuleInterop": true, 17 | "downlevelIteration": true, 18 | "experimentalDecorators": true, 19 | "lib": ["esnext"] 20 | }, 21 | "include": ["./src/**/*"], 22 | "exclude": ["node_modules", "examples", "docs", "lib", "build", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ide配置 2 | .vscode 3 | .idea 4 | .gradle 5 | .settings 6 | 7 | # 文件 8 | *.gif 9 | 10 | # 日志 11 | npm-debug.log 12 | 13 | # android 14 | gradlew 15 | gradlew.bat 16 | *.properties 17 | *.iml 18 | 19 | # Folder 20 | demo/ 21 | doc/ 22 | docs/ 23 | example/ 24 | examples/ 25 | src/ 26 | backup/ 27 | test/ 28 | interface/ 29 | 30 | # config file 31 | 32 | # babel 33 | .babelrc 34 | .babelrc.js 35 | babel.config.js 36 | 37 | # EditorConfig 38 | .editorconfig 39 | 40 | # eslint 41 | .eslintignore 42 | .eslintrc.yml 43 | 44 | # husky 45 | .huskyrc 46 | 47 | # lint-staged 48 | .lintstagedrc 49 | 50 | # prettier 51 | .prettierrc 52 | 53 | # commitlint 54 | commitlint.config.js 55 | 56 | # vscode config 57 | jsconfig.json 58 | 59 | # typesscript 60 | tsconfig.json 61 | 62 | # nodemon 63 | nodemon.json 64 | 65 | android-old-version/ 66 | src/ 67 | -------------------------------------------------------------------------------- /src/abstract-class/common.ts: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from 'react' 2 | import { size, get } from '../utils' 3 | import { IProps } from '../prop-types' 4 | 5 | export default class Common

extends Component { 6 | public childrenList: ReactNode[] 7 | 8 | public childrenSize: number 9 | 10 | public _childrenList: ReactNode[] 11 | 12 | public _childrenSize: number 13 | 14 | getChildren( 15 | children: ReactNode[] = get(this.props, 'children'), 16 | handleFunc: (child: ReactNode) => ReactNode = (child) => child 17 | ) { 18 | return React.Children.map(children, handleFunc) 19 | } 20 | 21 | setChildrenAttr() { 22 | this.childrenList = this.getChildren() 23 | this.childrenSize = size(this.childrenList) 24 | // 记录初次children值,childrenList之后可能会改变 25 | this._childrenList = this.childrenList 26 | this._childrenSize = this.childrenSize 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.47/RNScrollViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 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 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class RNScrollViewPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | // Deprecated RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.singletonList(new RNScrollViewManager()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.54/RNScrollViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 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 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class RNScrollViewPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | // Deprecated RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.singletonList(new RNScrollViewManager()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNScrollViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 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 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class RNScrollViewPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | // Deprecated RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.singletonList(new RNScrollViewManager()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNHorizontalScrollViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 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 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class RNHorizontalScrollViewPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | // Deprecated RN 0.47 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.singletonList(new RNHorizontalScrollViewManager()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/abstract-class/scroll-paged-abstract.tsx: -------------------------------------------------------------------------------- 1 | import { PanResponderCallbacks } from 'react-native' 2 | import { IProps } from '../prop-types' 3 | import Common from './common' 4 | 5 | export type DirectionValues = 'isStart' | 'isEnd' | 'none' 6 | 7 | export default abstract class ScrollPaged

extends Common< 8 | P, 9 | S 10 | > { 11 | protected _viewPagedProps?: PanResponderCallbacks 12 | 13 | public isBorder: boolean 14 | 15 | public isResponder: boolean 16 | 17 | public startX: number 18 | 19 | public startY: number 20 | 21 | public isTouchMove: boolean 22 | 23 | public hasScrollViewPages: number[] 24 | 25 | public borderDirection: DirectionValues 26 | 27 | public abstract setBorderValue( 28 | startValue: number, 29 | endValue: number, 30 | maxValue: number 31 | ): void 32 | 33 | public abstract _TouchStartEvent(x: number, y: number): void 34 | 35 | public abstract _TouchMoveEvent( 36 | x: number, 37 | y: number, 38 | sizeValue: number, 39 | layoutValue: number 40 | ): void 41 | 42 | public abstract _setScrollViewRef(ref: any): void 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 TaumuLu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/abstract-class/view-paged-abstract.tsx: -------------------------------------------------------------------------------- 1 | import { PanResponderInstance, GestureResponderHandlers } from 'react-native' 2 | import { IProps } from '../prop-types' 3 | import { IViewPagedState } from '../types' 4 | import Common from './common' 5 | 6 | export default abstract class ViewPaged< 7 | P = IProps, 8 | S = IViewPagedState 9 | > extends Common { 10 | protected _panResponder?: PanResponderInstance 11 | 12 | protected _AnimatedViewProps?: 13 | | GestureResponderHandlers 14 | | { 15 | onTouchStart: (e: any) => void 16 | ref: (ref: any) => void 17 | // onTouchMove: this._onTouchMove, 18 | onTouchEnd: (e: any) => void 19 | } 20 | 21 | public _isScrollView: boolean 22 | 23 | public _initialPage: number 24 | 25 | public _posPage: number 26 | 27 | public abstract _renderPage(): JSX.Element[] 28 | 29 | public abstract _onChange(): void 30 | 31 | public abstract _TouchStartEvent(): void 32 | 33 | public abstract _TouchMoveEvent(touchState: any): void 34 | 35 | public abstract _TouchEndEvent(touchState: any): void 36 | 37 | public abstract _runMeasurements(width: number, height: number): void 38 | } 39 | -------------------------------------------------------------------------------- /src/components/index.web.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animated from 'animated/lib/targets/react-dom' 3 | import Easing from 'animated/lib/Easing' 4 | 5 | export * from './common' 6 | 7 | const AnimatedView = Animated.div 8 | 9 | const View = (props) => { 10 | const { onLayout, ...otherProps } = props 11 | const extraProps: { ref?: any } = {} 12 | if (onLayout) { 13 | extraProps.ref = onLayout 14 | } 15 | 16 | return

17 | } 18 | 19 | const Style = { 20 | containerStyle: { 21 | flex: 1, 22 | display: 'flex', 23 | boxSizing: 'border-box', 24 | position: 'relative', 25 | overflow: 'hidden', 26 | width: '100%', 27 | height: '100%' 28 | }, 29 | wrapStyle: { 30 | flex: 1, 31 | display: 'flex', 32 | boxSizing: 'border-box', 33 | position: 'relative', 34 | overflow: 'hidden', 35 | width: '100%', 36 | height: '100%' 37 | }, 38 | AnimatedStyle: { 39 | flex: 1, 40 | display: 'flex' 41 | }, 42 | pageStyle: { 43 | flex: 1, 44 | display: 'flex', 45 | overflow: 'hidden' 46 | } 47 | } 48 | 49 | export { Style, Animated, AnimatedView, Easing, View } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # mac 64 | .DS_Store 65 | 66 | #################################### 67 | 68 | .vscode/ 69 | .idea/ 70 | .gradle/ 71 | .settings/ 72 | 73 | # custom 74 | lib/ 75 | build/ 76 | dist/ 77 | -------------------------------------------------------------------------------- /src/components/RNScrollView.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { 3 | ScrollView, 4 | Platform, 5 | findNodeHandle, 6 | requireNativeComponent, 7 | UIManager, 8 | ScrollViewProps 9 | } from 'react-native' 10 | 11 | export default class RNScrollView extends ScrollView { 12 | setScrollEnabled = (value) => { 13 | UIManager.dispatchViewManagerCommand( 14 | findNodeHandle(this), 15 | // @ts-ignore 16 | UIManager.RNScrollView.Commands.setScrollEnabled, 17 | [value] 18 | ) 19 | } 20 | 21 | render() { 22 | const reactElement = super.render() as ReactElement 23 | if (Platform.OS === 'ios') { 24 | return reactElement 25 | } 26 | const { props } = reactElement 27 | const { horizontal, ...otherProps } = props as ScrollViewProps 28 | 29 | if (horizontal) { 30 | return 31 | } 32 | return 33 | } 34 | } 35 | 36 | const nativeOnlyProps = { 37 | nativeOnly: { 38 | sendMomentumEvents: true 39 | } 40 | } 41 | 42 | let AndroidScrollView 43 | let AndroidHorizontalScrollView 44 | if (Platform.OS !== 'ios') { 45 | AndroidScrollView = requireNativeComponent( 46 | 'RNScrollView', 47 | // @ts-ignore 48 | ScrollView, 49 | nativeOnlyProps 50 | ) 51 | AndroidHorizontalScrollView = requireNativeComponent( 52 | 'RNHorizontalScrollView', 53 | // @ts-ignore 54 | ScrollView, 55 | nativeOnlyProps 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/prop-types/common.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | export const propTypes = { 4 | style: PropTypes.object, 5 | initialPage: PropTypes.number, 6 | vertical: PropTypes.bool, 7 | onChange: PropTypes.func, 8 | duration: PropTypes.number, 9 | withRef: PropTypes.bool, 10 | infinite: PropTypes.bool, 11 | renderHeader: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 12 | renderFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 13 | renderPosition: PropTypes.string, 14 | autoPlay: PropTypes.bool, 15 | autoPlaySpeed: PropTypes.number, 16 | hasAnimation: PropTypes.bool, 17 | locked: PropTypes.bool, 18 | preRenderRange: PropTypes.number, 19 | isMovingRender: PropTypes.bool, 20 | onScroll: PropTypes.func 21 | // children: PropTypes.array.isRequired, 22 | } 23 | 24 | export const defaultProps = { 25 | style: {}, 26 | initialPage: 0, 27 | vertical: true, 28 | onChange: undefined, 29 | duration: 400, 30 | withRef: false, 31 | infinite: false, 32 | renderHeader: undefined, 33 | renderFooter: undefined, 34 | renderPosition: 'top', 35 | autoPlay: false, 36 | autoPlaySpeed: 2000, 37 | hasAnimation: true, 38 | locked: false, 39 | preRenderRange: 0, 40 | isMovingRender: false, 41 | onScroll: undefined 42 | } 43 | 44 | export interface IProps { 45 | style: Record 46 | initialPage: number 47 | vertical: boolean 48 | onChange: (currentPage: number, prevPage: number) => void 49 | duration: number 50 | withRef: boolean 51 | infinite: boolean 52 | renderHeader: any 53 | renderFooter: any 54 | renderPosition: 'top' | 'left' | 'bottom' | 'right' 55 | autoPlay: boolean 56 | autoPlaySpeed: number 57 | hasAnimation: boolean 58 | locked: boolean 59 | preRenderRange: number 60 | isMovingRender: boolean 61 | onScroll: (event: any) => void 62 | } 63 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNScrollViewCommandHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | *

5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.taumu.rnscrollview; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | import java.util.Map; 15 | 16 | import com.facebook.react.bridge.ReadableArray; 17 | import com.facebook.react.views.scroll.ReactScrollViewCommandHelper; 18 | 19 | public class RNScrollViewCommandHelper extends ReactScrollViewCommandHelper { 20 | 21 | public static final int COMMAND_SET_SCROLL_ENABLED = 10; 22 | 23 | public interface ScrollCommandHandler extends ReactScrollViewCommandHelper.ScrollCommandHandler { 24 | void setRNScrollEnabled(T scrollView, boolean data); 25 | } 26 | 27 | public static Map getCommandsMap() { 28 | Map map = ReactScrollViewCommandHelper.getCommandsMap(); 29 | map.put("setScrollEnabled", COMMAND_SET_SCROLL_ENABLED); 30 | return map; 31 | } 32 | 33 | public static void receiveCommand( 34 | ScrollCommandHandler viewManager, 35 | T scrollView, 36 | int commandType, 37 | @Nullable ReadableArray args) { 38 | 39 | try { 40 | ReactScrollViewCommandHelper.receiveCommand(viewManager, scrollView, commandType, args); 41 | } catch (IllegalArgumentException e) { 42 | if(commandType == COMMAND_SET_SCROLL_ENABLED) { 43 | boolean isEnable = args.getBoolean(0); 44 | viewManager.setRNScrollEnabled(scrollView, isEnable); 45 | } else { 46 | throw e; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended', 7 | 'prettier', 8 | 'prettier/react', 9 | 'prettier/@typescript-eslint' 10 | ], 11 | env: { 12 | es6: true, 13 | browser: true, 14 | node: true, 15 | mocha: true, 16 | 'jest/globals': true 17 | }, 18 | plugins: ['@typescript-eslint', 'prettier', 'jest'], 19 | settings: { 20 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], 21 | 'import/resolver': { 22 | node: { 23 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 24 | } 25 | } 26 | }, 27 | rules: { 28 | 'react/jsx-filename-extension': 0, 29 | 'react/jsx-one-expression-per-line': 0, 30 | 'react/prefer-stateless-function': 0, 31 | 'no-use-before-define': 0, 32 | 'import/no-extraneous-dependencies': 0, 33 | 'jsx-a11y/click-events-have-key-events': 0, 34 | 'jsx-a11y/no-static-element-interactions': 0, 35 | 'react/prop-types': 0, 36 | '@typescript-eslint/explicit-function-return-type': 0, 37 | '@typescript-eslint/interface-name-prefix': 0, 38 | 'no-await-in-loop': 0, 39 | 'no-restricted-syntax': 0, 40 | 'interface-name': [0, 'never-prefix'], 41 | 'import/extensions': [ 42 | 2, 43 | 'ignorePackages', 44 | { 45 | js: 'never', 46 | mjs: 'never', 47 | jsx: 'never', 48 | ts: 'never', 49 | tsx: 'never' 50 | } 51 | ], 52 | 'no-underscore-dangle': 0, 53 | 'react/static-property-placement': 0, 54 | 'import/prefer-default-export': 0, 55 | 'react/jsx-props-no-spreading': 0, 56 | 'no-unused-expressions': 0, 57 | 'no-param-reassign': 0, 58 | 'no-plusplus': 0, 59 | 'react/no-array-index-key': 0, 60 | '@typescript-eslint/ban-ts-ignore': 0, 61 | 'class-methods-use-this': 0, 62 | 'max-classes-per-file': 0, 63 | 'no-nested-ternary': 0 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNScrollViewManager.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import android.util.Log; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import java.util.Map; 8 | 9 | import com.facebook.react.bridge.ReadableArray; 10 | import com.facebook.react.module.annotations.ReactModule; 11 | import com.facebook.react.uimanager.ThemedReactContext; 12 | import com.facebook.react.views.scroll.FpsListener; 13 | import com.facebook.react.views.scroll.ReactScrollView; 14 | import com.facebook.react.views.scroll.ReactScrollViewManager; 15 | 16 | @ReactModule(name = RNScrollViewManager.REACT_CLASS) 17 | public class RNScrollViewManager extends ReactScrollViewManager 18 | implements RNScrollViewCommandHelper.ScrollCommandHandler { 19 | 20 | protected static final String REACT_CLASS = "RNScrollView"; 21 | private @Nullable 22 | FpsListener mFpsListener = null; 23 | 24 | @Override 25 | public String getName() { 26 | return REACT_CLASS; 27 | } 28 | 29 | public RNScrollViewManager() { 30 | this(null); 31 | } 32 | 33 | public RNScrollViewManager(@Nullable FpsListener fpsListener) { 34 | mFpsListener = fpsListener; 35 | } 36 | 37 | 38 | @Override 39 | public ReactScrollView createViewInstance(ThemedReactContext context) { 40 | return new RNScrollView(context, mFpsListener); 41 | } 42 | 43 | @Override 44 | public @Nullable 45 | Map getCommandsMap() { 46 | return RNScrollViewCommandHelper.getCommandsMap(); 47 | } 48 | 49 | @Override 50 | public void receiveCommand( 51 | ReactScrollView scrollView, 52 | int commandId, 53 | @Nullable ReadableArray args) { 54 | RNScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); 55 | } 56 | 57 | public void setRNScrollEnabled(ReactScrollView scrollView, boolean value) { 58 | scrollView.setScrollEnabled(value); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-paged-view", 3 | "version": "2.3.0", 4 | "description": "Inside scroll, Full page scroll", 5 | "main": "./lib", 6 | "scripts": { 7 | "build": "rm -rf ./lib && tsc", 8 | "watch": "tsc -w", 9 | "prepublishOnly": "npm run build", 10 | "lint": "lint-staged" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/TaumuLu/react-scroll-paged-view.git" 15 | }, 16 | "author": "TaumuLu", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/TaumuLu/react-scroll-paged-view/issues" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "react-native", 24 | "scrollView", 25 | "Full page scroll", 26 | "Full screen scroll", 27 | "scroll", 28 | "paged", 29 | "view" 30 | ], 31 | "homepage": "https://github.com/TaumuLu/react-scroll-paged-view#readme", 32 | "dependencies": { 33 | "animated": "^0.2.2" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "^16.9.34", 37 | "@types/react-native": "^0.62.4", 38 | "@commitlint/cli": "^8.3.5", 39 | "@commitlint/config-conventional": "^8.3.4", 40 | "@typescript-eslint/eslint-plugin": "^2.29.0", 41 | "@typescript-eslint/parser": "^2.29.0", 42 | "eslint": "^6.8.0", 43 | "eslint-config-airbnb": "^18.1.0", 44 | "eslint-config-prettier": "^6.11.0", 45 | "eslint-plugin-babel": "^5.3.0", 46 | "eslint-plugin-import": "^2.20.2", 47 | "eslint-plugin-jest": "^23.8.2", 48 | "eslint-plugin-jsx-a11y": "^6.2.3", 49 | "eslint-plugin-prettier": "^3.1.3", 50 | "eslint-plugin-react": "^7.19.0", 51 | "eslint-plugin-react-hooks": "^3.0.0", 52 | "husky": "^4.2.5", 53 | "lint-staged": "^10.1.7", 54 | "prettier": "^2.0.5", 55 | "typescript": "^3.8.3", 56 | "react": "16.9.0", 57 | "prop-types": "^15.7.2", 58 | "react-native": "0.61.5" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16.13.1", 62 | "prop-types": "^15.7.2", 63 | "react-native": "^0.62.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/prop-types/index.ts: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { PanResponderCallbacks } from 'react-native' 3 | 4 | import { noop } from '../utils' 5 | import { 6 | propTypes as commonPropTypes, 7 | defaultProps as commonDefaultProps, 8 | IProps as commonIProps 9 | } from './common' 10 | 11 | const defaultResponder = (isResponder) => () => isResponder 12 | 13 | const panResponderMap = { 14 | onStartShouldSetPanResponder: { 15 | propType: PropTypes.func, 16 | defaultValue: defaultResponder(true) 17 | }, 18 | onStartShouldSetPanResponderCapture: { 19 | propType: PropTypes.func, 20 | defaultValue: defaultResponder(false) 21 | }, 22 | onMoveShouldSetPanResponder: { 23 | propType: PropTypes.func, 24 | defaultValue: defaultResponder(true) 25 | }, 26 | onMoveShouldSetPanResponderCapture: { 27 | propType: PropTypes.func, 28 | defaultValue: defaultResponder(false) 29 | }, 30 | onPanResponderTerminationRequest: { 31 | propType: PropTypes.func, 32 | defaultValue: defaultResponder(true) 33 | }, 34 | onShouldBlockNativeResponder: { 35 | propType: PropTypes.func, 36 | defaultValue: defaultResponder(true) 37 | }, 38 | onPanResponderTerminate: { 39 | propType: PropTypes.func, 40 | defaultValue: noop 41 | } 42 | } 43 | 44 | export const panResponderKey = Object.keys(panResponderMap) 45 | 46 | const getProperty = (name: string) => { 47 | return panResponderKey.reduce((p, k) => { 48 | p[k] = panResponderMap[k][name] 49 | return p 50 | }, {}) 51 | } 52 | 53 | export const propTypes = { 54 | ...commonPropTypes, 55 | ...getProperty('propType'), 56 | useScrollView: PropTypes.bool, 57 | scrollViewProps: PropTypes.object 58 | } 59 | 60 | export const defaultProps = { 61 | ...commonDefaultProps, 62 | ...getProperty('defaultValue'), 63 | useScrollView: true, 64 | scrollViewProps: {} 65 | } 66 | 67 | type keys = keyof typeof panResponderMap 68 | 69 | type PanResponder = Pick 70 | 71 | export interface IProps extends commonIProps, PanResponder { 72 | useScrollView?: boolean 73 | scrollViewProps?: object 74 | } 75 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNHorizontalScrollViewManager.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import android.util.Log; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import java.util.Map; 8 | 9 | import com.facebook.react.bridge.ReadableArray; 10 | import com.facebook.react.module.annotations.ReactModule; 11 | import com.facebook.react.uimanager.ThemedReactContext; 12 | import com.facebook.react.views.scroll.FpsListener; 13 | import com.facebook.react.views.scroll.ReactHorizontalScrollView; 14 | import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager; 15 | 16 | 17 | @ReactModule(name = RNHorizontalScrollViewManager.REACT_CLASS) 18 | public class RNHorizontalScrollViewManager extends ReactHorizontalScrollViewManager 19 | implements RNScrollViewCommandHelper.ScrollCommandHandler { 20 | 21 | protected static final String REACT_CLASS = "RNHorizontalScrollView"; 22 | private @Nullable 23 | FpsListener mFpsListener = null; 24 | 25 | @Override 26 | public String getName() { 27 | return REACT_CLASS; 28 | } 29 | 30 | public RNHorizontalScrollViewManager() { 31 | this(null); 32 | } 33 | 34 | public RNHorizontalScrollViewManager(@Nullable FpsListener fpsListener) { 35 | mFpsListener = fpsListener; 36 | } 37 | 38 | 39 | @Override 40 | public ReactHorizontalScrollView createViewInstance(ThemedReactContext context) { 41 | return new RNHorizontalScrollView(context, mFpsListener); 42 | } 43 | 44 | @Override 45 | public @Nullable 46 | Map getCommandsMap() { 47 | return RNScrollViewCommandHelper.getCommandsMap(); 48 | } 49 | 50 | @Override 51 | public void receiveCommand( 52 | ReactHorizontalScrollView scrollView, 53 | int commandId, 54 | @Nullable ReadableArray args) { 55 | RNScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); 56 | } 57 | 58 | public void setRNScrollEnabled(ReactHorizontalScrollView scrollView, boolean value) { 59 | scrollView.setScrollEnabled(value); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/agent-scroll-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { ScrollViewProps } from 'react-native' 3 | import RNScrollView from './RNScrollView' 4 | import { get } from '../utils' 5 | 6 | const viewKeys = ['scrollViewRef', 'scrollViewLayout', 'scrollViewSize'] 7 | 8 | interface IAgentScrollView extends ScrollViewProps { 9 | nativeProps?: any 10 | } 11 | 12 | export default class AgentScrollView extends Component { 13 | scrollViewRef: any 14 | 15 | scrollViewSize: any 16 | 17 | scrollViewLayout: { width: number; height: number } 18 | 19 | setScrollEnabled = (flag) => { 20 | if (this.scrollViewRef) { 21 | this.scrollViewRef.setScrollEnabled(flag) 22 | } 23 | } 24 | 25 | _setScrollViewSize = (width, height) => { 26 | if (width && height) { 27 | const oldSize = this.scrollViewSize || {} 28 | this.scrollViewSize = { width, height } 29 | 30 | const { onContentSizeChange } = this.props 31 | if (onContentSizeChange) onContentSizeChange(oldSize, this.scrollViewSize) 32 | } 33 | } 34 | 35 | _setScrollViewLayout = (event) => { 36 | if (event) { 37 | const { onLayout } = this.props 38 | const { layout } = event.nativeEvent 39 | const height = Math.ceil(layout.height) 40 | const width = Math.ceil(layout.width) 41 | 42 | this.scrollViewLayout = { width, height } 43 | onLayout && onLayout(event) 44 | } 45 | } 46 | 47 | _setScrollViewRef = (ref) => { 48 | if (ref) this.scrollViewRef = ref 49 | } 50 | 51 | _agentMethod = (propsKey, event) => { 52 | const { nativeProps } = this.props 53 | const method = get(this.props, propsKey) 54 | if (method) { 55 | const agentInfo = viewKeys.reduce( 56 | (p, key) => ({ ...p, [key]: this[key] || {} }), 57 | {} 58 | ) 59 | method(event, agentInfo) 60 | const nativeMethod = get(nativeProps, propsKey) 61 | nativeMethod && nativeMethod(event) 62 | } 63 | } 64 | 65 | render() { 66 | return ( 67 | this._agentMethod('onTouchStart', event)} 75 | onTouchMove={(event) => this._agentMethod('onTouchMove', event)} 76 | // onTouchEnd={this._onTouchEnd} 77 | /> 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.47/RNScrollViewCommandHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.taumu.rnscrollview; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | import java.util.Map; 15 | 16 | import com.facebook.react.bridge.ReadableArray; 17 | import com.facebook.react.uimanager.PixelUtil; 18 | import com.facebook.infer.annotation.Assertions; 19 | import com.facebook.react.common.MapBuilder; 20 | 21 | 22 | public class RNScrollViewCommandHelper { 23 | 24 | public static final int COMMAND_SCROLL_TO = 1; 25 | public static final int COMMAND_SCROLL_TO_END = 2; 26 | public static final int COMMAND_SET_SCROLL_ENABLED = 3; 27 | 28 | public interface ScrollCommandHandler { 29 | void scrollTo(T scrollView, ScrollToCommandData data); 30 | void scrollToEnd(T scrollView, ScrollToEndCommandData data); 31 | void setRNScrollEnabled(T scrollView, boolean data); 32 | } 33 | 34 | public static class ScrollToCommandData { 35 | 36 | public final int mDestX, mDestY; 37 | public final boolean mAnimated; 38 | 39 | ScrollToCommandData(int destX, int destY, boolean animated) { 40 | mDestX = destX; 41 | mDestY = destY; 42 | mAnimated = animated; 43 | } 44 | } 45 | 46 | public static class ScrollToEndCommandData { 47 | 48 | public final boolean mAnimated; 49 | 50 | ScrollToEndCommandData(boolean animated) { 51 | mAnimated = animated; 52 | } 53 | } 54 | 55 | public static Map getCommandsMap() { 56 | return MapBuilder.of( 57 | "scrollTo", 58 | COMMAND_SCROLL_TO, 59 | "scrollToEnd", 60 | COMMAND_SCROLL_TO_END, 61 | "setScrollEnabled", 62 | COMMAND_SET_SCROLL_ENABLED); 63 | } 64 | 65 | public static void receiveCommand( 66 | ScrollCommandHandler viewManager, 67 | T scrollView, 68 | int commandType, 69 | @Nullable ReadableArray args) { 70 | Assertions.assertNotNull(viewManager); 71 | Assertions.assertNotNull(scrollView); 72 | Assertions.assertNotNull(args); 73 | switch (commandType) { 74 | case COMMAND_SCROLL_TO: { 75 | int destX = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(0))); 76 | int destY = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(1))); 77 | boolean animated = args.getBoolean(2); 78 | viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY, animated)); 79 | return; 80 | } 81 | case COMMAND_SCROLL_TO_END: { 82 | boolean animated = args.getBoolean(0); 83 | viewManager.scrollToEnd(scrollView, new ScrollToEndCommandData(animated)); 84 | return; 85 | } 86 | case COMMAND_SET_SCROLL_ENABLED: { 87 | boolean isEnable = args.getBoolean(0); 88 | viewManager.setRNScrollEnabled(scrollView, isEnable); 89 | return; 90 | } 91 | default: 92 | throw new IllegalArgumentException(String.format( 93 | "Unsupported command %d received by %s.", 94 | commandType, 95 | viewManager.getClass().getSimpleName())); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.54/RNScrollViewCommandHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.taumu.rnscrollview; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | import java.util.Map; 15 | 16 | import com.facebook.react.bridge.ReadableArray; 17 | import com.facebook.react.uimanager.PixelUtil; 18 | import com.facebook.infer.annotation.Assertions; 19 | import com.facebook.react.common.MapBuilder; 20 | 21 | 22 | public class RNScrollViewCommandHelper { 23 | 24 | public static final int COMMAND_SCROLL_TO = 1; 25 | public static final int COMMAND_SCROLL_TO_END = 2; 26 | public static final int COMMAND_FLASH_SCROLL_INDICATORS = 3; 27 | public static final int COMMAND_SET_SCROLL_ENABLED = 4; 28 | 29 | public interface ScrollCommandHandler { 30 | void scrollTo(T scrollView, ScrollToCommandData data); 31 | void scrollToEnd(T scrollView, ScrollToEndCommandData data); 32 | void flashScrollIndicators(T scrollView); 33 | void setRNScrollEnabled(T scrollView, boolean data); 34 | } 35 | 36 | public static class ScrollToCommandData { 37 | 38 | public final int mDestX, mDestY; 39 | public final boolean mAnimated; 40 | 41 | ScrollToCommandData(int destX, int destY, boolean animated) { 42 | mDestX = destX; 43 | mDestY = destY; 44 | mAnimated = animated; 45 | } 46 | } 47 | 48 | public static class ScrollToEndCommandData { 49 | 50 | public final boolean mAnimated; 51 | 52 | ScrollToEndCommandData(boolean animated) { 53 | mAnimated = animated; 54 | } 55 | } 56 | 57 | public static Map getCommandsMap() { 58 | return MapBuilder.of( 59 | "scrollTo", 60 | COMMAND_SCROLL_TO, 61 | "scrollToEnd", 62 | COMMAND_SCROLL_TO_END, 63 | "flashScrollIndicators", 64 | COMMAND_FLASH_SCROLL_INDICATORS, 65 | "setScrollEnabled", 66 | COMMAND_SET_SCROLL_ENABLED); 67 | } 68 | 69 | public static void receiveCommand( 70 | ScrollCommandHandler viewManager, 71 | T scrollView, 72 | int commandType, 73 | @Nullable ReadableArray args) { 74 | Assertions.assertNotNull(viewManager); 75 | Assertions.assertNotNull(scrollView); 76 | Assertions.assertNotNull(args); 77 | switch (commandType) { 78 | case COMMAND_SCROLL_TO: { 79 | int destX = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(0))); 80 | int destY = Math.round(PixelUtil.toPixelFromDIP(args.getDouble(1))); 81 | boolean animated = args.getBoolean(2); 82 | viewManager.scrollTo(scrollView, new ScrollToCommandData(destX, destY, animated)); 83 | return; 84 | } 85 | case COMMAND_SCROLL_TO_END: { 86 | boolean animated = args.getBoolean(0); 87 | viewManager.scrollToEnd(scrollView, new ScrollToEndCommandData(animated)); 88 | return; 89 | } 90 | case COMMAND_FLASH_SCROLL_INDICATORS: 91 | viewManager.flashScrollIndicators(scrollView); 92 | return; 93 | case COMMAND_SET_SCROLL_ENABLED: { 94 | boolean isEnable = args.getBoolean(0); 95 | viewManager.setRNScrollEnabled(scrollView, isEnable); 96 | return; 97 | } 98 | default: 99 | throw new IllegalArgumentException(String.format( 100 | "Unsupported command %d received by %s.", 101 | commandType, 102 | viewManager.getClass().getSimpleName())); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/view-paged/view-paged.web.tsx: -------------------------------------------------------------------------------- 1 | import { get } from '../utils' 2 | import { ViewPagedAbstract } from '../abstract-class' 3 | 4 | export default abstract class ViewPaged extends ViewPagedAbstract { 5 | startX: number 6 | 7 | startY: number 8 | 9 | isScroll: boolean 10 | 11 | isTouch: boolean 12 | 13 | isMove: boolean 14 | 15 | endX: number 16 | 17 | endY: number 18 | 19 | _animatedDivRef: any 20 | 21 | constructor(props) { 22 | super(props) 23 | 24 | const { locked } = props 25 | 26 | this._AnimatedViewProps = {} 27 | if (!locked) { 28 | this._AnimatedViewProps = { 29 | onTouchStart: this._onTouchStart, 30 | ref: this._setAnimatedDivRef, // 代替move事件绑定 31 | // onTouchMove: this._onTouchMove, 32 | onTouchEnd: this._onTouchEnd 33 | } 34 | } 35 | } 36 | 37 | _getStyles() { 38 | const { 39 | props: { vertical }, 40 | state: { pos, isReady } 41 | } = this 42 | if (!isReady) return {} 43 | const basis = this.childrenSize * 100 44 | const key = `translate${vertical ? 'Y' : 'X'}` 45 | 46 | return { 47 | AnimatedStyle: { 48 | transform: [{ [key]: pos }], 49 | flex: `1 0 ${basis}%` 50 | } 51 | } 52 | } 53 | 54 | _onTouchStart = (e) => { 55 | e.stopPropagation() 56 | const targetTouche = get(e, 'targetTouches.0') || {} 57 | const { clientX, clientY } = targetTouche 58 | this._TouchStartEvent() 59 | 60 | this.startX = clientX 61 | this.startY = clientY 62 | // 是否为反向滚动 63 | this.isScroll = false 64 | // 是否达成触摸滑动操作,此类变量可用于web端区分点击事件 65 | this.isTouch = false 66 | // 是否判断过移动方向,只判断一次,判断过后不再判断 67 | this.isMove = false 68 | } 69 | 70 | _getDistance(targetTouche) { 71 | const { vertical } = this.props 72 | const suffix = vertical ? 'Y' : 'X' 73 | return targetTouche[`client${suffix}`] - this[`start${suffix}`] 74 | } 75 | 76 | _onTouchMove = (e) => { 77 | const targetTouche = get(e, 'targetTouches.0') || {} 78 | const { clientX, clientY } = targetTouche 79 | const { startX, startY } = this 80 | if (!this.isMove) { 81 | this.isMove = true 82 | // 是否达成触摸滑动操作 83 | if (clientX !== startX || clientY !== startY) { 84 | this.isTouch = true 85 | } 86 | // 判断滚动方向是否正确 87 | const horDistance = Math.abs(clientX - startX) 88 | const verDistance = Math.abs(clientY - startY) 89 | const { vertical } = this.props 90 | if (vertical ? verDistance <= horDistance : horDistance <= verDistance) { 91 | this.isScroll = true 92 | } 93 | } 94 | 95 | if (!this.isScroll) { 96 | e.stopPropagation() 97 | this._TouchMoveEvent(targetTouche) 98 | } 99 | // 判断默认行为是否可以被禁用 100 | if (e.cancelable) { 101 | // 判断默认行为是否已经被禁用 102 | if (!e.defaultPrevented) { 103 | e.preventDefault() 104 | } 105 | } 106 | } 107 | 108 | _onTouchEnd = (e) => { 109 | const changedTouche = get(e, 'changedTouches.0') || {} 110 | const { clientX, clientY } = changedTouche 111 | this.endX = clientX 112 | this.endY = clientY 113 | // 触发Move事件才能去判断是否跳转 114 | if (!this.isScroll && this.isMove) { 115 | this._TouchEndEvent(changedTouche) 116 | } 117 | } 118 | 119 | _setAnimatedDivRef = (ref) => { 120 | if (ref && !this._animatedDivRef) { 121 | this._animatedDivRef = ref 122 | // ReactDOM.findDOMNode(this._animatedDivRef) 123 | const divDom = get(ref, 'refs.node') 124 | // safari阻止拖动回弹,通过dom绑定事件 125 | divDom.addEventListener('touchmove', this._onTouchMove, false) 126 | } 127 | } 128 | 129 | _onLayout = (dom) => { 130 | if (dom) { 131 | const { offsetWidth, offsetHeight } = dom || {} 132 | this._runMeasurements(offsetWidth, offsetHeight) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/scroll-paged/scroll-paged.web.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getMergeObject } from '../utils' 3 | import { ScrollPagedAbstract, DirectionValues } from '../abstract-class' 4 | 5 | export default abstract class ScrollPaged extends ScrollPagedAbstract { 6 | private _startBorderDirection: DirectionValues 7 | 8 | private _isTouch: boolean 9 | 10 | _onTouchStart = (e) => { 11 | const { targetTouches } = e 12 | const { clientX, clientY } = targetTouches[0] || {} 13 | 14 | // 是否达成触摸滑动操作,此类变量可用于web端区分点击事件 15 | // 所有children共享类变量,从当前组件获取 16 | this._isTouch = false 17 | this._TouchStartEvent(clientX, clientY) 18 | 19 | // web端在touchStart中设置一次边界值,这样做的好处是 20 | // 1 touchStart事件只触发一次,保证下次move事件必定取到最为准确的边界信息 21 | // 2 虽然touchEnd也只触发一次,但touchEnd的信息并不准确,在touchEnd后页面会滑行一段距离,此时又不得不监听onScroll事件 22 | // 1 关于滑行在safari上的处理不一致,在回弹时触发翻页时会在翻页onChange事件后继续触发onScroll事件 23 | // 2 这会导致计算的borderDirection值又会改变,状态又变为上一页的结束状态,且onScroll事件触发的次数也很多,无法监听结束状态 24 | // 3 所以放在touchStart事件中做处理最好,这里我受定势思维影响采用和Rn一样的处理方法,加入了onTouchEnd和onScroll事件来处理问题,其实根本原因在于RN的touchStart事件中取不到滚动元素的高度和滚动高度这些信息,所以才没在touchStart事件中处理 25 | this._setBorderValue(e) 26 | // 记录开始的方向,用来在move事件中检验方向,只有方向一致才能发生翻页 27 | this._startBorderDirection = this.borderDirection 28 | } 29 | 30 | _setBorderValue = (e) => { 31 | const { 32 | currentTarget: { 33 | scrollHeight, 34 | scrollWidth, 35 | scrollTop, 36 | scrollLeft, 37 | clientHeight, 38 | clientWidth 39 | } 40 | } = e 41 | const { vertical } = this.props 42 | 43 | const startValue = vertical ? scrollTop : scrollLeft 44 | const endValue = vertical ? clientHeight : clientWidth 45 | const maxValue = vertical ? scrollHeight : scrollWidth 46 | 47 | this.setBorderValue(startValue, endValue, maxValue) 48 | } 49 | 50 | // _onTouchEnd = (e) => { 51 | // if(this.borderDirection) { 52 | // if (e.cancelable) { 53 | // // 判断默认行为是否已经被禁用 54 | // if (!e.defaultPrevented) { 55 | // e.preventDefault() 56 | // } 57 | // } 58 | // } 59 | // if (this.isTouchMove) { 60 | // this._scrollEndCommon(e) 61 | // } 62 | // } 63 | 64 | // _onScroll = (e) => { 65 | // this._scrollEndCommon(e) 66 | // } 67 | 68 | _onTouchMove = (e) => { 69 | const { targetTouches } = e 70 | const { clientX, clientY } = targetTouches[0] || {} 71 | const { 72 | currentTarget: { scrollHeight, scrollWidth, clientHeight, clientWidth } 73 | } = e 74 | const { vertical } = this.props 75 | // 是否达成触摸滑动操作 76 | if (!this._isTouch) { 77 | const { startX, startY } = this 78 | if (clientX !== startX || clientY !== startY) { 79 | this._isTouch = true 80 | } 81 | } 82 | 83 | const sizeValue = vertical ? scrollHeight : scrollWidth 84 | const layoutValue = vertical ? clientHeight : clientWidth 85 | this._TouchMoveEvent(clientX, clientY, sizeValue, layoutValue) 86 | 87 | // 边界不一致也停止冒泡 88 | if ( 89 | !this.isResponder || 90 | this.borderDirection !== this._startBorderDirection 91 | ) { 92 | e.stopPropagation() 93 | // 到达边界时阻止默认事件 94 | if (this.isResponder && this.borderDirection) { 95 | if (e.cancelable) { 96 | // 判断默认行为是否已经被禁用 97 | if (!e.defaultPrevented) { 98 | e.preventDefault() 99 | } 100 | } 101 | } 102 | } else if (e.cancelable) { 103 | // 判断默认行为是否已经被禁用 104 | if (!e.defaultPrevented) { 105 | e.preventDefault() 106 | } 107 | } 108 | } 109 | 110 | _webSetScrollViewRef = (ref) => { 111 | this._setScrollViewRef(ref) 112 | if (ref) { 113 | // safari阻止拖动回弹,通过dom绑定事件 114 | ref.addEventListener('touchmove', this._onTouchMove, false) 115 | } 116 | } 117 | 118 | // 子元素调用一定要传入index值来索引对应数据,且最好执行懒加载 119 | ScrollViewMonitor = ({ children, webProps = {} }) => { 120 | const { vertical } = this.props 121 | const mergeProps = getMergeObject( 122 | { 123 | onTouchStart: this._onTouchStart, 124 | // onTouchMove: this._onTouchMove, 125 | // onTouchEnd: this._onTouchEnd, 126 | // onScroll: this._onScroll, 127 | style: { 128 | flex: 1, 129 | overflowX: vertical ? 'hidden' : 'scroll', 130 | overflowY: !vertical ? 'hidden' : 'scroll', 131 | position: 'relative', 132 | WebkitOverflowScrolling: 'touch' 133 | } 134 | }, 135 | webProps 136 | ) 137 | 138 | return ( 139 |

140 | {children} 141 |
142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/scroll-paged/scroll-paged.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { isAndroid, isEmpty, getMergeObject } from '../utils' 3 | import { AgentScrollView } from '../components' 4 | import { ScrollPagedAbstract } from '../abstract-class' 5 | 6 | export default abstract class ScrollPaged extends ScrollPagedAbstract { 7 | _scrollViewRef: AgentScrollView 8 | 9 | constructor(props) { 10 | super(props) 11 | 12 | this._viewPagedProps = { 13 | onStartShouldSetPanResponder: this._startResponder, 14 | onMoveShouldSetPanResponder: this._moveResponder, 15 | onStartShouldSetPanResponderCapture: this._startResponderCapture, 16 | onMoveShouldSetPanResponderCapture: this._moveResponderCapture, 17 | onPanResponderTerminationRequest: this._onPanResponderTerminationRequest 18 | // onShouldBlockNativeResponder: this._onShouldBlockNativeResponder, 19 | // onPanResponderTerminate: this._onPanResponderTerminate, 20 | } 21 | } 22 | 23 | // 暂未观测出设置的先后顺序影响 24 | setResponder(flag) { 25 | if (isAndroid) { 26 | if (this._scrollViewRef) { 27 | this._scrollViewRef.setScrollEnabled(!flag) 28 | // this._scrollViewRef.setNativeProps({ 29 | // scrollEnabled: !flag, 30 | // }) 31 | } 32 | } 33 | } 34 | 35 | _onContentSizeChange = (oldSize, newSize) => { 36 | // 修复高度变化后边界已判断操作,只有第一页需要判断 37 | if (!isEmpty(oldSize)) { 38 | const { 39 | isResponder, 40 | props: { vertical } 41 | } = this 42 | const newValue = vertical ? newSize.height : newSize.width 43 | const oldValue = vertical ? oldSize.height : oldSize.width 44 | 45 | if (isResponder && newValue > oldValue) { 46 | this.isBorder = false 47 | this.borderDirection = 'none' 48 | this.setResponder(false) 49 | } 50 | } 51 | } 52 | 53 | _onTouchStart = ({ nativeEvent }, { scrollViewRef }) => { 54 | const { pageX, pageY } = nativeEvent 55 | this._scrollViewRef = scrollViewRef 56 | 57 | this._TouchStartEvent(pageX, pageY) 58 | } 59 | 60 | // _onTouchEnd = () => { 61 | // if (isAndroid && this.androidMove) { 62 | // const _scrollViewRef = this.getScrollViewConfig('scrollViewRef') 63 | // this._startTime = Date.now() 64 | // this.onUpdate(this._fromValue, (y) => { 65 | // console.log(y) 66 | // _scrollViewRef.scrollTo({ x: 0, y, animated: false }) 67 | // }) 68 | 69 | // this.androidMove = false 70 | // } 71 | // } 72 | 73 | _onScrollEndDrag = (event) => { 74 | // if (isAndroid && this.androidMove) { 75 | // this.androidMove = false 76 | // const _scrollViewRef = this.getScrollViewConfig('scrollViewRef') 77 | 78 | // this._startTime = Date.now() 79 | // this._onUpdate(this._fromValue, (y) => { 80 | // _scrollViewRef.scrollTo({ x: 0, y, animated: false }) 81 | // }) 82 | // } 83 | 84 | this._onMomentumScrollEnd(event) 85 | } 86 | 87 | _onMomentumScrollEnd = ({ nativeEvent }) => { 88 | if (this.isTouchMove) { 89 | const { vertical } = this.props 90 | const { 91 | contentOffset: { y, x }, 92 | contentSize: { height: maxHeight, width: maxWidth }, 93 | layoutMeasurement: { height, width } 94 | } = nativeEvent 95 | const startValue = vertical ? y : x 96 | const endValue = vertical ? height : width 97 | const maxValue = vertical ? maxHeight : maxWidth 98 | 99 | this.setBorderValue(startValue, endValue, maxValue) 100 | } 101 | } 102 | 103 | _onTouchMove = ({ nativeEvent }, { scrollViewSize, scrollViewLayout }) => { 104 | const { pageX, pageY } = nativeEvent 105 | const { vertical } = this.props 106 | 107 | const sizeValue = vertical ? scrollViewSize.height : scrollViewSize.width 108 | const layoutValue = vertical 109 | ? scrollViewLayout.height 110 | : scrollViewLayout.width 111 | this._TouchMoveEvent(pageX, pageY, sizeValue, layoutValue) 112 | } 113 | 114 | // 子元素调用一定要传入index值来索引对应数据,且最好执行懒加载 115 | ScrollViewMonitor = ({ children, nativeProps = {} }) => { 116 | const { vertical } = this.props 117 | const mergeProps = getMergeObject( 118 | { 119 | onContentSizeChange: this._onContentSizeChange, 120 | onMomentumScrollEnd: this._onMomentumScrollEnd, 121 | onScrollEndDrag: this._onScrollEndDrag, 122 | onTouchStart: this._onTouchStart, 123 | onTouchMove: this._onTouchMove, 124 | // onTouchEnd: this._onTouchEnd, 125 | showsVerticalScrollIndicator: false, 126 | bounces: false, 127 | style: { flex: 1 } 128 | }, 129 | nativeProps 130 | ) 131 | 132 | return ( 133 | 138 | {children} 139 | 140 | ) 141 | } 142 | 143 | _startResponder = () => { 144 | return false 145 | } 146 | 147 | _moveResponder = () => { 148 | return this.isResponder 149 | } 150 | 151 | _startResponderCapture = () => { 152 | return false 153 | } 154 | 155 | _moveResponderCapture = () => { 156 | return this.isResponder 157 | } 158 | 159 | _onPanResponderTerminationRequest = () => { 160 | return !this.isResponder 161 | } 162 | 163 | // _onShouldBlockNativeResponder = () => { 164 | // return false 165 | // } 166 | 167 | // _onPanResponderTerminate = () => { 168 | // if (this.isResponder) { 169 | // this.setResponder(false) 170 | 171 | // this.isTerminate = true 172 | // } else { 173 | // this.isTerminate = false 174 | // } 175 | // } 176 | } 177 | -------------------------------------------------------------------------------- /src/view-paged/view-paged.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PanResponder, Animated } from 'react-native' 3 | import { ViewPagedAbstract } from '../abstract-class' 4 | import { panResponderKey } from '../prop-types' 5 | 6 | export default abstract class ViewPaged extends ViewPagedAbstract { 7 | private _scrollViewRef: typeof Animated.ScrollView 8 | 9 | constructor(props) { 10 | super(props) 11 | const { locked, infinite, vertical, useScrollView } = props 12 | 13 | let panResponderValue = panResponderKey.reduce( 14 | (values, key) => ({ [key]: props[key], ...values }), 15 | {} 16 | ) 17 | if (!locked) { 18 | panResponderValue = { 19 | ...panResponderValue, 20 | onPanResponderGrant: this._onPanResponderGrant, 21 | onPanResponderMove: this._onPanResponderMove, 22 | onPanResponderRelease: this._onPanResponderRelease 23 | } 24 | } 25 | 26 | this._isScrollView = !vertical && !infinite && useScrollView 27 | this._panResponder = PanResponder.create(panResponderValue) 28 | this._AnimatedViewProps = this._panResponder.panHandlers 29 | } 30 | 31 | _getStyles() { 32 | const { 33 | props: { vertical }, 34 | state: { pos } 35 | } = this 36 | const key = vertical ? 'top' : 'left' 37 | 38 | return { 39 | AnimatedStyle: { 40 | [key]: pos 41 | } 42 | } 43 | } 44 | 45 | _getDistance(gestureState) { 46 | const { vertical } = this.props 47 | const suffix = vertical ? 'y' : 'x' 48 | return gestureState[`d${suffix}`] 49 | } 50 | 51 | _onPanResponderGrant = () => { 52 | this._TouchStartEvent() 53 | } 54 | 55 | _onPanResponderMove = (_evt, gestureState) => { 56 | this._TouchMoveEvent(gestureState) 57 | } 58 | 59 | _onPanResponderRelease = (_evt, gestureState) => { 60 | this._TouchEndEvent(gestureState) 61 | } 62 | 63 | _onLayout = ({ nativeEvent }) => { 64 | const { width, height } = nativeEvent.layout || {} 65 | this._runMeasurements(width, height) 66 | } 67 | 68 | _scrollToPage = ( 69 | posPage = this._posPage, 70 | hasAnimation = this.props.hasAnimation 71 | ) => { 72 | const { width } = this.state 73 | this._posPage = posPage 74 | 75 | const offset = this._posPage * width 76 | if (this._scrollViewRef) { 77 | const animated = hasAnimation 78 | this._scrollViewRef.getNode().scrollTo({ x: offset, y: 0, animated }) 79 | } 80 | 81 | this._onChange() 82 | } 83 | 84 | // _onScrollViewTouchStart = () => { 85 | // this._referX = null 86 | // this._startDirection = null 87 | // } 88 | 89 | // _onScrollViewTouchEnd = () => { 90 | // this._referX = null 91 | // this._isTouchEnd = true 92 | // // this._endDirection = null 93 | // } 94 | 95 | _onScroll = (event) => { 96 | // const { nativeEvent } = event 97 | const { onScroll } = this.props 98 | // const { x } = nativeEvent.contentOffset 99 | 100 | // // 优化体验,提前预知需要加载的下一页,避免监听onMomentumScrollEnd需等待滚动结束后才开始加载 101 | // if (!this._referX) { 102 | // this._referX = x 103 | // } else if (!this._startDirection) { 104 | // this._startDirection = x > this._referX ? 'right' : 'left' 105 | // } else if (this._isTouchEnd) { 106 | // const _endDirection = x > this._referX ? 'right' : 'left' 107 | // if (this._startDirection === _endDirection) { 108 | // let newPosPage = this._posPage 109 | // if (_endDirection === 'right') { 110 | // newPosPage = this._posPage + 1 111 | // } else { 112 | // newPosPage = this._posPage - 1 113 | // } 114 | // // 处理ios两侧回弹计算错误,用scrollView不会处理无限滚动 115 | // if (newPosPage >= 0 && newPosPage < this._childrenSize) { 116 | // this._posPage = newPosPage 117 | // this._onChange() 118 | // } 119 | // } 120 | 121 | // this._isTouchEnd = false 122 | // } 123 | 124 | onScroll && onScroll(event) 125 | } 126 | 127 | _onMomentumScrollHandle = ({ nativeEvent }) => { 128 | const { width } = this.state 129 | const offsetX = nativeEvent.contentOffset.x 130 | this._posPage = Math.round(offsetX / width) 131 | 132 | this._onChange() 133 | } 134 | 135 | _setScrollViewRef = (ref) => { 136 | this._scrollViewRef = ref 137 | } 138 | 139 | _renderContent() { 140 | if (this._isScrollView) { 141 | const { locked, scrollViewProps } = this.props 142 | const { width, pos } = this.state 143 | 144 | return ( 145 | 169 | {this._renderPage()} 170 | 171 | ) 172 | } 173 | 174 | return null 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/scroll-paged/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { accAdd } from '../utils' 4 | import ViewPaged from '../view-paged' 5 | import ScrollPagedPlatform from './scroll-paged' 6 | import { propTypes, defaultProps } from '../prop-types' 7 | 8 | export default class ScrollPaged extends ScrollPagedPlatform { 9 | static propTypes = propTypes 10 | 11 | static defaultProps = defaultProps 12 | 13 | static childContextTypes = { 14 | ScrollView: PropTypes.func 15 | } 16 | 17 | viewPagedRef: ViewPaged 18 | 19 | constructor(props) { 20 | super(props) 21 | 22 | this.isBorder = true 23 | this.borderDirection = 'isStart' 24 | // 默认为true,为了处理没有使用context的滚动组件child 25 | this.isResponder = true 26 | this.isTouchMove = false 27 | this.hasScrollViewPages = [] 28 | } 29 | 30 | onChange = (index, oldIndex) => { 31 | const { onChange } = this.props 32 | // 肯定处于边界位置,多此一举设置 33 | this.isBorder = true 34 | // 判断只有下次move事件后才能触发end事件 35 | this.isTouchMove = false 36 | // 描述下一页的边界方向 37 | this.borderDirection = index > oldIndex ? 'isStart' : 'isEnd' 38 | 39 | // 这一步设置false很重要,可以避免页间快速切换造成的scrollView位置错移 40 | // 通过打印手势和scrollView的事件触发的先后顺序,发现这和设置此值无关 41 | // 但如果此值为true在下次Touchstart中会被设置为false,之后move中如果是翻页又会被设置为true 42 | // 造成scrollView的滚动状态切换了3次,如果此值为false,scrollView的滚动状态只会切换一次,推测可能是这里出了问题 43 | // 是否能作手势操作应完全交给scrollView的Touchmove事件去处理 44 | const flag = this.hasScrollViewPages.includes(+index) 45 | this.setResponder(!flag) 46 | 47 | onChange && onChange(index, oldIndex) 48 | } 49 | 50 | setResponder(flag) { 51 | // RN android需要单独处理 52 | if (super.setResponder) { 53 | super.setResponder(flag) 54 | } 55 | this.isResponder = flag 56 | } 57 | 58 | getChildContext() { 59 | return { 60 | ScrollView: this.ScrollViewMonitor 61 | } 62 | } 63 | 64 | setViewPagedRef = (ref) => { 65 | if (ref) { 66 | this.viewPagedRef = ref 67 | } 68 | } 69 | 70 | _setScrollViewRef = (ref) => { 71 | if (ref) { 72 | // 初次渲染重置状态并保存页数 73 | this.setResponder(false) 74 | if (this.viewPagedRef) { 75 | const { currentPage } = this.viewPagedRef 76 | this.hasScrollViewPages.push(+currentPage) 77 | } 78 | } 79 | } 80 | 81 | getViewPagedInstance() { 82 | const { withRef } = this.props 83 | if (!withRef) { 84 | console.warn( 85 | 'To access the viewPage instance, you need to specify withRef=true in the props' 86 | ) 87 | } 88 | return this.viewPagedRef 89 | } 90 | 91 | setBorderValue(startValue, endValue, maxValue) { 92 | const isStart = parseFloat(startValue) <= 0 93 | const isEnd = 94 | parseFloat(accAdd(startValue, endValue).toFixed(2)) >= 95 | parseFloat(maxValue.toFixed(2)) 96 | this.borderDirection = isStart ? 'isStart' : isEnd ? 'isEnd' : 'none' 97 | this.isBorder = this.triggerJudge(isStart, isEnd) 98 | } 99 | 100 | triggerJudge(isStart, isEnd) { 101 | const { infinite } = this.props 102 | let expression = this.viewPagedRef.currentPage 103 | if (infinite) expression = null 104 | 105 | switch (expression) { 106 | case 0: 107 | return isEnd && this.borderDirection === 'isEnd' 108 | case this.childrenSize - 1: 109 | return isStart && this.borderDirection === 'isStart' 110 | default: 111 | return ( 112 | (isStart && this.borderDirection === 'isStart') || 113 | (isEnd && this.borderDirection === 'isEnd') 114 | ) 115 | } 116 | } 117 | 118 | checkMove(x, y) { 119 | const { 120 | startY, 121 | startX, 122 | props: { vertical } 123 | } = this 124 | const yValue = y - startY 125 | const xValue = x - startX 126 | if (vertical) { 127 | return Math.abs(yValue) > Math.abs(xValue) 128 | } 129 | return Math.abs(xValue) > Math.abs(yValue) 130 | } 131 | 132 | checkScrollContent(sizeValue, layoutValue) { 133 | return parseFloat(sizeValue.toFixed(2)) > parseFloat(layoutValue.toFixed(2)) 134 | } 135 | 136 | _TouchStartEvent(x, y) { 137 | this.startX = x 138 | this.startY = y 139 | this.setResponder(false) 140 | } 141 | 142 | _TouchMoveEvent(x, y, sizeValue, layoutValue) { 143 | if (!this.isTouchMove) this.isTouchMove = true 144 | 145 | if (this.checkMove(x, y)) { 146 | const { 147 | startY, 148 | startX, 149 | props: { vertical } 150 | } = this 151 | const hasScrollContent = this.checkScrollContent(sizeValue, layoutValue) 152 | 153 | if (hasScrollContent) { 154 | if (this.isBorder) { 155 | const distance = vertical ? y - startY : x - startX 156 | // 大于1.6为了防抖 157 | if (distance !== 0 && Math.abs(distance) > 1.6) { 158 | const direction = distance > 0 // 向上 159 | 160 | if (this.triggerJudge(direction, !direction)) { 161 | this.setResponder(true) 162 | } else { 163 | this.isBorder = false 164 | this.borderDirection = 'none' 165 | this.setResponder(false) 166 | } 167 | } 168 | } 169 | } else { 170 | this.setResponder(true) 171 | } 172 | } 173 | } 174 | 175 | render() { 176 | this.setChildrenAttr() 177 | 178 | return ( 179 | 188 | {this.childrenList} 189 | 190 | ) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => undefined 2 | 3 | export const getType = (object) => { 4 | return Object.prototype.toString.call(object).slice(8, -1) 5 | } 6 | 7 | export const isEmpty = (value) => { 8 | const type = getType(value) 9 | 10 | switch (type) { 11 | case 'Array': 12 | return !value.length 13 | case 'Object': 14 | return !Object.keys(value).length 15 | default: 16 | return !value 17 | } 18 | } 19 | 20 | const baseGetSet = (path) => { 21 | const type = getType(path) 22 | switch (type) { 23 | case 'Array': 24 | return path 25 | case 'String': 26 | case 'Number': 27 | return `${path}`.split('.') 28 | default: 29 | return [] 30 | } 31 | } 32 | 33 | export const get = (object, path, defaultValue?) => { 34 | const pathArray = baseGetSet(path) 35 | 36 | return ( 37 | pathArray.reduce((obj, key) => { 38 | return obj && obj[key] ? obj[key] : null 39 | }, object) || defaultValue 40 | ) 41 | } 42 | 43 | export const set = (object, path, value) => { 44 | const pathArray = baseGetSet(path) 45 | const len = pathArray.length 46 | 47 | return pathArray.reduce((obj, key, ind) => { 48 | if (obj && ind === len - 1) { 49 | obj[key] = value 50 | } 51 | 52 | return obj ? obj[key] : null 53 | }, object) 54 | } 55 | 56 | const keys = (value) => { 57 | const type = getType(value) 58 | 59 | switch (type) { 60 | case 'Array': 61 | case 'Object': 62 | return Object.keys(value) 63 | default: 64 | return [] 65 | } 66 | } 67 | 68 | export const size = (value) => { 69 | if (value) { 70 | const type = getType(value) 71 | switch (type) { 72 | case 'Array': 73 | return value.length 74 | case 'Object': 75 | return keys(value).length 76 | default: 77 | return value.length || 0 78 | } 79 | } 80 | return 0 81 | } 82 | 83 | const getFind = (value, handle) => { 84 | const len = size(value) 85 | for (let i = 0; i < len; i++) { 86 | const item = value[i] 87 | if (handle(item, i, value)) return item 88 | } 89 | return undefined 90 | } 91 | 92 | export const find = (value, handle) => { 93 | if (value) { 94 | const type = getType(value) 95 | switch (type) { 96 | case 'Array': 97 | return value.find ? value.find(handle) : getFind(value, handle) 98 | default: 99 | return undefined 100 | } 101 | } 102 | return undefined 103 | } 104 | 105 | export const findLast = (value, handle) => { 106 | const arr = value && value.reverse && value.slice().reverse() 107 | return find(arr, handle) 108 | } 109 | 110 | export const mergeWith = (originObject, mergeObject, handle) => { 111 | const originKeys = keys(originObject) 112 | const mergeKeys = keys(mergeObject) 113 | const reObject = {} 114 | originKeys.forEach((key) => { 115 | const mergeIndex = mergeKeys.indexOf(key) 116 | if (mergeIndex > -1) { 117 | reObject[key] = handle( 118 | originObject[key], 119 | mergeObject[key], 120 | key, 121 | originObject, 122 | mergeObject 123 | ) 124 | mergeKeys.splice(mergeIndex, 1) 125 | } else { 126 | reObject[key] = originObject[key] 127 | } 128 | }) 129 | mergeKeys.forEach((key) => { 130 | reObject[key] = mergeObject[key] 131 | }) 132 | 133 | return reObject 134 | } 135 | 136 | export const mergeStyle = (...styles) => 137 | styles.reduce((p, c) => ({ ...(p || {}), ...(c || {}) }), {}) 138 | 139 | export const getMergeObject = ( 140 | originObject: Record, 141 | mergeObject: Record 142 | ): Record => { 143 | return mergeWith(originObject, mergeObject, (originValue, mergeValue) => { 144 | const type = getType(originValue) 145 | 146 | switch (type) { 147 | case 'Array': 148 | return [...originValue, ...mergeValue] 149 | case 'Function': 150 | return (...params) => { 151 | originValue(...params) 152 | mergeValue(...params) 153 | } 154 | case 'Object': 155 | return { ...originValue, ...mergeValue } 156 | default: 157 | return mergeValue 158 | } 159 | }) 160 | } 161 | 162 | export const getDisplayName = (component) => { 163 | return component.displayName || component.name || 'Component' 164 | } 165 | 166 | export const getPrototypeOf = (object) => { 167 | return Object.getPrototypeOf 168 | ? Object.getPrototypeOf(object) 169 | : // eslint-disable-next-line no-proto 170 | object.__proto__ 171 | } 172 | 173 | export const copyStatic = (target, source, options) => { 174 | const { finallyInherit = Object, exclude = [] } = options || {} 175 | const inherited = getPrototypeOf(source) 176 | 177 | if (inherited && inherited !== getPrototypeOf(finallyInherit)) { 178 | copyStatic(target, inherited, options) 179 | } 180 | 181 | const propertys = Object.keys(source) 182 | propertys.forEach((key) => { 183 | if (exclude.indexOf(key) !== -1) { 184 | target[key] = source[key] 185 | } 186 | }) 187 | 188 | return target 189 | } 190 | 191 | export const accAdd = (arg1: number, arg2: number) => { 192 | let r1: number 193 | let r2: number 194 | try { 195 | r1 = arg1.toString().split('.')[1].length 196 | } catch (e) { 197 | r1 = 0 198 | } 199 | try { 200 | r2 = arg2.toString().split('.')[1].length 201 | } catch (e) { 202 | r2 = 0 203 | } 204 | const c = Math.abs(r1 - r2) 205 | 206 | // eslint-disable-next-line no-restricted-properties 207 | const m = Math.pow(10, Math.max(r1, r2)) 208 | // m = 10 ** Math.max(r1, r2) 209 | 210 | let value1: number 211 | let value2: number 212 | if (c > 0) { 213 | // eslint-disable-next-line no-restricted-properties 214 | const cm = Math.pow(10, c) 215 | if (r1 > r2) { 216 | value1 = Number(arg1.toString().replace('.', '')) 217 | value2 = Number(arg2.toString().replace('.', '')) * cm 218 | } else { 219 | value1 = Number(arg1.toString().replace('.', '')) * cm 220 | value2 = Number(arg2.toString().replace('.', '')) 221 | } 222 | } else { 223 | value1 = Number(arg1.toString().replace('.', '')) 224 | value2 = Number(arg2.toString().replace('.', '')) 225 | } 226 | return (value1 + value2) / m 227 | } 228 | -------------------------------------------------------------------------------- /docs/Dev_Record.md: -------------------------------------------------------------------------------- 1 | # 开发中遇到的问题及总结 2 | 3 | ## 如何实现 4 | 5 | 在目前这种实现之前我想了很多种实现方法,不论 ios 或是 android 都没能做出来满意的效果 6 | 起初思想一直局限在如何实现嵌套的 scrollView,其实这种方式的实现在原生上都不好处理,网上也有很多关于原生 scrollview 嵌套的问题 7 | 之后我将实现寄托在 View 版的 Animated 上,通过改变 top 值来切换不同页面,通过 PanResponder 来控制拖动,动画交由 RN 的 Animated 处理 8 | 目前的实现为外层``,里层的子元素使用`` 9 | 通过监听 ScrollView 是否滚动到底部或顶部来判断外层 View 是否拦截触摸事件滚动整页 10 | 11 | ## 关于提供的 RNScrollView 组件 12 | 13 | 为什么 android 要单独提供 RNScrollView 组件,ios 却不需要 14 | android 的手势系统并不完善有 bug 导致的,需要改动 ScrollView 的源码所以写了 RNScrollView 组件,不过相对 ios 的我没研究,只是对比预期和不同情况下的测试所得的结论,下面会列出测试情况 15 | 16 | ### android 和 ios 的手势响应差异 17 | 18 | - isResponder 代表是否可以进行触摸操作,指 RN 提供的手势响应系统 19 | - scrollEnabled 代表是否响应滚动,作为 ScrollView 的 props 在原生的 onInterceptTouchEvent 和 onTouchEvent 里会用来判断是否返回 false 来取消处理事件 20 | 21 | #### 差异情况 22 | 23 | | 序号 | isResponder | scrollEnabled | ios | android | 24 | | ---- | ----------- | ------------- | ------------------ | ----------------------------------------------- | 25 | | 1 | true | true | 只响应外层 view | 基本只响应 ScrollView,外层 view 会受到影响抖动 | 26 | | 2 | true | false | 只响应外层 view | 只响应外层 view | 27 | | 3 | flase | true | 只响应 scrollView | 只响应 ScrollView | 28 | | 4 | false | false | 都不响应,无法滚动 | 第一次都不响应,无法滚动,之后只响应 ScrollView | 29 | 30 | #### 用到的 api 31 | 32 | - onStartShouldSetPanResponder 初次触摸时是否愿意成为响应者 33 | - onMoveShouldSetPanResponder 后续未离开屏幕继续移动时是否愿意成为响应者 34 | - onStartShouldSetPanResponderCapture 是否在“捕获阶段”拦截初次触摸事件,阻止子组件成为响应者 35 | - onMoveShouldSetPanResponderCapture 是否在“捕获阶段”拦截后续移动事件,阻止子组件成为响应者 36 | - onPanResponderTerminationRequest 其他组件请求接替响应者时,当前的组件是否放权 37 | - onPanResponderTerminate 响应者权力已经交出时(可能为强制夺权)触发 38 | 39 | #### 数据取用 40 | 41 | isResponder 的值作为上述前 5 个 api 的返回值,即如果为 true 时,前 4 个 api 返回 true,第 5 个取反,表示拦截所有事件自己成为响应者,阻止子组件成为响应者并且不会放权 42 | scrollEnabled 的值只是作为 scrollView 的 props 传入 43 | 44 | ### 结论 45 | 46 | #### 第一种情况 android 并未和预期表现一致 47 | 48 | - ScrollView 还能继续滚动,表示外层的 View 并未和预期一样拦截事件成为响应者 49 | - 并且外层 View 还触发了 onPanResponderTerminate 表示交出权力,这说明 onPanResponderTerminationRequest 并未触发,通过打日志也说明确实未触发,所以权力会被交出 50 | - 在此还有一点值得注意,除了响应 ScrollView,外层 View 也会受到影响抖动,表明有外层 view 有成为响应者的时候,不过在刚成为响应者就被夺权了,通过打日志发现每次滚动都会触发 onPanResponderTerminate 事件,表明每次都在成为响应者后立即又被夺权,所以会有抖动 51 | 52 | #### 第二种和第三种情况 ios 和 android 表现一致,这里两个值取反,意义不大,仅作为参考对比实验 53 | 54 | #### 第三种情况值得注意下 55 | 56 | - 即使都为 false 也还是能滚动,给人一种 android 之后强加事件的感觉 57 | - 作为对比我删除所有组件只有 ScrollView 并将 scrollEnabled 设置为 false 结果是无法滚动了,无论触摸多少次 58 | - 两次的区别是外层的 view 是否有添加手势响应属性,不过虽然添加了手势响应属性但并未做任何处理,不应该表现不一致,但这是唯一的区别证明问题只能出在这里,添加了 onPanResponderTerminate 也未触发,表明外层的 view 并未有成为响应者的机会,这点符合预期 59 | 60 | ## 解决 android 下的触摸 bug 61 | 62 | 通过之前的分析其实如果熟悉 android 的事件响应是能有一些想法解决的,但开始我并不熟悉,于是在网上找解决方法,终于功夫不负有心人,我找到了和我[遇到相同问题的人](https://smallpath.me/post/react-native-bugfix),他指出是在 ReactScrollView.java 这个类中的拦截方法 onInterceptTouchEvent 中会取消掉 js 层的手势操作,这也就是为何第一种情况会触发 onPanResponderTerminate 的原因所在 63 | 64 | > 它会将滑动时的触摸操作停止, 转而在当前 View 的 TopView 中触发原生手势, 槽点又来了, 这个 TopView 和 React 的源码一样, 居然还是先到先得, 没有优先级的判断, 因此直接在 ListView 中触发了原生手势而导致 JS 层手势被忽略! 65 | 66 | ### 如何解决 67 | 68 | 知道了问题所在就好办了,提供的 RNScrollView 组件,我拷贝了核心类 ReactScrollView 并做了修改,其中需要引用到的类我继续引用 RN 提供的,一番修改作为独立的包后,顺利解决问题,之前也一直是这种处理方式 69 | 70 | ### 升级 RN 造成的问题 71 | 72 | 当我解决后就准备发布到 npm 上去,写 readme 时我需要给出 demo,于是我用 react-native init 命令快速生成了一个 RN 的 app,里面用我提供的包做了个简单的例子,但是在写好后运行时出问题了 73 | 我写包时用的 RN 版本是 0.47,但 demo 里生成的是 0.54,看了报错发现问题出在 RNScrollView 上,有些调用 RN 类的方法签名变了,因为之前我只是拷了核心类其他依赖类还是继续引用的 RN,但随着版本升级拷贝类中的代码并没有发生变化,而引用的类却跟随 RN 的升级变了,从而导致调用方法出错 74 | 75 | ### 解决方案 76 | 77 | 1. 升级拷贝的核心类,但这种做法又会造成其他版本的 RN 使用不了,要兼容所有的 RN 版本要写很多版本的包,不可取 78 | 2. 使用继承,继承核心类,重写 onInterceptTouchEvent 方法,这种方法应该最好,可以跟随当前的 RN 版本,但问题出在这个方法需要调用重写的父类方法,这重写就没有意义了,那我需要调用的是祖父的方法 79 | 80 | ### 浅尝辄止 81 | 82 | 最终我还是选择以继承的方式去解决问题,这让我想起学 java 时的一句话,要优先使用组合而不是继承,不过我觉得我的情况更适合继承,我需要继承类的改变来满足需求,最大化的复用代码,减少影响 83 | 84 | #### 调用祖父方法 85 | 86 | 开始我一直都陷入 java 如何调用祖父的方法,并做了很多尝试,终以失败告终,调用祖父的方法,这种实现想法就有问题,不改出现这样的类设计 87 | 88 | ##### 初次尝试 89 | 90 | 继承 ReactScrollView 类,重写 onInterceptTouchEvent 方法,想利用反射调用,取用祖父的 class,通过 getDeclaredMethod 获取重写的方法,invoke 中强转当前 this 为祖父类型,调用方法结果还是调用到了当前类的方法上,java 强转为父类后虽然不能调用子类的方法,但还是能调用子类重写的方法,看来在反射中也是这种机制,以失败告终 91 | 92 | ##### 再次尝试 93 | 94 | 我还是不相信 java 调用不到祖父的方法,于是再次谷歌找到,也就是深入理解 java 虚拟机上的这本书上的方法(虽然书中的第一个方法是错的),但是书中的方法需要用到 java.lang.invoke 这个类,这个类是 java8 提供的,然而 android 使用的 java7,升级会造成 minSdkVersion 版本提升,兼容不了低版本 android,还是以失败告终 95 | 96 | #### 修改方法或类 97 | 98 | 我又想如果可以修改 ReactScrollView 这个类中的方法不就行了,其实我也就是想注释方法中的一行代码而已,直接修改这个方法不就行了,以 js 的角度来说改相当于改原型上的方法是很容易的,然而我发现我错了,java 是门静态语言,相比 js 很不灵活,想在运行中改源码还是很难得 99 | 不过之后我找到了 javassist 这个库,看了 android 下的使用,需要在 gradle 里用 groovy 写插件,这个改动成本大,难度高,而且修改会造成原始类被改变,作为提供的包不能影响到使用者的代码,虽然可以生成一个类不在原始类上改动,但想想还是算了,实在无计可施后再考虑这个方案 100 | 101 | #### 使用组合 102 | 103 | 继承不行那就想办法用组合去解决吧,创建一个祖父类,由它去调方法,spring 里有提供了一个工具方法 BeanUtils.copyProperties(source, target),可以拷贝类,android 下可以找类似的实现,但问题是虽然两个类一样了,但操作的不是同一个实例,拷贝类里的方法只会改自身实例对象的值,其实我想到可以监听值的变化并用反射去另一个类的值,嗯,这样是不是太麻烦了 104 | 105 | #### 简单即真理 106 | 107 | 之后我想为什么不直接复制祖父的方法,给个其他名字,然后在重写的 onInterceptTouchEvent 方法里去调这个方法,相当于调用了祖父的方法曲线救国,再用反射解决复制的祖父方法里用到的私有属性,嗯,貌似是可以的,撸起袖子就是干,但在写好后一运行发现还是有问题的,很多私有方法用反射竟然取不到,断点调试后发现确实没有找到,但源码中明明看到有的,一番谷歌后发现,android 会把源码中的方法或类隐藏了,也是不建议去调用源码 108 | 109 | ### 回到起点 110 | 111 | 在纠结于上述解决方案时,我并未去深入了解真正的原因,但也在一次次尝试中渐渐找到了问题所在 112 | 还是需要了解下 android 的事件流机制,一番学习发现 android 的触摸事件流和 dom 的看似有点类似,捕获、目标、冒泡,但其实本质上不同,dom 的事件模型是广播和扩散,android 的是责任链模式,他们的表现形式类似 113 | 114 | android 通过调用 dispatchTouchEvent 来分发事件,在此期间会调用 onInterceptTouchEvent 来判断是否需要拦截,暂停一下,到这里才找到我们的主角,问题就出在 onInterceptTouchEvent 这个方法上,之前一直做的努力都是在重写这个方法,要弄明白这个方法的处理才是关键所在,并不能陷入如何去调用祖父类这样的实现上 115 | 116 | #### onInterceptTouchEvent 117 | 118 | 作为拦截事件的方法,dispatchTouchEvent 方法分发时会调用,且只关心此方法的返回值,返回 true 交由自身的 onTouchEvent 处理,返回 false 则传递给下级处理 119 | 120 | 简单了解后再看下 RN 中 ReactScrollView 类对此方法的重写实现,首先判断 scrollEnabled 这个 props 的值,如果为 false 则返回 false,然后调用父类被重写的方法,也就是 android 的原生组件类 ScrollView 中的 onInterceptTouchEvent,如果返回 false 则返回 false,如果为 true 则调用一些方法设置一些变量并返回 true 表示消拦截消费事件,这里贴下源码 121 | 122 | ```java 123 | public boolean onInterceptTouchEvent(MotionEvent ev) { 124 | if (!this.mScrollEnabled) { // 这个变量是组件props中scrollEnabled的值 125 | return false; 126 | } else if (super.onInterceptTouchEvent(ev)) { // 调用重写的父类方法 127 | NativeGestureUtil.notifyNativeGestureStarted(this, ev); // 调用dispatchCancelEvent取消js的手势控制 128 | ReactScrollViewHelper.emitScrollBeginDragEvent(this); 129 | this.mDragging = true; 130 | this.enableFpsListener(); 131 | return true; 132 | } else { 133 | return false; 134 | } 135 | } 136 | ``` 137 | 138 | 问题出在这个方法里的一行代码,`NativeGestureUtil.notifyNativeGestureStarted(this, ev);`,删除后和 ios 表现就一致了,之前做的努力其实就是为了不执行这行代码,现在删除不执行已经不用考虑了,而需要了解为何这行代码执行后会造成外层 View 手势响应被中止,交出控制权,一番追踪之后我们发现如同之前所说这个方法最后会调用到 dispatchCancelEvent 这个方法来取消 js 的手势控制,手势这里一旦被取消也做不了逆向处理,之有想办法不去执行他了 139 | 140 | ### 最终解决 141 | 142 | 如之前所说 onInterceptTouchEvent 这个方法的存在就是返回值给 dispatchTouchEvent 来决定是否拦截,所以某种意义上我们可以不用关心父类被重写的此方法,因为真正处理滚动的是 onTouchEvent 方法 143 | 所以如果我们完全控制这个方法的返回值根据 scrollEnabled,那么就可以不必调用父类的方法,从而直接继承重写此方法即可 144 | 此外还有一点需要注意的是外层 View 在滚动完整页后要设置 isResponder 为 false,从而使得下次触摸事件不会被拦截以保证能传递到 ScrollView 子组件,因为 android 外层组件一但处理了事件,之后的事件都不会再流入到子组件中,所以这里切换完页面后设置 false,保证下次非翻页操作依然可以滚动 145 | 至此问题终于解决了,再也不用维护多个版本,世界都清净了! 146 | 147 | ### 其他问题 148 | 149 | 重写的方法里需要用到父类的私有变量 mScrollEnabled,之前我都是在当前类再声明一个同名的变量去取用,也不用写反射了,虽然没有什么问题,后来发现 java 继承类中同名的变量会在实例上产生多个值,以类名.变量的形式作为命名空间,通过反射可以打印出名称,是拼接的出来的,最后子类上的变量才是实例访问的这个变量,这里会有什么问题的,问题就出在父类和子类操作的不是同一个变量,有多个继承,如果都有同名变量那么调用他们方法中所访问的这个同名变量都是各自的,所以还是不要出现同名变量的好,取不到老老实实用反射去获取 150 | 151 | ## 未了解知识点 152 | 153 | RN 手势处理的实现,具体实现在 PanResponder.js 和 ScrollResponder.js,需要花时间研究学习 154 | RN 的 js 手势和原生如何协调,android 所有触摸相关知识,ios 触摸事件流程 155 | 156 | ## 总结 157 | 158 | 从写这个库到现在,断断续续完善了很多,也终于解决了 android 下的问题,学到了很多方面的知识,对 React Native 更加热爱,之后也会继续学习做 RN 相关的包 159 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # react-scroll-paged-view 2 | 3 | [View README in English](./README.md) 4 | [如果你对我的开发过程感兴趣不妨读读,相信会有所收获](./docs/Dev_Record.md) 5 | 6 | **滚动视图,内滚动,整页滚动,嵌套滚动视图** 7 | 8 | ## 安装 9 | 10 | ``` 11 | npm install react-scroll-paged-view --save 12 | ``` 13 | 14 | ## 简介 15 | 16 | - 支持 RN 端,相应的 web 端组件也有 17 | - 整页滚动和页内滚动结合,类似京东等 app 的商品详情页上下页查看 18 | - iOS RN 代码完美支持,Android 则提供了原生包支持,基于 RN ScrollView 改动了部分代码得以支持 19 | - 目前开源的 RN 项目中并没有内滚动和页滚动结合的,基于需要写了这个组件 20 | - 此外还额外提供了核心功能模块 ViewPaged 可供使用 21 | - 提供子组件封装的 ScrollView 组件,可以选择使用 22 | - 所有分页为按需加载,不必担心初始会全部渲染 23 | - 无限分页也是懒处理,最小程度校准当前索引页,即使快速切换滑动也很流畅 24 | - RN 和 web 动画基于 animated 库,共用一套代码处理 25 | - 提供了 renderHeader 和 renderFooter 可做 tab 切换或轮播图等 26 | - web 版的两个组件都有提供类变量 isTouch 用于判断是否为触摸事件,可借此区分滚动触发的点击事件 27 | - 支持 ssr,2.1+版本移除初始测量尺寸所导致的组件重复创建和销毁,性能更好 28 | - 2.1.3+版本在为横向滚动且不无限滚动时使用 ScrollView 作为滚动容器,这样子视图可以使用 ScrollView 来纵向滚动 29 | 30 | ## 注意 31 | 32 | - ~~**兼容至"react-native": "~0.54.0"版本**~~ 33 | - ~~**react native0.47 版本的使用 0.1.\*版本**~~ 34 | - **已完美兼容以上 RN 的版本,直接安装最新的包即可** 35 | - **没有出现在内部 ScrollView 组件中的点击事件可以用 onPressIn 代替** 36 | - **infinite 和 autoPlay 只提供给 ViewPaged 组件,ScrollPagedView 会默认关闭此选项** 37 | 38 | ## Demo 39 | 40 | | IOS | Android | Web | 41 | | ---------------------- | ------------------------------ | ---------------------- | 42 | | ![IOS](./demo/ios.gif) | ![Android](./demo/android.gif) | ![Web](./demo/web.gif) | 43 | 44 | ### Other 45 | 46 | 你所能实现的取决于你所能想象的 47 | 48 | | Horizontal | Tab | Carousel | 49 | | ------------------------------------ | ---------------------- | -------------------------------- | 50 | | ![Horizontal](./demo/horizontal.gif) | ![Tab](./demo/tab.gif) | ![Carousel](./demo/carousel.gif) | 51 | 52 | ## 使用 53 | 54 | ### ScrollPagedView 55 | 56 | ScrollPagedView 组件基于 ViewPaged 组件封装了内滚动组件,通过 context 使用 57 | 58 | ```javascript 59 | import ScrollPagedView from 'react-scroll-paged-view' 60 | import InsideScrollView from './InsideScrollView' 61 | 62 | ... 63 | _onChange = (pageIndex) => { 64 | ... 65 | } 66 | 67 | render() { 68 | return ( 69 | 73 | 74 | 75 | 76 | 77 | ) 78 | } 79 | ... 80 | ``` 81 | 82 | #### Context ScrollView(InsideScrollView) 83 | 84 | ```javascript 85 | ... 86 | static contextTypes = { 87 | ScrollView: PropTypes.func, 88 | } 89 | 90 | render() { 91 | const ScrollView = this.context.ScrollView 92 | return ( 93 | 94 | ... 95 | 96 | ) 97 | } 98 | ... 99 | ``` 100 | 101 | ### ViewPaged 102 | 103 | ViewPaged 组件和 ScrollPagedView 组件用法一致,可以自由使用 infinite 和 autoPlay 104 | 105 | ```javascript 106 | import { ViewPaged } from 'react-scroll-paged-view' 107 | ``` 108 | 109 | ## Export module 110 | 111 | - default - ScrollPagedView 112 | - ViewPaged 113 | 114 | ## 属性 115 | 116 | ### ScrollPagedView 117 | 118 | ScrollPagedView 组件基于 ViewPaged 组件,可以根据需要传入 ViewPaged 的 props,参考下面 ViewPaged 组件的 props 119 | 120 | | Name | propType | default value | description | 121 | | ------- | -------- | ------------- | ------------------------------------------------------------- | 122 | | withRef | bool | false | 获取 ViewPaged 实例 ref,通过组件的 getViewPagedInstance 方法 | 123 | 124 | ### Context ScrollView 125 | 126 | | Name | propType | default value | description | 127 | | ------------------------ | -------- | ------------- | -------------------- | 128 | | nativeProps(native only) | object | {} | RN scrollView Props | 129 | | webProps(web only) | object | {} | Web scrollView Props | 130 | 131 | ### ViewPaged 132 | 133 | RN 和 web 有相同的 props,表现也一致 134 | 135 | #### Common Props 136 | 137 | | Name | propType | default value | description | 138 | | -------------- | ---------------- | ------------- | ------------------------------------------------------------------ | 139 | | style | object | {} | ViewPaged 样式 | 140 | | initialPage | number | 0 | 初始页索引 | 141 | | vertical | bool | true | 是否为垂直切换视图 | 142 | | onChange | function | () => {} | 切换分页回调,参数为 currentPage 和 prevPage | 143 | | duration | number | 400 | 动画持续时间(以毫秒为单位) | 144 | | infinite | bool | false | 是否为无限滚动视图 | 145 | | renderHeader | function/element | undefined | Header 组件,参数为 activeTab, goToPage, width, pos | 146 | | renderFooter | function/element | undefined | Footer 组件,参数为 activeTab, goToPage, width, pos | 147 | | renderPosition | string | top | Header/Footer 方向,有 4 个值,分别为'top','left','bottom','right' | 148 | | autoPlay | bool | false | 是否自动轮播 | 149 | | autoPlaySpeed | number | 2000 | 自动轮播间隔时间(以毫秒为单位) | 150 | | hasAnimation | bool | true | 点击切换时否有动画 | 151 | | locked | bool | false | 是否允许拖动切换 | 152 | | preRenderRange | number | 0 | 控制每次更新时 render 组件的范围 | 153 | | isMovingRender | bool | false | 触摸移动时预加载下一页 | 154 | 155 | #### RN Only Props 156 | 157 | | Name | propType | default value | description | 158 | | ----------------------------------- | -------- | ------------- | ---------------------------------- | 159 | | onStartShouldSetPanResponder | function | () => true | 参考 React Native 官网手势响应系统 | 160 | | onStartShouldSetPanResponderCapture | function | () => false | 参考 React Native 官网手势响应系统 | 161 | | onMoveShouldSetPanResponder | function | () => true | 参考 React Native 官网手势响应系统 | 162 | | onMoveShouldSetPanResponderCapture | function | () => false | 参考 React Native 官网手势响应系统 | 163 | | onPanResponderTerminationRequest | function | () => true | 参考 React Native 官网手势响应系统 | 164 | | onShouldBlockNativeResponder | function | () => true | 参考 React Native 官网手势响应系统 | 165 | | onPanResponderTerminate | function | () => {} | 参考 React Native 官网手势响应系统 | 166 | 167 | ## TODO 168 | 169 | - [x] 优化滚动区域索引,使用代理 ScrollView 完成 170 | - [x] Android 兼容 React Native 不同版本 171 | - [x] 支持 web 端组件 172 | - [x] 优化 web 端组件 173 | - [x] 优化 web 无限滚动 174 | - [x] 完善 web 端 ViewPaged 175 | - [x] 优化结构、代码,统一命名 176 | - [x] 统一兼容 React Native 不同版本 177 | - [x] 记录开发过程 178 | - [x] 完善 RN 端 ViewPaged 达到和 web 端表现一致 179 | - [x] 更多 props 配置 180 | 181 | ## Changelog 182 | 183 | - 0.1.\* 184 | - 1.0.\* 185 | - 1.1.\* 186 | - 1.2.\* 187 | - 1.3.\* 188 | - 1.5.\* 189 | - 1.6.\* 190 | 191 | ### 2.0.\* 192 | 193 | - 整体重构项目,针对 web 端重构提高代码复用 194 | - 增加了依赖包 animated,动画处理更为流畅,性能更好 195 | - 使用 hoc 最大程度复用了三端的公共代码,各个端仅保留自己平台的代码 196 | - 统一了 RN 端和 web 端的 props,并使其表现一致 197 | - 针对 ssr 服务端渲染也做了支持 198 | 199 | ### 2.1.\* 200 | 201 | - 针对 ssr 统一了 RN 和 web 的 render 方法 202 | - 移除初始测量组件尺寸时独立的一次 render 203 | - 避免子组件重复创建和销毁,性能更好 204 | 205 | ### 2.1.4+ 206 | 207 | - 移除了上传 npm 包里的.babelrc 等配置文件,react native 会使用包里的 babel 配置,没有安装这些配置依赖会报错 208 | 209 | ### 2.2.0+ 210 | 211 | - 优化代码结构,精确控制组件 render 次数,提高页面性能,并提供预加载和 render 范围的 props 212 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.54/RNScrollViewManager.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import java.util.Map; 6 | 7 | import android.graphics.Color; 8 | 9 | import com.facebook.react.bridge.ReadableArray; 10 | import com.facebook.react.common.MapBuilder; 11 | import com.facebook.react.module.annotations.ReactModule; 12 | import com.facebook.react.uimanager.PixelUtil; 13 | import com.facebook.react.uimanager.Spacing; 14 | import com.facebook.react.uimanager.ViewProps; 15 | import com.facebook.react.uimanager.annotations.ReactProp; 16 | import com.facebook.react.uimanager.ThemedReactContext; 17 | import com.facebook.react.uimanager.ViewGroupManager; 18 | import com.facebook.react.uimanager.ReactClippingViewGroupHelper; 19 | import com.facebook.react.uimanager.annotations.ReactPropGroup; 20 | import com.facebook.yoga.YogaConstants; 21 | 22 | import com.facebook.react.views.scroll.FpsListener; 23 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 24 | import com.facebook.react.views.scroll.ScrollEventType; 25 | 26 | 27 | @ReactModule(name = RNScrollViewManager.REACT_CLASS) 28 | public class RNScrollViewManager 29 | extends ViewGroupManager 30 | implements RNScrollViewCommandHelper.ScrollCommandHandler { 31 | 32 | protected static final String REACT_CLASS = "RNScrollView"; 33 | 34 | private static final int[] SPACING_TYPES = { 35 | Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM, 36 | }; 37 | 38 | private @Nullable FpsListener mFpsListener = null; 39 | 40 | public RNScrollViewManager() { 41 | this(null); 42 | } 43 | 44 | public RNScrollViewManager(@Nullable FpsListener fpsListener) { 45 | mFpsListener = fpsListener; 46 | } 47 | 48 | @Override 49 | public String getName() { 50 | return REACT_CLASS; 51 | } 52 | 53 | @Override 54 | public RNScrollView createViewInstance(ThemedReactContext context) { 55 | return new RNScrollView(context, mFpsListener); 56 | } 57 | 58 | @ReactProp(name = "scrollEnabled", defaultBoolean = true) 59 | public void setScrollEnabled(RNScrollView view, boolean value) { 60 | view.setScrollEnabled(value); 61 | } 62 | 63 | @ReactProp(name = "showsVerticalScrollIndicator") 64 | public void setShowsVerticalScrollIndicator(RNScrollView view, boolean value) { 65 | view.setVerticalScrollBarEnabled(value); 66 | } 67 | 68 | @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) 69 | public void setRemoveClippedSubviews(RNScrollView view, boolean removeClippedSubviews) { 70 | view.setRemoveClippedSubviews(removeClippedSubviews); 71 | } 72 | 73 | /** 74 | * Computing momentum events is potentially expensive since we post a runnable on the UI thread 75 | * to see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This 76 | * is handled automatically in js by checking if there is a listener on the momentum events. 77 | * 78 | * @param view 79 | * @param sendMomentumEvents 80 | */ 81 | @ReactProp(name = "sendMomentumEvents") 82 | public void setSendMomentumEvents(RNScrollView view, boolean sendMomentumEvents) { 83 | view.setSendMomentumEvents(sendMomentumEvents); 84 | } 85 | 86 | /** 87 | * Tag used for logging scroll performance on this scroll view. Will force momentum events to be 88 | * turned on (see setSendMomentumEvents). 89 | * 90 | * @param view 91 | * @param scrollPerfTag 92 | */ 93 | @ReactProp(name = "scrollPerfTag") 94 | public void setScrollPerfTag(RNScrollView view, String scrollPerfTag) { 95 | view.setScrollPerfTag(scrollPerfTag); 96 | } 97 | 98 | /** 99 | * When set, fills the rest of the scrollview with a color to avoid setting a background and 100 | * creating unnecessary overdraw. 101 | * @param view 102 | * @param color 103 | */ 104 | @ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color") 105 | public void setBottomFillColor(RNScrollView view, int color) { 106 | view.setEndFillColor(color); 107 | } 108 | 109 | /** 110 | * Controls overScroll behaviour 111 | */ 112 | @ReactProp(name = "overScrollMode") 113 | public void setOverScrollMode(RNScrollView view, String value) { 114 | view.setOverScrollMode(ReactScrollViewHelper.parseOverScrollMode(value)); 115 | } 116 | 117 | @Override 118 | public @Nullable Map getCommandsMap() { 119 | return RNScrollViewCommandHelper.getCommandsMap(); 120 | } 121 | 122 | @Override 123 | public void receiveCommand( 124 | RNScrollView scrollView, 125 | int commandId, 126 | @Nullable ReadableArray args) { 127 | RNScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); 128 | } 129 | 130 | @Override 131 | public void flashScrollIndicators(RNScrollView scrollView) { 132 | scrollView.flashScrollIndicators(); 133 | } 134 | 135 | @Override 136 | public void scrollTo( 137 | RNScrollView scrollView, 138 | RNScrollViewCommandHelper.ScrollToCommandData data) { 139 | if (data.mAnimated) { 140 | scrollView.smoothScrollTo(data.mDestX, data.mDestY); 141 | } else { 142 | scrollView.scrollTo(data.mDestX, data.mDestY); 143 | } 144 | } 145 | 146 | @Override 147 | public void setRNScrollEnabled(RNScrollView view, boolean value) { 148 | view.setScrollEnabled(value); 149 | } 150 | 151 | @ReactPropGroup(names = { 152 | ViewProps.BORDER_RADIUS, 153 | ViewProps.BORDER_TOP_LEFT_RADIUS, 154 | ViewProps.BORDER_TOP_RIGHT_RADIUS, 155 | ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, 156 | ViewProps.BORDER_BOTTOM_LEFT_RADIUS 157 | }, defaultFloat = YogaConstants.UNDEFINED) 158 | public void setBorderRadius(RNScrollView view, int index, float borderRadius) { 159 | if (!YogaConstants.isUndefined(borderRadius)) { 160 | borderRadius = PixelUtil.toPixelFromDIP(borderRadius); 161 | } 162 | 163 | if (index == 0) { 164 | view.setBorderRadius(borderRadius); 165 | } else { 166 | view.setBorderRadius(borderRadius, index - 1); 167 | } 168 | } 169 | 170 | @ReactProp(name = "borderStyle") 171 | public void setBorderStyle(RNScrollView view, @Nullable String borderStyle) { 172 | view.setBorderStyle(borderStyle); 173 | } 174 | 175 | @ReactPropGroup(names = { 176 | ViewProps.BORDER_WIDTH, 177 | ViewProps.BORDER_LEFT_WIDTH, 178 | ViewProps.BORDER_RIGHT_WIDTH, 179 | ViewProps.BORDER_TOP_WIDTH, 180 | ViewProps.BORDER_BOTTOM_WIDTH, 181 | }, defaultFloat = YogaConstants.UNDEFINED) 182 | public void setBorderWidth(RNScrollView view, int index, float width) { 183 | if (!YogaConstants.isUndefined(width)) { 184 | width = PixelUtil.toPixelFromDIP(width); 185 | } 186 | view.setBorderWidth(SPACING_TYPES[index], width); 187 | } 188 | 189 | @ReactPropGroup(names = { 190 | "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor" 191 | }, customType = "Color") 192 | public void setBorderColor(RNScrollView view, int index, Integer color) { 193 | float rgbComponent = 194 | color == null ? YogaConstants.UNDEFINED : (float) ((int)color & 0x00FFFFFF); 195 | float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int)color >>> 24); 196 | view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); 197 | } 198 | 199 | @Override 200 | public void scrollToEnd( 201 | RNScrollView scrollView, 202 | RNScrollViewCommandHelper.ScrollToEndCommandData data) { 203 | // ScrollView always has one child - the scrollable area 204 | int bottom = 205 | scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom(); 206 | if (data.mAnimated) { 207 | scrollView.smoothScrollTo(scrollView.getScrollX(), bottom); 208 | } else { 209 | scrollView.scrollTo(scrollView.getScrollX(), bottom); 210 | } 211 | } 212 | 213 | @Override 214 | public @Nullable Map getExportedCustomDirectEventTypeConstants() { 215 | return createExportedCustomDirectEventTypeConstants(); 216 | } 217 | 218 | public static Map createExportedCustomDirectEventTypeConstants() { 219 | return MapBuilder.builder() 220 | .put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll")) 221 | .put(ScrollEventType.BEGIN_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollBeginDrag")) 222 | .put(ScrollEventType.END_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollEndDrag")) 223 | .put(ScrollEventType.MOMENTUM_BEGIN.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollBegin")) 224 | .put(ScrollEventType.MOMENTUM_END.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollEnd")) 225 | .build(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.47/RNScrollViewManager.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import java.util.Map; 6 | 7 | import android.graphics.Color; 8 | import android.view.View; 9 | 10 | import com.facebook.react.bridge.ReadableArray; 11 | import com.facebook.react.common.MapBuilder; 12 | import com.facebook.react.module.annotations.ReactModule; 13 | import com.facebook.react.uimanager.PixelUtil; 14 | import com.facebook.react.uimanager.Spacing; 15 | import com.facebook.react.uimanager.ViewProps; 16 | import com.facebook.react.uimanager.annotations.ReactProp; 17 | import com.facebook.react.uimanager.ThemedReactContext; 18 | import com.facebook.react.uimanager.ViewGroupManager; 19 | import com.facebook.react.uimanager.ReactClippingViewGroupHelper; 20 | import com.facebook.react.uimanager.annotations.ReactPropGroup; 21 | import com.facebook.yoga.YogaConstants; 22 | 23 | import com.facebook.react.views.scroll.FpsListener; 24 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 25 | import com.facebook.react.views.scroll.ScrollEventType; 26 | 27 | 28 | @ReactModule(name = RNScrollViewManager.REACT_CLASS) 29 | public class RNScrollViewManager 30 | extends ViewGroupManager 31 | implements RNScrollViewCommandHelper.ScrollCommandHandler { 32 | 33 | protected static final String REACT_CLASS = "RNScrollView"; 34 | 35 | private static final int[] SPACING_TYPES = { 36 | Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM, 37 | }; 38 | 39 | private @Nullable FpsListener mFpsListener = null; 40 | 41 | public RNScrollViewManager() { 42 | this(null); 43 | } 44 | 45 | public RNScrollViewManager(@Nullable FpsListener fpsListener) { 46 | mFpsListener = fpsListener; 47 | } 48 | 49 | @Override 50 | public String getName() { 51 | return REACT_CLASS; 52 | } 53 | 54 | @Override 55 | public RNScrollView createViewInstance(ThemedReactContext context) { 56 | return new RNScrollView(context, mFpsListener); 57 | } 58 | 59 | @ReactProp(name = "scrollEnabled", defaultBoolean = true) 60 | public void setScrollEnabled(RNScrollView view, boolean value) { 61 | view.setScrollEnabled(value); 62 | } 63 | 64 | @ReactProp(name = "showsVerticalScrollIndicator") 65 | public void setShowsVerticalScrollIndicator(RNScrollView view, boolean value) { 66 | view.setVerticalScrollBarEnabled(value); 67 | } 68 | 69 | @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) 70 | public void setRemoveClippedSubviews(RNScrollView view, boolean removeClippedSubviews) { 71 | view.setRemoveClippedSubviews(removeClippedSubviews); 72 | } 73 | 74 | /** 75 | * Computing momentum events is potentially expensive since we post a runnable on the UI thread 76 | * to see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This 77 | * is handled automatically in js by checking if there is a listener on the momentum events. 78 | * 79 | * @param view 80 | * @param sendMomentumEvents 81 | */ 82 | @ReactProp(name = "sendMomentumEvents") 83 | public void setSendMomentumEvents(RNScrollView view, boolean sendMomentumEvents) { 84 | view.setSendMomentumEvents(sendMomentumEvents); 85 | } 86 | 87 | /** 88 | * Tag used for logging scroll performance on this scroll view. Will force momentum events to be 89 | * turned on (see setSendMomentumEvents). 90 | * 91 | * @param view 92 | * @param scrollPerfTag 93 | */ 94 | @ReactProp(name = "scrollPerfTag") 95 | public void setScrollPerfTag(RNScrollView view, String scrollPerfTag) { 96 | view.setScrollPerfTag(scrollPerfTag); 97 | } 98 | 99 | /** 100 | * When set, fills the rest of the scrollview with a color to avoid setting a background and 101 | * creating unnecessary overdraw. 102 | * @param view 103 | * @param color 104 | */ 105 | @ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color") 106 | public void setBottomFillColor(RNScrollView view, int color) { 107 | view.setEndFillColor(color); 108 | } 109 | 110 | /** 111 | * Controls overScroll behaviour 112 | */ 113 | @ReactProp(name = "overScrollMode") 114 | public void setOverScrollMode(RNScrollView view, String value) { 115 | view.setOverScrollMode(ReactScrollViewHelper.parseOverScrollMode(value)); 116 | } 117 | 118 | @Override 119 | public @Nullable Map getCommandsMap() { 120 | return RNScrollViewCommandHelper.getCommandsMap(); 121 | } 122 | 123 | @Override 124 | public void receiveCommand( 125 | RNScrollView scrollView, 126 | int commandId, 127 | @Nullable ReadableArray args) { 128 | RNScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args); 129 | } 130 | 131 | @Override 132 | public void scrollTo( 133 | RNScrollView scrollView, 134 | RNScrollViewCommandHelper.ScrollToCommandData data) { 135 | if (data.mAnimated) { 136 | scrollView.smoothScrollTo(data.mDestX, data.mDestY); 137 | } else { 138 | scrollView.scrollTo(data.mDestX, data.mDestY); 139 | } 140 | } 141 | 142 | @Override 143 | public void setRNScrollEnabled(RNScrollView view, boolean value) { 144 | view.setScrollEnabled(value); 145 | } 146 | 147 | @ReactPropGroup(names = { 148 | ViewProps.BORDER_RADIUS, 149 | ViewProps.BORDER_TOP_LEFT_RADIUS, 150 | ViewProps.BORDER_TOP_RIGHT_RADIUS, 151 | ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, 152 | ViewProps.BORDER_BOTTOM_LEFT_RADIUS 153 | }, defaultFloat = YogaConstants.UNDEFINED) 154 | public void setBorderRadius(RNScrollView view, int index, float borderRadius) { 155 | if (!YogaConstants.isUndefined(borderRadius)) { 156 | borderRadius = PixelUtil.toPixelFromDIP(borderRadius); 157 | } 158 | 159 | if (index == 0) { 160 | view.setBorderRadius(borderRadius); 161 | } else { 162 | view.setBorderRadius(borderRadius, index - 1); 163 | } 164 | } 165 | 166 | @ReactProp(name = "borderStyle") 167 | public void setBorderStyle(RNScrollView view, @Nullable String borderStyle) { 168 | view.setBorderStyle(borderStyle); 169 | } 170 | 171 | @ReactPropGroup(names = { 172 | ViewProps.BORDER_WIDTH, 173 | ViewProps.BORDER_LEFT_WIDTH, 174 | ViewProps.BORDER_RIGHT_WIDTH, 175 | ViewProps.BORDER_TOP_WIDTH, 176 | ViewProps.BORDER_BOTTOM_WIDTH, 177 | }, defaultFloat = YogaConstants.UNDEFINED) 178 | public void setBorderWidth(RNScrollView view, int index, float width) { 179 | if (!YogaConstants.isUndefined(width)) { 180 | width = PixelUtil.toPixelFromDIP(width); 181 | } 182 | view.setBorderWidth(SPACING_TYPES[index], width); 183 | } 184 | 185 | @ReactPropGroup(names = { 186 | "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor" 187 | }, customType = "Color") 188 | public void setBorderColor(RNScrollView view, int index, Integer color) { 189 | float rgbComponent = 190 | color == null ? YogaConstants.UNDEFINED : (float) ((int)color & 0x00FFFFFF); 191 | float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int)color >>> 24); 192 | view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); 193 | } 194 | 195 | @Override 196 | public void scrollToEnd( 197 | RNScrollView scrollView, 198 | RNScrollViewCommandHelper.ScrollToEndCommandData data) { 199 | // ScrollView always has one child - the scrollable area 200 | int bottom = 201 | scrollView.getChildAt(0).getHeight() + scrollView.getPaddingBottom(); 202 | if (data.mAnimated) { 203 | scrollView.smoothScrollTo(scrollView.getScrollX(), bottom); 204 | } else { 205 | scrollView.scrollTo(scrollView.getScrollX(), bottom); 206 | } 207 | } 208 | 209 | @Override 210 | public @Nullable Map getExportedCustomDirectEventTypeConstants() { 211 | return createExportedCustomDirectEventTypeConstants(); 212 | } 213 | 214 | public static Map createExportedCustomDirectEventTypeConstants() { 215 | return MapBuilder.builder() 216 | .put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll")) 217 | .put(ScrollEventType.BEGIN_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollBeginDrag")) 218 | .put(ScrollEventType.END_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollEndDrag")) 219 | .put(ScrollEventType.ANIMATION_END.getJSEventName(), MapBuilder.of("registrationName", "onScrollAnimationEnd")) 220 | .put(ScrollEventType.MOMENTUM_BEGIN.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollBegin")) 221 | .put(ScrollEventType.MOMENTUM_END.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollEnd")) 222 | .build(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNScrollView.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | import android.os.StrictMode; 12 | import android.util.Log; 13 | import android.view.MotionEvent; 14 | import android.view.VelocityTracker; 15 | import android.view.View; 16 | import android.view.ViewParent; 17 | import android.widget.OverScroller; 18 | import android.widget.ScrollView; 19 | import com.facebook.react.bridge.ReactContext; 20 | import com.facebook.react.views.modal.ReactModalHostView; 21 | import com.facebook.react.views.scroll.FpsListener; 22 | import com.facebook.react.views.scroll.ReactScrollView; 23 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 24 | 25 | import java.lang.reflect.Field; 26 | import java.lang.reflect.Method; 27 | 28 | import javax.annotation.Nullable; 29 | 30 | 31 | public class RNScrollView extends ReactScrollView { 32 | // private Class scrollViewClass = ScrollView.class; 33 | private Class reactScrollViewClass = ReactScrollView.class; 34 | 35 | public RNScrollView(ReactContext context) { 36 | super(context); 37 | } 38 | 39 | public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) { 40 | super(context, fpsListener); 41 | } 42 | 43 | @Override 44 | public boolean onInterceptTouchEvent(MotionEvent ev) { 45 | boolean mScrollEnabled = getInvokeField(reactScrollViewClass, "mScrollEnabled"); 46 | if (!mScrollEnabled) { 47 | return false; 48 | } 49 | 50 | // if (super.onInterceptTouchEvent()) { 51 | // 会将滑动时的触摸操作停止 52 | // NativeGestureUtil.notifyNativeGestureStarted(this, ev); 53 | // 这里不调用父类的方法去判断是否拦截,是否拦截取决与mScrollEnabled 54 | ReactScrollViewHelper.emitScrollBeginDragEvent(this); 55 | setInvokeField(reactScrollViewClass, "mDragging", true); 56 | 57 | invokeMethod(reactScrollViewClass, "enableFpsListener"); 58 | return true; 59 | // } 60 | // 61 | // return false; 62 | } 63 | 64 | public T getInvokeField(Class fieldClass, String fieldName) { 65 | try { 66 | Field field = fieldClass.getDeclaredField(fieldName); 67 | field.setAccessible(true); 68 | return (T) field.get(this); 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | } 72 | return null; 73 | } 74 | 75 | public T setInvokeField(Class fieldClass, String fieldName, T value) { 76 | try { 77 | Field field = fieldClass.getDeclaredField(fieldName); 78 | field.setAccessible(true); 79 | field.set(this, value); 80 | } catch (Exception e) { 81 | e.printStackTrace(); 82 | } 83 | return value; 84 | } 85 | 86 | public T invokeMethod(Class methodClass, String methodName, Object... params) { 87 | try { 88 | Method method = methodClass.getDeclaredMethod(methodName); 89 | method.setAccessible(true); 90 | return (T) method.invoke(this, params); 91 | } catch (Exception e) { 92 | e.printStackTrace(); 93 | } 94 | return null; 95 | } 96 | 97 | // public boolean invokeOnInterceptTouchEvent(MotionEvent event) { 98 | // Class classSV = ScrollView.class; 99 | // //使用MethodType构造出要调用方法的返回类型 100 | // // MethodType methodType = MethodType.methodType(void.class); 101 | // 102 | // try { 103 | // Method onInterceptTouchEvent = classSV.getDeclaredMethod("onInterceptTouchEvent", MotionEvent.class); 104 | // Object test = onInterceptTouchEvent.invoke(this, event); 105 | // return (boolean) test; 106 | // } catch (Exception e) { 107 | // e.printStackTrace(); 108 | // } 109 | // return false; 110 | // } 111 | // 112 | // public boolean superOnInterceptTouchEvent(MotionEvent ev) { 113 | // Log.i("Test", "superOnInterceptTouchEvent: "); 114 | // /* 115 | // * This method JUST determines whether we want to intercept the motion. 116 | // * If we return true, onMotionEvent will be called and we do the actual 117 | // * scrolling there. 118 | // */ 119 | // 120 | // /* 121 | // * Shortcut the most recurring case: the user is in the dragging 122 | // * state and he is moving his finger. We want to intercept this 123 | // * motion. 124 | // */ 125 | // final int action = ev.getAction(); 126 | // boolean mIsBeingDragged = getInvokeField("mIsBeingDragged"); 127 | // if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 128 | // return true; 129 | // } 130 | // 131 | // /* 132 | // * Don't try to intercept touch if we can't scroll anyway. 133 | // */ 134 | // if (getScrollY() == 0 && !canScrollVertically(1)) { 135 | // return false; 136 | // } 137 | // VelocityTracker mVelocityTracker = getInvokeField("mVelocityTracker"); 138 | // final int INVALID_POINTER = getInvokeField("INVALID_POINTER"); 139 | // OverScroller mScroller = getInvokeField("mScroller"); 140 | // 141 | // switch (action & MotionEvent.ACTION_MASK) { 142 | // case MotionEvent.ACTION_MOVE: { 143 | // /* 144 | // * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 145 | // * whether the user has moved far enough from his original down touch. 146 | // */ 147 | // 148 | // /* 149 | // * Locally do absolute value. mLastMotionY is set to the y value 150 | // * of the down event. 151 | // */ 152 | // final int activePointerId = getInvokeField("mActivePointerId"); 153 | // if (activePointerId == INVALID_POINTER) { 154 | // // If we don't have a valid id, the touch down wasn't on content. 155 | // break; 156 | // } 157 | // 158 | // final int pointerIndex = ev.findPointerIndex(activePointerId); 159 | // final String TAG = getInvokeField("TAG"); 160 | // if (pointerIndex == -1) { 161 | // Log.e(TAG, "Invalid pointerId=" + activePointerId 162 | // + " in onInterceptTouchEvent"); 163 | // break; 164 | // } 165 | // 166 | // final int y = (int) ev.getY(pointerIndex); 167 | // 168 | // int mLastMotionY = getInvokeField("mLastMotionY"); 169 | // final int yDiff = Math.abs(y - mLastMotionY); 170 | // 171 | // int mTouchSlop = getInvokeField("mTouchSlop"); 172 | // if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 173 | // mIsBeingDragged = true; 174 | // mLastMotionY = setInvokeField("mLastMotionY", y); 175 | // invokeMethod("initVelocityTrackerIfNotExists"); 176 | // invokeMethod("initVelocityTrackerIfNotExists"); 177 | // mVelocityTracker.addMovement(ev); 178 | // setInvokeField("mNestedYOffset", 0); 179 | // Object mScrollStrictSpan = getInvokeField("mScrollStrictSpan"); 180 | // if (mScrollStrictSpan == null) { 181 | // mScrollStrictSpan = setInvokeField("mScrollStrictSpan", strictModeInvokeMethod("enterCriticalSpan", "ScrollView-scroll")); 182 | // } 183 | // final ViewParent parent = getParent(); 184 | // if (parent != null) { 185 | // parent.requestDisallowInterceptTouchEvent(true); 186 | // } 187 | // } 188 | // break; 189 | // } 190 | // 191 | // case MotionEvent.ACTION_DOWN: { 192 | // final int y = (int) ev.getY(); 193 | // boolean isInChild = inChild((int) ev.getX(), (int) y); 194 | // if (!isInChild) { 195 | // mIsBeingDragged = false; 196 | // invokeMethod("recycleVelocityTracker"); 197 | // break; 198 | // } 199 | // 200 | // /* 201 | // * Remember location of down touch. 202 | // * ACTION_DOWN always refers to pointer index 0. 203 | // */ 204 | // int mLastMotionY = setInvokeField("mLastMotionY", y); 205 | // int mActivePointerId = setInvokeField("mActivePointerId", ev.getPointerId(0)); 206 | // 207 | // invokeMethod("initOrResetVelocityTracker"); 208 | // mVelocityTracker.addMovement(ev); 209 | // /* 210 | // * If being flinged and user touches the screen, initiate drag; 211 | // * otherwise don't. mScroller.isFinished should be false when 212 | // * being flinged. 213 | // */ 214 | // 215 | // mIsBeingDragged = !mScroller.isFinished(); 216 | // Object mScrollStrictSpan = getInvokeField("mScrollStrictSpan"); 217 | // if (mIsBeingDragged && mScrollStrictSpan == null) { 218 | // mScrollStrictSpan = setInvokeField("mScrollStrictSpan", strictModeInvokeMethod("enterCriticalSpan", "ScrollView-scroll")); 219 | // } 220 | // startNestedScroll(SCROLL_AXIS_VERTICAL); 221 | // break; 222 | // } 223 | // 224 | // case MotionEvent.ACTION_CANCEL: 225 | // case MotionEvent.ACTION_UP: 226 | // /* Release the drag */ 227 | // mIsBeingDragged = setInvokeField("mIsBeingDragged", false); 228 | // int mActivePointerId = setInvokeField("mActivePointerId", INVALID_POINTER); 229 | // invokeMethod("recycleVelocityTracker"); 230 | // int mScrollX = getInvokeField("mScrollX"); 231 | // int mScrollY = getInvokeField("mScrollY"); 232 | // 233 | // if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, (int) invokeMethod("getScrollRange"))) { 234 | // postInvalidateOnAnimation(); 235 | // } 236 | // stopNestedScroll(); 237 | // break; 238 | // case MotionEvent.ACTION_POINTER_UP: 239 | // invokeMethod("onSecondaryPointerUp", ev); 240 | // break; 241 | // } 242 | // 243 | // /* 244 | // * The only time we want to intercept motion events is if we are in the 245 | // * drag mode. 246 | // */ 247 | // return mIsBeingDragged; 248 | // } 249 | 250 | // private boolean inChild(int x, int y) { 251 | // if (getChildCount() > 0) { 252 | // final int scrollY = getInvokeField("mScrollY"); 253 | // final View child = getChildAt(0); 254 | // return !(y < child.getTop() - scrollY 255 | // || y >= child.getBottom() - scrollY 256 | // || x < child.getLeft() 257 | // || x >= child.getRight()); 258 | // } 259 | // return false; 260 | // } 261 | } 262 | -------------------------------------------------------------------------------- /android/src/main/java/com/taumu/rnscrollview/RNHorizontalScrollView.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | import android.os.StrictMode; 12 | import android.util.Log; 13 | import android.view.MotionEvent; 14 | import android.view.VelocityTracker; 15 | import android.view.View; 16 | import android.view.ViewParent; 17 | import android.widget.OverScroller; 18 | import android.widget.ScrollView; 19 | import com.facebook.react.bridge.ReactContext; 20 | import com.facebook.react.views.modal.ReactModalHostView; 21 | import com.facebook.react.views.scroll.FpsListener; 22 | import com.facebook.react.views.scroll.ReactHorizontalScrollView; 23 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 24 | 25 | import java.lang.reflect.Field; 26 | import java.lang.reflect.Method; 27 | 28 | import javax.annotation.Nullable; 29 | 30 | 31 | public class RNHorizontalScrollView extends ReactHorizontalScrollView { 32 | // private Class scrollViewClass = ScrollView.class; 33 | private Class reactScrollViewClass = ReactHorizontalScrollView.class; 34 | 35 | public RNHorizontalScrollView(ReactContext context) { 36 | super(context); 37 | } 38 | 39 | public RNHorizontalScrollView(ReactContext context, @Nullable FpsListener fpsListener) { 40 | super(context, fpsListener); 41 | } 42 | 43 | @Override 44 | public boolean onInterceptTouchEvent(MotionEvent ev) { 45 | boolean mScrollEnabled = getInvokeField(reactScrollViewClass, "mScrollEnabled"); 46 | if (!mScrollEnabled) { 47 | return false; 48 | } 49 | 50 | // if (super.onInterceptTouchEvent()) { 51 | // 会将滑动时的触摸操作停止 52 | // NativeGestureUtil.notifyNativeGestureStarted(this, ev); 53 | // 这里不调用父类的方法去判断是否拦截,是否拦截取决与mScrollEnabled 54 | ReactScrollViewHelper.emitScrollBeginDragEvent(this); 55 | setInvokeField(reactScrollViewClass, "mDragging", true); 56 | 57 | invokeMethod(reactScrollViewClass, "enableFpsListener"); 58 | return true; 59 | // } 60 | // 61 | // return false; 62 | } 63 | 64 | public T getInvokeField(Class fieldClass, String fieldName) { 65 | try { 66 | Field field = fieldClass.getDeclaredField(fieldName); 67 | field.setAccessible(true); 68 | return (T) field.get(this); 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | } 72 | return null; 73 | } 74 | 75 | public T setInvokeField(Class fieldClass, String fieldName, T value) { 76 | try { 77 | Field field = fieldClass.getDeclaredField(fieldName); 78 | field.setAccessible(true); 79 | field.set(this, value); 80 | } catch (Exception e) { 81 | e.printStackTrace(); 82 | } 83 | return value; 84 | } 85 | 86 | public T invokeMethod(Class methodClass, String methodName, Object... params) { 87 | try { 88 | Method method = methodClass.getDeclaredMethod(methodName); 89 | method.setAccessible(true); 90 | return (T) method.invoke(this, params); 91 | } catch (Exception e) { 92 | e.printStackTrace(); 93 | } 94 | return null; 95 | } 96 | 97 | // public boolean invokeOnInterceptTouchEvent(MotionEvent event) { 98 | // Class classSV = ScrollView.class; 99 | // //使用MethodType构造出要调用方法的返回类型 100 | // // MethodType methodType = MethodType.methodType(void.class); 101 | // 102 | // try { 103 | // Method onInterceptTouchEvent = classSV.getDeclaredMethod("onInterceptTouchEvent", MotionEvent.class); 104 | // Object test = onInterceptTouchEvent.invoke(this, event); 105 | // return (boolean) test; 106 | // } catch (Exception e) { 107 | // e.printStackTrace(); 108 | // } 109 | // return false; 110 | // } 111 | // 112 | // public boolean superOnInterceptTouchEvent(MotionEvent ev) { 113 | // Log.i("Test", "superOnInterceptTouchEvent: "); 114 | // /* 115 | // * This method JUST determines whether we want to intercept the motion. 116 | // * If we return true, onMotionEvent will be called and we do the actual 117 | // * scrolling there. 118 | // */ 119 | // 120 | // /* 121 | // * Shortcut the most recurring case: the user is in the dragging 122 | // * state and he is moving his finger. We want to intercept this 123 | // * motion. 124 | // */ 125 | // final int action = ev.getAction(); 126 | // boolean mIsBeingDragged = getInvokeField("mIsBeingDragged"); 127 | // if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 128 | // return true; 129 | // } 130 | // 131 | // /* 132 | // * Don't try to intercept touch if we can't scroll anyway. 133 | // */ 134 | // if (getScrollY() == 0 && !canScrollVertically(1)) { 135 | // return false; 136 | // } 137 | // VelocityTracker mVelocityTracker = getInvokeField("mVelocityTracker"); 138 | // final int INVALID_POINTER = getInvokeField("INVALID_POINTER"); 139 | // OverScroller mScroller = getInvokeField("mScroller"); 140 | // 141 | // switch (action & MotionEvent.ACTION_MASK) { 142 | // case MotionEvent.ACTION_MOVE: { 143 | // /* 144 | // * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 145 | // * whether the user has moved far enough from his original down touch. 146 | // */ 147 | // 148 | // /* 149 | // * Locally do absolute value. mLastMotionY is set to the y value 150 | // * of the down event. 151 | // */ 152 | // final int activePointerId = getInvokeField("mActivePointerId"); 153 | // if (activePointerId == INVALID_POINTER) { 154 | // // If we don't have a valid id, the touch down wasn't on content. 155 | // break; 156 | // } 157 | // 158 | // final int pointerIndex = ev.findPointerIndex(activePointerId); 159 | // final String TAG = getInvokeField("TAG"); 160 | // if (pointerIndex == -1) { 161 | // Log.e(TAG, "Invalid pointerId=" + activePointerId 162 | // + " in onInterceptTouchEvent"); 163 | // break; 164 | // } 165 | // 166 | // final int y = (int) ev.getY(pointerIndex); 167 | // 168 | // int mLastMotionY = getInvokeField("mLastMotionY"); 169 | // final int yDiff = Math.abs(y - mLastMotionY); 170 | // 171 | // int mTouchSlop = getInvokeField("mTouchSlop"); 172 | // if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 173 | // mIsBeingDragged = true; 174 | // mLastMotionY = setInvokeField("mLastMotionY", y); 175 | // invokeMethod("initVelocityTrackerIfNotExists"); 176 | // invokeMethod("initVelocityTrackerIfNotExists"); 177 | // mVelocityTracker.addMovement(ev); 178 | // setInvokeField("mNestedYOffset", 0); 179 | // Object mScrollStrictSpan = getInvokeField("mScrollStrictSpan"); 180 | // if (mScrollStrictSpan == null) { 181 | // mScrollStrictSpan = setInvokeField("mScrollStrictSpan", strictModeInvokeMethod("enterCriticalSpan", "ScrollView-scroll")); 182 | // } 183 | // final ViewParent parent = getParent(); 184 | // if (parent != null) { 185 | // parent.requestDisallowInterceptTouchEvent(true); 186 | // } 187 | // } 188 | // break; 189 | // } 190 | // 191 | // case MotionEvent.ACTION_DOWN: { 192 | // final int y = (int) ev.getY(); 193 | // boolean isInChild = inChild((int) ev.getX(), (int) y); 194 | // if (!isInChild) { 195 | // mIsBeingDragged = false; 196 | // invokeMethod("recycleVelocityTracker"); 197 | // break; 198 | // } 199 | // 200 | // /* 201 | // * Remember location of down touch. 202 | // * ACTION_DOWN always refers to pointer index 0. 203 | // */ 204 | // int mLastMotionY = setInvokeField("mLastMotionY", y); 205 | // int mActivePointerId = setInvokeField("mActivePointerId", ev.getPointerId(0)); 206 | // 207 | // invokeMethod("initOrResetVelocityTracker"); 208 | // mVelocityTracker.addMovement(ev); 209 | // /* 210 | // * If being flinged and user touches the screen, initiate drag; 211 | // * otherwise don't. mScroller.isFinished should be false when 212 | // * being flinged. 213 | // */ 214 | // 215 | // mIsBeingDragged = !mScroller.isFinished(); 216 | // Object mScrollStrictSpan = getInvokeField("mScrollStrictSpan"); 217 | // if (mIsBeingDragged && mScrollStrictSpan == null) { 218 | // mScrollStrictSpan = setInvokeField("mScrollStrictSpan", strictModeInvokeMethod("enterCriticalSpan", "ScrollView-scroll")); 219 | // } 220 | // startNestedScroll(SCROLL_AXIS_VERTICAL); 221 | // break; 222 | // } 223 | // 224 | // case MotionEvent.ACTION_CANCEL: 225 | // case MotionEvent.ACTION_UP: 226 | // /* Release the drag */ 227 | // mIsBeingDragged = setInvokeField("mIsBeingDragged", false); 228 | // int mActivePointerId = setInvokeField("mActivePointerId", INVALID_POINTER); 229 | // invokeMethod("recycleVelocityTracker"); 230 | // int mScrollX = getInvokeField("mScrollX"); 231 | // int mScrollY = getInvokeField("mScrollY"); 232 | // 233 | // if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, (int) invokeMethod("getScrollRange"))) { 234 | // postInvalidateOnAnimation(); 235 | // } 236 | // stopNestedScroll(); 237 | // break; 238 | // case MotionEvent.ACTION_POINTER_UP: 239 | // invokeMethod("onSecondaryPointerUp", ev); 240 | // break; 241 | // } 242 | // 243 | // /* 244 | // * The only time we want to intercept motion events is if we are in the 245 | // * drag mode. 246 | // */ 247 | // return mIsBeingDragged; 248 | // } 249 | 250 | // private boolean inChild(int x, int y) { 251 | // if (getChildCount() > 0) { 252 | // final int scrollY = getInvokeField("mScrollY"); 253 | // final View child = getChildAt(0); 254 | // return !(y < child.getTop() - scrollY 255 | // || y >= child.getBottom() - scrollY 256 | // || x < child.getLeft() 257 | // || x >= child.getRight()); 258 | // } 259 | // return false; 260 | // } 261 | } 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-scroll-paged-view 2 | 3 | [以中文查看](./README_zh-CN.md) 4 | [If you are interested in my development process, you may read it, I believe you will gain something](./docs/Dev_Record.md) 5 | 6 | **scroll view, Inside scroll, Full page scroll, Nesting ScrollView** 7 | 8 | ## Installation 9 | 10 | ``` 11 | npm install react-scroll-paged-view --save 12 | ``` 13 | 14 | ## Introduction 15 | 16 | - Support React(web) & React Native(RN) 17 | - Full-page scrolling and in-page scrolling 18 | - iOS RN code is perfectly supported. Android provides native package support. Based on RN ScrollView, some code changes are supported 19 | - There is no combination of internal scrolling and page scrolling in open source RN projects. Write this component based on need 20 | - In addition, the core functional module ViewPaged is also available for use 21 | - ScrollView component that provides subassembly packaging that can optionally be used 22 | - All pagination loads on demand, don't worry about initial rendering 23 | - Infinite pagination is also lazy, minimizing the current index page, even when switching quickly 24 | - RN and web animation based on animated library, sharing a set of code processing 25 | - Provides renderHeader and renderFooter for tab switching or carousel graphics, etc. 26 | - Both components of the web version provide a class variable isTouch for judging whether it is a touch event, thereby distinguishing the scrolling triggered click event. 27 | - Support for ssr, 2.1+ version removes the initial measurement size caused by repeated creation and destruction of components, better performance 28 | - 2.1.3+ version uses ScrollView as a scrolling container when scrolling horizontally and not infinitely, so subviews can be scrolled vertically using ScrollView 29 | 30 | ## Notice 31 | 32 | - ~~**Compatible version "react-native": "~0.54.0"**~~ 33 | - ~~**The react native 0.47 version uses the 0.1.\* version**~~ 34 | - **Has been perfectly compatible with the above RN version, directly install the latest package** 35 | - **Click events that do not appear in the internal ScrollView component can be replaced with onPressIn** 36 | - **infinite and autoPlay are only available to the ViewPaged component, ScrollPagedView will turn off this option by default** 37 | 38 | ## Demo 39 | 40 | | IOS | Android | Web | 41 | | ---------------------- | ------------------------------ | ---------------------- | 42 | | ![IOS](./demo/ios.gif) | ![Android](./demo/android.gif) | ![Web](./demo/web.gif) | 43 | 44 | ### Other 45 | 46 | What you can achieve depends on what you can imagine. 47 | 48 | | Horizontal | Tab | Carousel | 49 | | ------------------------------------ | ---------------------- | -------------------------------- | 50 | | ![Horizontal](./demo/horizontal.gif) | ![Tab](./demo/tab.gif) | ![Carousel](./demo/carousel.gif) | 51 | 52 | ## Usage 53 | 54 | ### ScrollPagedView 55 | 56 | The ScrollPagedView component encapsulates the inner scrolling component based on the ViewPaged component and uses it through the context. 57 | 58 | ```javascript 59 | import ScrollPagedView from 'react-scroll-paged-view' 60 | import InsideScrollView from './InsideScrollView' 61 | 62 | ... 63 | _onChange = (pageIndex) => { 64 | ... 65 | } 66 | 67 | render() { 68 | return ( 69 | 73 | 74 | 75 | 76 | 77 | ) 78 | } 79 | ... 80 | ``` 81 | 82 | #### Context ScrollView(InsideScrollView) 83 | 84 | ```javascript 85 | ... 86 | static contextTypes = { 87 | ScrollView: PropTypes.func, 88 | } 89 | 90 | render() { 91 | const ScrollView = this.context.ScrollView 92 | return ( 93 | 94 | ... 95 | 96 | ) 97 | } 98 | ... 99 | ``` 100 | 101 | ### ViewPaged 102 | 103 | The ViewPaged component is consistent with the ScrollPagedView component and is free to use infinite and autoPlay. 104 | 105 | ```javascript 106 | import { ViewPaged } from 'react-scroll-paged-view' 107 | ``` 108 | 109 | ## Export module 110 | 111 | - default - ScrollPagedView 112 | - ViewPaged 113 | 114 | ## Properties 115 | 116 | ### ScrollPagedView 117 | 118 | The ScrollPagedView component is based on the ViewPaged component, which can be passed to the props of the ViewPaged as needed. Refer to the props of the ViewPaged component below. 119 | 120 | | Name | propType | default value | description | 121 | | ------- | -------- | ------------- | ------------------------------------------------------------------------------- | 122 | | withRef | bool | false | Get ViewPaged instance ref, through the component's getViewPagedInstance method | 123 | 124 | ### Context ScrollView 125 | 126 | | Name | propType | default value | description | 127 | | ------------------------ | -------- | ------------- | -------------------- | 128 | | nativeProps(native only) | object | {} | RN scrollView Props | 129 | | webProps(web only) | object | {} | Web scrollView Props | 130 | 131 | ### ViewPaged 132 | 133 | RN and web have the same props and the performance is consistent 134 | 135 | #### Common Props 136 | 137 | | Name | propType | default value | description | 138 | | -------------- | ---------------- | ------------- | ----------------------------------------------------------------------------- | 139 | | style | object | {} | ViewPaged style | 140 | | initialPage | number | 0 | Initial page index | 141 | | vertical | bool | true | Whether to switch the view vertically | 142 | | onChange | function | () => {} | Switch paging callbacks, The parameters are currentPage and prevPage | 143 | | duration | number | 400 | Animation duration(In milliseconds) | 144 | | infinite | bool | false | Whether it is an infinite scroll view | 145 | | renderHeader | function/element | undefined | Header Component, The parameters are activeTab, goToPage, width, pos | 146 | | renderFooter | function/element | undefined | Footer Component, The parameters are activeTab, goToPage, width, pos | 147 | | renderPosition | string | top | Header/Footer direction, There are 4 values, 'top', 'left', 'bottom', 'right' | 148 | | autoPlay | bool | false | Whether to auto rotate | 149 | | autoPlaySpeed | number | 2000 | Automatic carousel interval (In milliseconds) | 150 | | hasAnimation | bool | true | Click to switch whether there is an animation | 151 | | locked | bool | false | Whether to allow drag toggle | 152 | | preRenderRange | number | 0 | Control the scope of the render component each time it is updated | 153 | | isMovingRender | bool | false | Preload the next page when you touch Move | 154 | 155 | #### RN Only Props 156 | 157 | | Name | propType | default value | description | 158 | | ----------------------------------- | -------- | ------------- | ------------------------------------------------------ | 159 | | onStartShouldSetPanResponder | function | () => true | Reference React Native website gesture response system | 160 | | onStartShouldSetPanResponderCapture | function | () => false | Reference React Native website gesture response system | 161 | | onMoveShouldSetPanResponder | function | () => true | Reference React Native website gesture response system | 162 | | onMoveShouldSetPanResponderCapture | function | () => false | Reference React Native website gesture response system | 163 | | onPanResponderTerminationRequest | function | () => true | Reference React Native website gesture response system | 164 | | onShouldBlockNativeResponder | function | () => true | Reference React Native website gesture response system | 165 | | onPanResponderTerminate | function | () => {} | Reference React Native website gesture response system | 166 | 167 | ## TODO 168 | 169 | - [x] Optimize scroll region index, use proxy scrollView to complete 170 | - [x] Android compatible React Native different versions 171 | - [x] Support web side components 172 | - [x] Optimize web side components 173 | - [x] Optimize web infinite scrolling 174 | - [x] Perfect web-side ViewPaged 175 | - [x] Optimize structure, code, unified naming 176 | - [x] Uniformly compatible with different versions of React Native 177 | - [x] Record development process 178 | - [x] Perfect RN end ViewPaged achieves consistency with web performance 179 | - [x] More props configuration 180 | 181 | ## Changelog 182 | 183 | - 0.1.\* 184 | - 1.0.\* 185 | - 1.1.\* 186 | - 1.2.\* 187 | - 1.3.\* 188 | - 1.5.\* 189 | - 1.6.\* 190 | - 2.0.\* 191 | - 2.1.\* 192 | 193 | ### 2.0.\* 194 | 195 | - Overall refactoring project to improve code reuse for web-side refactoring 196 | - Added dependency package animated, smoother animation and better performance 197 | - Use hoc to maximize the reuse of three-terminal public code, each side only retains the code of its own platform 198 | - Unify the props on the RN and web, and make them consistent 199 | - Support for ssr server rendering 200 | 201 | ### 2.1.\* 202 | 203 | - Unified RN and web render methods for ssr 204 | - A separate render when removing the initial measurement component size 205 | - Avoid sub-components to be created and destroyed repeatedly, with better performance 206 | 207 | ### 2.1.4+ 208 | 209 | - Remove configuration files such as. babelrc from the uploaded NPM package, react native will use the Babel configuration in the package, and failing to install these configuration dependencies will report errors 210 | 211 | ### 2.2.0+ 212 | 213 | - Optimize code structure, precisely control component render times, improve page performance, and provide preload and render scope props 214 | -------------------------------------------------------------------------------- /android/react-scroll-paged-view.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.54/RNScrollView.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.graphics.drawable.Drawable; 9 | import android.support.v4.view.ViewCompat; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.OverScroller; 15 | import android.widget.ScrollView; 16 | 17 | import com.facebook.infer.annotation.Assertions; 18 | import com.facebook.react.bridge.ReactContext; 19 | import com.facebook.react.common.ReactConstants; 20 | import com.facebook.react.uimanager.MeasureSpecAssertions; 21 | import com.facebook.react.uimanager.ReactClippingViewGroup; 22 | import com.facebook.react.uimanager.ReactClippingViewGroupHelper; 23 | import com.facebook.react.views.scroll.FpsListener; 24 | import com.facebook.react.views.scroll.OnScrollDispatchHelper; 25 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 26 | import com.facebook.react.views.scroll.VelocityHelper; 27 | import com.facebook.react.views.view.ReactViewBackgroundManager; 28 | 29 | import java.lang.reflect.Field; 30 | 31 | import javax.annotation.Nullable; 32 | 33 | 34 | @TargetApi(11) 35 | public class RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener { 36 | 37 | private static @Nullable Field sScrollerField; 38 | private static boolean sTriedToGetScrollerField = false; 39 | 40 | private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); 41 | private final @Nullable OverScroller mScroller; 42 | private final VelocityHelper mVelocityHelper = new VelocityHelper(); 43 | 44 | private @Nullable Rect mClippingRect; 45 | private boolean mDoneFlinging; 46 | private boolean mDragging; 47 | private boolean mFlinging; 48 | private boolean mRemoveClippedSubviews; 49 | static private boolean mScrollEnabled = true; 50 | private boolean mSendMomentumEvents; 51 | private @Nullable FpsListener mFpsListener = null; 52 | private @Nullable String mScrollPerfTag; 53 | private @Nullable Drawable mEndBackground; 54 | private int mEndFillColor = Color.TRANSPARENT; 55 | private View mContentView; 56 | private ReactViewBackgroundManager mReactBackgroundManager; 57 | 58 | public RNScrollView(ReactContext context) { 59 | this(context, null); 60 | } 61 | 62 | public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) { 63 | super(context); 64 | mFpsListener = fpsListener; 65 | mReactBackgroundManager = new ReactViewBackgroundManager(this); 66 | 67 | mScroller = getOverScrollerFromParent(); 68 | setOnHierarchyChangeListener(this); 69 | setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY); 70 | } 71 | 72 | @Nullable 73 | private OverScroller getOverScrollerFromParent() { 74 | OverScroller scroller; 75 | 76 | if (!sTriedToGetScrollerField) { 77 | sTriedToGetScrollerField = true; 78 | try { 79 | sScrollerField = ScrollView.class.getDeclaredField("mScroller"); 80 | sScrollerField.setAccessible(true); 81 | } catch (NoSuchFieldException e) { 82 | Log.w( 83 | ReactConstants.TAG, 84 | "Failed to get mScroller field for ScrollView! " + 85 | "This app will exhibit the bounce-back scrolling bug :("); 86 | } 87 | } 88 | 89 | if (sScrollerField != null) { 90 | try { 91 | Object scrollerValue = sScrollerField.get(this); 92 | if (scrollerValue instanceof OverScroller) { 93 | scroller = (OverScroller) scrollerValue; 94 | } else { 95 | Log.w( 96 | ReactConstants.TAG, 97 | "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + 98 | "This app will exhibit the bounce-back scrolling bug :("); 99 | scroller = null; 100 | } 101 | } catch (IllegalAccessException e) { 102 | throw new RuntimeException("Failed to get mScroller from ScrollView!", e); 103 | } 104 | } else { 105 | scroller = null; 106 | } 107 | 108 | return scroller; 109 | } 110 | 111 | public void setSendMomentumEvents(boolean sendMomentumEvents) { 112 | mSendMomentumEvents = sendMomentumEvents; 113 | } 114 | 115 | public void setScrollPerfTag(@Nullable String scrollPerfTag) { 116 | mScrollPerfTag = scrollPerfTag; 117 | } 118 | 119 | public void setScrollEnabled(boolean scrollEnabled) { 120 | mScrollEnabled = scrollEnabled; 121 | } 122 | 123 | public void flashScrollIndicators() { 124 | awakenScrollBars(); 125 | } 126 | 127 | @Override 128 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 129 | MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); 130 | 131 | setMeasuredDimension( 132 | MeasureSpec.getSize(widthMeasureSpec), 133 | MeasureSpec.getSize(heightMeasureSpec)); 134 | } 135 | 136 | @Override 137 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 138 | // Call with the present values in order to re-layout if necessary 139 | scrollTo(getScrollX(), getScrollY()); 140 | } 141 | 142 | @Override 143 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 144 | super.onSizeChanged(w, h, oldw, oldh); 145 | if (mRemoveClippedSubviews) { 146 | updateClippingRect(); 147 | } 148 | } 149 | 150 | @Override 151 | protected void onAttachedToWindow() { 152 | super.onAttachedToWindow(); 153 | if (mRemoveClippedSubviews) { 154 | updateClippingRect(); 155 | } 156 | } 157 | 158 | @Override 159 | protected void onScrollChanged(int x, int y, int oldX, int oldY) { 160 | super.onScrollChanged(x, y, oldX, oldY); 161 | 162 | if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { 163 | if (mRemoveClippedSubviews) { 164 | updateClippingRect(); 165 | } 166 | 167 | if (mFlinging) { 168 | mDoneFlinging = false; 169 | } 170 | 171 | ReactScrollViewHelper.emitScrollEvent( 172 | this, 173 | mOnScrollDispatchHelper.getXFlingVelocity(), 174 | mOnScrollDispatchHelper.getYFlingVelocity()); 175 | } 176 | } 177 | 178 | @Override 179 | public boolean onInterceptTouchEvent(MotionEvent ev) { 180 | if (!mScrollEnabled) { 181 | return false; 182 | } 183 | 184 | if (super.onInterceptTouchEvent(ev)) { 185 | // 会将滑动时的触摸操作停止 186 | // NativeGestureUtil.notifyNativeGestureStarted(this, ev); 187 | ReactScrollViewHelper.emitScrollBeginDragEvent(this); 188 | mDragging = true; 189 | enableFpsListener(); 190 | return true; 191 | } 192 | 193 | return false; 194 | } 195 | 196 | @Override 197 | public boolean onTouchEvent(MotionEvent ev) { 198 | if (!mScrollEnabled) { 199 | return false; 200 | } 201 | 202 | mVelocityHelper.calculateVelocity(ev); 203 | int action = ev.getAction() & MotionEvent.ACTION_MASK; 204 | if (action == MotionEvent.ACTION_UP && mDragging) { 205 | ReactScrollViewHelper.emitScrollEndDragEvent( 206 | this, 207 | mVelocityHelper.getXVelocity(), 208 | mVelocityHelper.getYVelocity()); 209 | mDragging = false; 210 | disableFpsListener(); 211 | } 212 | 213 | return super.onTouchEvent(ev); 214 | } 215 | 216 | @Override 217 | public void setRemoveClippedSubviews(boolean removeClippedSubviews) { 218 | if (removeClippedSubviews && mClippingRect == null) { 219 | mClippingRect = new Rect(); 220 | } 221 | mRemoveClippedSubviews = removeClippedSubviews; 222 | updateClippingRect(); 223 | } 224 | 225 | @Override 226 | public boolean getRemoveClippedSubviews() { 227 | return mRemoveClippedSubviews; 228 | } 229 | 230 | @Override 231 | public void updateClippingRect() { 232 | if (!mRemoveClippedSubviews) { 233 | return; 234 | } 235 | 236 | Assertions.assertNotNull(mClippingRect); 237 | 238 | ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); 239 | View contentView = getChildAt(0); 240 | if (contentView instanceof ReactClippingViewGroup) { 241 | ((ReactClippingViewGroup) contentView).updateClippingRect(); 242 | } 243 | } 244 | 245 | @Override 246 | public void getClippingRect(Rect outClippingRect) { 247 | outClippingRect.set(Assertions.assertNotNull(mClippingRect)); 248 | } 249 | 250 | @Override 251 | public void fling(int velocityY) { 252 | if (mScroller != null) { 253 | // FB SCROLLVIEW CHANGE 254 | 255 | // We provide our own version of fling that uses a different call to the standard OverScroller 256 | // which takes into account the possibility of adding new content while the ScrollView is 257 | // animating. Because we give essentially no max Y for the fling, the fling will continue as long 258 | // as there is content. See #onOverScrolled() to see the second part of this change which properly 259 | // aborts the scroller animation when we get to the bottom of the ScrollView content. 260 | 261 | int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 262 | 263 | mScroller.fling( 264 | getScrollX(), 265 | getScrollY(), 266 | 0, 267 | velocityY, 268 | 0, 269 | 0, 270 | 0, 271 | Integer.MAX_VALUE, 272 | 0, 273 | scrollWindowHeight / 2); 274 | 275 | ViewCompat.postInvalidateOnAnimation(this); 276 | 277 | // END FB SCROLLVIEW CHANGE 278 | } else { 279 | super.fling(velocityY); 280 | } 281 | 282 | if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) { 283 | mFlinging = true; 284 | enableFpsListener(); 285 | ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, 0, velocityY); 286 | Runnable r = new Runnable() { 287 | @Override 288 | public void run() { 289 | if (mDoneFlinging) { 290 | mFlinging = false; 291 | disableFpsListener(); 292 | ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this); 293 | } else { 294 | mDoneFlinging = true; 295 | ViewCompat.postOnAnimationDelayed( 296 | RNScrollView.this, 297 | this, 298 | ReactScrollViewHelper.MOMENTUM_DELAY); 299 | } 300 | } 301 | }; 302 | ViewCompat.postOnAnimationDelayed(this, r, ReactScrollViewHelper.MOMENTUM_DELAY); 303 | } 304 | } 305 | 306 | private void enableFpsListener() { 307 | if (isScrollPerfLoggingEnabled()) { 308 | Assertions.assertNotNull(mFpsListener); 309 | Assertions.assertNotNull(mScrollPerfTag); 310 | mFpsListener.enable(mScrollPerfTag); 311 | } 312 | } 313 | 314 | private void disableFpsListener() { 315 | if (isScrollPerfLoggingEnabled()) { 316 | Assertions.assertNotNull(mFpsListener); 317 | Assertions.assertNotNull(mScrollPerfTag); 318 | mFpsListener.disable(mScrollPerfTag); 319 | } 320 | } 321 | 322 | private boolean isScrollPerfLoggingEnabled() { 323 | return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty(); 324 | } 325 | 326 | private int getMaxScrollY() { 327 | int contentHeight = mContentView.getHeight(); 328 | int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 329 | return Math.max(0, contentHeight - viewportHeight); 330 | } 331 | 332 | @Override 333 | public void draw(Canvas canvas) { 334 | if (mEndFillColor != Color.TRANSPARENT) { 335 | final View content = getChildAt(0); 336 | if (mEndBackground != null && content != null && content.getBottom() < getHeight()) { 337 | mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight()); 338 | mEndBackground.draw(canvas); 339 | } 340 | } 341 | super.draw(canvas); 342 | } 343 | 344 | public void setEndFillColor(int color) { 345 | if (color != mEndFillColor) { 346 | mEndFillColor = color; 347 | mEndBackground = new ColorDrawable(mEndFillColor); 348 | } 349 | } 350 | 351 | @Override 352 | protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 353 | if (mScroller != null) { 354 | // FB SCROLLVIEW CHANGE 355 | 356 | // This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for 357 | // more information. 358 | 359 | if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) { 360 | int scrollRange = getMaxScrollY(); 361 | if (scrollY >= scrollRange) { 362 | mScroller.abortAnimation(); 363 | scrollY = scrollRange; 364 | } 365 | } 366 | 367 | // END FB SCROLLVIEW CHANGE 368 | } 369 | 370 | super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); 371 | } 372 | 373 | @Override 374 | public void onChildViewAdded(View parent, View child) { 375 | mContentView = child; 376 | mContentView.addOnLayoutChangeListener(this); 377 | } 378 | 379 | @Override 380 | public void onChildViewRemoved(View parent, View child) { 381 | mContentView.removeOnLayoutChangeListener(this); 382 | mContentView = null; 383 | } 384 | 385 | /** 386 | * Called when a mContentView's layout has changed. Fixes the scroll position if it's too large 387 | * after the content resizes. Without this, the user would see a blank ScrollView when the scroll 388 | * position is larger than the ScrollView's max scroll position after the content shrinks. 389 | */ 390 | @Override 391 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 392 | if (mContentView == null) { 393 | return; 394 | } 395 | 396 | int currentScrollY = getScrollY(); 397 | int maxScrollY = getMaxScrollY(); 398 | if (currentScrollY > maxScrollY) { 399 | scrollTo(getScrollX(), maxScrollY); 400 | } 401 | } 402 | 403 | @Override 404 | public void setBackgroundColor(int color) { 405 | mReactBackgroundManager.setBackgroundColor(color); 406 | } 407 | 408 | public void setBorderWidth(int position, float width) { 409 | mReactBackgroundManager.setBorderWidth(position, width); 410 | } 411 | 412 | public void setBorderColor(int position, float color, float alpha) { 413 | mReactBackgroundManager.setBorderColor(position, color, alpha); 414 | } 415 | 416 | public void setBorderRadius(float borderRadius) { 417 | mReactBackgroundManager.setBorderRadius(borderRadius); 418 | } 419 | 420 | public void setBorderRadius(float borderRadius, int position) { 421 | mReactBackgroundManager.setBorderRadius(borderRadius, position); 422 | } 423 | 424 | public void setBorderStyle(@Nullable String style) { 425 | mReactBackgroundManager.setBorderStyle(style); 426 | } 427 | 428 | } 429 | -------------------------------------------------------------------------------- /android-old-version/rnscrollview-0.47/RNScrollView.java: -------------------------------------------------------------------------------- 1 | package com.taumu.rnscrollview; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.Rect; 6 | import android.graphics.drawable.ColorDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.graphics.drawable.LayerDrawable; 9 | import android.util.Log; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.OverScroller; 14 | import android.widget.ScrollView; 15 | 16 | import com.facebook.infer.annotation.Assertions; 17 | import com.facebook.react.bridge.ReactContext; 18 | import com.facebook.react.common.ReactConstants; 19 | import com.facebook.react.uimanager.MeasureSpecAssertions; 20 | import com.facebook.react.uimanager.ReactClippingViewGroup; 21 | import com.facebook.react.uimanager.ReactClippingViewGroupHelper; 22 | import com.facebook.react.views.scroll.FpsListener; 23 | import com.facebook.react.views.scroll.OnScrollDispatchHelper; 24 | import com.facebook.react.views.scroll.ReactScrollViewHelper; 25 | import com.facebook.react.views.scroll.VelocityHelper; 26 | import com.facebook.react.views.view.ReactViewBackgroundDrawable; 27 | 28 | import java.lang.reflect.Field; 29 | 30 | import javax.annotation.Nullable; 31 | 32 | 33 | public class RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener { 34 | 35 | private static Field sScrollerField; 36 | private static boolean sTriedToGetScrollerField = false; 37 | 38 | private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); 39 | private final OverScroller mScroller; 40 | private final VelocityHelper mVelocityHelper = new VelocityHelper(); 41 | 42 | private @Nullable Rect mClippingRect; 43 | private boolean mDoneFlinging; 44 | private boolean mDragging; 45 | private boolean mFlinging; 46 | private boolean mRemoveClippedSubviews; 47 | static private boolean mScrollEnabled = true; 48 | private boolean mSendMomentumEvents; 49 | private @Nullable 50 | FpsListener mFpsListener = null; 51 | private @Nullable String mScrollPerfTag; 52 | private @Nullable Drawable mEndBackground; 53 | private int mEndFillColor = Color.TRANSPARENT; 54 | private View mContentView; 55 | private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; 56 | 57 | public RNScrollView(ReactContext context) { 58 | this(context, null); 59 | } 60 | 61 | public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) { 62 | super(context); 63 | mFpsListener = fpsListener; 64 | 65 | if (!sTriedToGetScrollerField) { 66 | sTriedToGetScrollerField = true; 67 | try { 68 | sScrollerField = ScrollView.class.getDeclaredField("mScroller"); 69 | sScrollerField.setAccessible(true); 70 | } catch (NoSuchFieldException e) { 71 | Log.w( 72 | ReactConstants.TAG, 73 | "Failed to get mScroller field for ScrollView! " + 74 | "This app will exhibit the bounce-back scrolling bug :("); 75 | } 76 | } 77 | 78 | if (sScrollerField != null) { 79 | try { 80 | Object scroller = sScrollerField.get(this); 81 | if (scroller instanceof OverScroller) { 82 | mScroller = (OverScroller) scroller; 83 | } else { 84 | Log.w( 85 | ReactConstants.TAG, 86 | "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + 87 | "This app will exhibit the bounce-back scrolling bug :("); 88 | mScroller = null; 89 | } 90 | } catch (IllegalAccessException e) { 91 | throw new RuntimeException("Failed to get mScroller from ScrollView!", e); 92 | } 93 | } else { 94 | mScroller = null; 95 | } 96 | 97 | setOnHierarchyChangeListener(this); 98 | setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY); 99 | } 100 | 101 | public void setSendMomentumEvents(boolean sendMomentumEvents) { 102 | mSendMomentumEvents = sendMomentumEvents; 103 | } 104 | 105 | public void setScrollPerfTag(String scrollPerfTag) { 106 | mScrollPerfTag = scrollPerfTag; 107 | } 108 | 109 | public void setScrollEnabled(boolean scrollEnabled) { 110 | mScrollEnabled = scrollEnabled; 111 | // Log.i("setScrollEnabled", "value:" + scrollEnabled + " setValue:" + mScrollEnabled); 112 | } 113 | 114 | @Override 115 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 116 | MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec); 117 | 118 | setMeasuredDimension( 119 | MeasureSpec.getSize(widthMeasureSpec), 120 | MeasureSpec.getSize(heightMeasureSpec)); 121 | } 122 | 123 | @Override 124 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 125 | // Call with the present values in order to re-layout if necessary 126 | scrollTo(getScrollX(), getScrollY()); 127 | } 128 | 129 | @Override 130 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 131 | super.onSizeChanged(w, h, oldw, oldh); 132 | if (mRemoveClippedSubviews) { 133 | updateClippingRect(); 134 | } 135 | } 136 | 137 | @Override 138 | protected void onAttachedToWindow() { 139 | super.onAttachedToWindow(); 140 | if (mRemoveClippedSubviews) { 141 | updateClippingRect(); 142 | } 143 | } 144 | 145 | @Override 146 | protected void onScrollChanged(int x, int y, int oldX, int oldY) { 147 | super.onScrollChanged(x, y, oldX, oldY); 148 | 149 | if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { 150 | if (mRemoveClippedSubviews) { 151 | updateClippingRect(); 152 | } 153 | 154 | if (mFlinging) { 155 | mDoneFlinging = false; 156 | } 157 | 158 | ReactScrollViewHelper.emitScrollEvent( 159 | this, 160 | mOnScrollDispatchHelper.getXFlingVelocity(), 161 | mOnScrollDispatchHelper.getYFlingVelocity()); 162 | } 163 | } 164 | 165 | @Override 166 | public boolean onInterceptTouchEvent(MotionEvent ev) { 167 | // Log.i("onInterceptTouchEvent", "value:" + mScrollEnabled); 168 | if (!mScrollEnabled) { 169 | return false; 170 | } 171 | 172 | if (super.onInterceptTouchEvent(ev)) { 173 | // 会将滑动时的触摸操作停止 174 | // NativeGestureUtil.notifyNativeGestureStarted(this, ev); 175 | ReactScrollViewHelper.emitScrollBeginDragEvent(this); 176 | mDragging = true; 177 | enableFpsListener(); 178 | return true; 179 | } 180 | 181 | return false; 182 | } 183 | 184 | // @Override 185 | // public boolean dispatchTouchEvent(MotionEvent ev) { 186 | // Log.i("dispatchTouchEvent", "value:" + mScrollEnabled); 187 | // if (!mScrollEnabled) { 188 | // return false; 189 | // } 190 | // return super.dispatchTouchEvent(ev); 191 | // } 192 | 193 | @Override 194 | public boolean onTouchEvent(MotionEvent ev) { 195 | // Log.i("onTouchEvent", "value:" + mScrollEnabled); 196 | if (!mScrollEnabled) { 197 | return false; 198 | } 199 | 200 | mVelocityHelper.calculateVelocity(ev); 201 | int action = ev.getAction() & MotionEvent.ACTION_MASK; 202 | if (action == MotionEvent.ACTION_UP && mDragging) { 203 | ReactScrollViewHelper.emitScrollEndDragEvent( 204 | this, 205 | mVelocityHelper.getXVelocity(), 206 | mVelocityHelper.getYVelocity()); 207 | mDragging = false; 208 | disableFpsListener(); 209 | } 210 | 211 | return super.onTouchEvent(ev); 212 | } 213 | 214 | @Override 215 | public void setRemoveClippedSubviews(boolean removeClippedSubviews) { 216 | if (removeClippedSubviews && mClippingRect == null) { 217 | mClippingRect = new Rect(); 218 | } 219 | mRemoveClippedSubviews = removeClippedSubviews; 220 | updateClippingRect(); 221 | } 222 | 223 | @Override 224 | public boolean getRemoveClippedSubviews() { 225 | return mRemoveClippedSubviews; 226 | } 227 | 228 | @Override 229 | public void updateClippingRect() { 230 | if (!mRemoveClippedSubviews) { 231 | return; 232 | } 233 | 234 | Assertions.assertNotNull(mClippingRect); 235 | 236 | ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); 237 | View contentView = getChildAt(0); 238 | if (contentView instanceof ReactClippingViewGroup) { 239 | ((ReactClippingViewGroup) contentView).updateClippingRect(); 240 | } 241 | } 242 | 243 | @Override 244 | public void getClippingRect(Rect outClippingRect) { 245 | outClippingRect.set(Assertions.assertNotNull(mClippingRect)); 246 | } 247 | 248 | @Override 249 | public void fling(int velocityY) { 250 | if (mScroller != null) { 251 | // FB SCROLLVIEW CHANGE 252 | 253 | // We provide our own version of fling that uses a different call to the standard OverScroller 254 | // which takes into account the possibility of adding new content while the ScrollView is 255 | // animating. Because we give essentially no max Y for the fling, the fling will continue as long 256 | // as there is content. See #onOverScrolled() to see the second part of this change which properly 257 | // aborts the scroller animation when we get to the bottom of the ScrollView content. 258 | 259 | int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 260 | 261 | mScroller.fling( 262 | getScrollX(), 263 | getScrollY(), 264 | 0, 265 | velocityY, 266 | 0, 267 | 0, 268 | 0, 269 | Integer.MAX_VALUE, 270 | 0, 271 | scrollWindowHeight / 2); 272 | 273 | postInvalidateOnAnimation(); 274 | 275 | // END FB SCROLLVIEW CHANGE 276 | } else { 277 | super.fling(velocityY); 278 | } 279 | 280 | if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) { 281 | mFlinging = true; 282 | enableFpsListener(); 283 | ReactScrollViewHelper.emitScrollMomentumBeginEvent(this); 284 | Runnable r = new Runnable() { 285 | @Override 286 | public void run() { 287 | if (mDoneFlinging) { 288 | mFlinging = false; 289 | disableFpsListener(); 290 | ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this); 291 | } else { 292 | mDoneFlinging = true; 293 | RNScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY); 294 | } 295 | } 296 | }; 297 | postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY); 298 | } 299 | } 300 | 301 | private void enableFpsListener() { 302 | if (isScrollPerfLoggingEnabled()) { 303 | Assertions.assertNotNull(mFpsListener); 304 | Assertions.assertNotNull(mScrollPerfTag); 305 | mFpsListener.enable(mScrollPerfTag); 306 | } 307 | } 308 | 309 | private void disableFpsListener() { 310 | if (isScrollPerfLoggingEnabled()) { 311 | Assertions.assertNotNull(mFpsListener); 312 | Assertions.assertNotNull(mScrollPerfTag); 313 | mFpsListener.disable(mScrollPerfTag); 314 | } 315 | } 316 | 317 | private boolean isScrollPerfLoggingEnabled() { 318 | return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty(); 319 | } 320 | 321 | private int getMaxScrollY() { 322 | int contentHeight = mContentView.getHeight(); 323 | int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 324 | return Math.max(0, contentHeight - viewportHeight); 325 | } 326 | 327 | @Override 328 | public void draw(Canvas canvas) { 329 | if (mEndFillColor != Color.TRANSPARENT) { 330 | final View content = getChildAt(0); 331 | if (mEndBackground != null && content != null && content.getBottom() < getHeight()) { 332 | mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight()); 333 | mEndBackground.draw(canvas); 334 | } 335 | } 336 | super.draw(canvas); 337 | } 338 | 339 | public void setEndFillColor(int color) { 340 | if (color != mEndFillColor) { 341 | mEndFillColor = color; 342 | mEndBackground = new ColorDrawable(mEndFillColor); 343 | } 344 | } 345 | 346 | @Override 347 | protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 348 | if (mScroller != null) { 349 | // FB SCROLLVIEW CHANGE 350 | 351 | // This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for 352 | // more information. 353 | 354 | if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) { 355 | int scrollRange = getMaxScrollY(); 356 | if (scrollY >= scrollRange) { 357 | mScroller.abortAnimation(); 358 | scrollY = scrollRange; 359 | } 360 | } 361 | 362 | // END FB SCROLLVIEW CHANGE 363 | } 364 | 365 | super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); 366 | } 367 | 368 | @Override 369 | public void onChildViewAdded(View parent, View child) { 370 | mContentView = child; 371 | mContentView.addOnLayoutChangeListener(this); 372 | } 373 | 374 | @Override 375 | public void onChildViewRemoved(View parent, View child) { 376 | mContentView.removeOnLayoutChangeListener(this); 377 | mContentView = null; 378 | } 379 | 380 | /** 381 | * Called when a mContentView's layout has changed. Fixes the scroll position if it's too large 382 | * after the content resizes. Without this, the user would see a blank ScrollView when the scroll 383 | * position is larger than the ScrollView's max scroll position after the content shrinks. 384 | */ 385 | @Override 386 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 387 | if (mContentView == null) { 388 | return; 389 | } 390 | 391 | int currentScrollY = getScrollY(); 392 | int maxScrollY = getMaxScrollY(); 393 | if (currentScrollY > maxScrollY) { 394 | scrollTo(getScrollX(), maxScrollY); 395 | } 396 | } 397 | 398 | public void setBackgroundColor(int color) { 399 | if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) { 400 | // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background 401 | } else { 402 | getOrCreateReactViewBackground().setColor(color); 403 | } 404 | } 405 | 406 | public void setBorderWidth(int position, float width) { 407 | getOrCreateReactViewBackground().setBorderWidth(position, width); 408 | } 409 | 410 | public void setBorderColor(int position, float color, float alpha) { 411 | getOrCreateReactViewBackground().setBorderColor(position, color, alpha); 412 | } 413 | 414 | public void setBorderRadius(float borderRadius) { 415 | getOrCreateReactViewBackground().setRadius(borderRadius); 416 | } 417 | 418 | public void setBorderRadius(float borderRadius, int position) { 419 | getOrCreateReactViewBackground().setRadius(borderRadius, position); 420 | } 421 | 422 | public void setBorderStyle(@Nullable String style) { 423 | getOrCreateReactViewBackground().setBorderStyle(style); 424 | } 425 | 426 | private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { 427 | if (mReactBackgroundDrawable == null) { 428 | mReactBackgroundDrawable = new ReactViewBackgroundDrawable(); 429 | Drawable backgroundDrawable = getBackground(); 430 | super.setBackground(null); // required so that drawable callback is cleared before we add the 431 | // drawable back as a part of LayerDrawable 432 | if (backgroundDrawable == null) { 433 | super.setBackground(mReactBackgroundDrawable); 434 | } else { 435 | LayerDrawable layerDrawable = 436 | new LayerDrawable(new Drawable[]{mReactBackgroundDrawable, backgroundDrawable}); 437 | super.setBackground(layerDrawable); 438 | } 439 | } 440 | return mReactBackgroundDrawable; 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/view-paged/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Animated, 4 | AnimatedView, 5 | Easing, 6 | Style, 7 | View, 8 | StaticContainer 9 | } from '../components' 10 | import { size, find, findLast, mergeStyle, getMergeObject } from '../utils' 11 | import { IProps } from '../prop-types' 12 | import ViewPagedPlatform from './view-paged' 13 | 14 | // position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 15 | // const initialStyle = { flex: 1, display: 'flex', backgroundColor: 'transparent' } 16 | const longSwipesMs = 300 17 | const flexDirectionMap = { 18 | top: 'column', 19 | bottom: 'column-reverse', 20 | left: 'row', 21 | right: 'row-reverse' 22 | } 23 | 24 | const SceneComponent = ({ shouldUpdated, children, ...otherProps }) => { 25 | return ( 26 | 27 | {children} 28 | 29 | ) 30 | } 31 | 32 | type Listener = (params: { value: number }) => void 33 | 34 | // @Connect 35 | export default class ViewPaged extends ViewPagedPlatform { 36 | _lastPos: number 37 | 38 | _posListener: Set 39 | 40 | _autoPlayTimer: number 41 | 42 | currentPage: number 43 | 44 | _addIndexs: number[] 45 | 46 | _touchSartTime: number 47 | 48 | _boxSize: number 49 | 50 | _prevPage: number 51 | 52 | Styles: Record 53 | 54 | constructor(props: IProps) { 55 | super(props) 56 | const { infinite } = props 57 | this._posPage = this.getCheckInitialPage() 58 | if (infinite) { 59 | this._posPage += 1 60 | } 61 | // 真正的初始页 62 | this._initialPage = this._posPage 63 | const pos = new Animated.Value(0) 64 | this._posListener = this._saveListener(pos) 65 | 66 | this.state = { 67 | width: 0, 68 | height: 0, 69 | pos, 70 | isReady: false, 71 | loadIndex: [this._initialPage] 72 | } 73 | 74 | this._lastPos = 0 75 | this._autoPlayTimer = null 76 | this.currentPage = this._getCurrnetPage() 77 | this._addIndexs = [] 78 | } 79 | 80 | componentDidMount() { 81 | if (super.componentDidMount) { 82 | super.componentDidMount() 83 | } 84 | this.autoPlay() 85 | } 86 | 87 | // 检验纠正初始页参数 88 | getCheckInitialPage() { 89 | let { initialPage } = this.props 90 | if (initialPage < 0) { 91 | initialPage = 0 92 | } else if (initialPage >= this._childrenSize) { 93 | initialPage = this._childrenSize - 1 94 | } 95 | 96 | return initialPage 97 | } 98 | 99 | autoPlay() { 100 | const { autoPlay, autoPlaySpeed } = this.props 101 | if (autoPlay) { 102 | this._clearAutoPlayTimer() 103 | // 排除第0页时重置操作,否则更新pos至下次连续切换 104 | if (this._posPage && this._resetLastPos()) { 105 | this._updateAnimatedValue(this._lastPos) 106 | } 107 | 108 | this._autoPlayTimer = setTimeout(() => { 109 | const nextPosPage = this._getNextPosPage() 110 | this._goToPage(nextPosPage) 111 | // this.autoPlay() 112 | }, autoPlaySpeed) 113 | } 114 | } 115 | 116 | // 计算下一索引,针对无限滚动 117 | _getNextPosPage() { 118 | const { infinite } = this.props 119 | const nextPosPage = this._posPage + 1 120 | if (nextPosPage > this.childrenSize - 1) { 121 | return infinite ? 2 : 0 122 | } 123 | 124 | return nextPosPage 125 | } 126 | 127 | _getCurrnetPage() { 128 | return this._getCurrentPageForPosPage(this._posPage) 129 | } 130 | 131 | _getCurrentPageForPosPage(posPage) { 132 | const { infinite } = this.props 133 | if (infinite) { 134 | switch (posPage) { 135 | case 0: 136 | return this.childrenSize - 3 137 | case this.childrenSize - 1: 138 | return 0 139 | default: 140 | return posPage - 1 141 | } 142 | } 143 | return posPage 144 | } 145 | 146 | _getPosPageForCurrentPage(page) { 147 | const { infinite } = this.props 148 | if (infinite) { 149 | return page + 1 150 | } 151 | return page 152 | } 153 | 154 | // 计算重制跳转页 155 | _getResetPage() { 156 | const { _posPage } = this 157 | const { infinite } = this.props 158 | if (infinite) { 159 | if (_posPage === 0) { 160 | return this._childrenSize 161 | } 162 | if (_posPage === this._childrenSize + 1) { 163 | // return _posPage - this._childrenSize 164 | return 1 165 | } 166 | } 167 | return _posPage 168 | } 169 | 170 | // 无限滚动重置滚动位置 171 | _resetLastPos() { 172 | const { _posPage } = this 173 | const page = this._getResetPage() 174 | if (page !== _posPage) { 175 | this._lastPos = this._getPosForPage(page) 176 | return true 177 | } 178 | return false 179 | } 180 | 181 | // 判断未无限滚动时是否到头不可拖动 182 | _isMoveBorder = (distance) => { 183 | const { locked, infinite } = this.props 184 | if (locked) return false 185 | if (infinite) return infinite 186 | 187 | if (distance > 0 && this.currentPage !== 0) return true 188 | if (distance < 0 && this.currentPage + 1 !== this.childrenSize) return true 189 | 190 | return false 191 | } 192 | 193 | _TouchStartEvent() { 194 | if (!this.state.isReady) return 195 | 196 | this._touchSartTime = Date.now() 197 | 198 | this._clearAutoPlayTimer() 199 | } 200 | 201 | _TouchMoveEvent(touchState) { 202 | if (!this.state.isReady) return 203 | const { isMovingRender } = this.props 204 | 205 | this._resetLastPos() 206 | const distance = this._getDistance(touchState) 207 | const nextValue = this._lastPos + distance 208 | if (isMovingRender) { 209 | // 预加载页数 210 | const posPage = this._getPageForPos(distance, nextValue) 211 | this._onChange(posPage, true, false) 212 | } 213 | 214 | this._updateAnimatedValue(nextValue) 215 | } 216 | 217 | _TouchEndEvent(touchState) { 218 | this._addIndexs = [] 219 | if (!this.state.isReady) return 220 | 221 | const distance = this._getDistance(touchState) 222 | const { _boxSize, _touchSartTime } = this 223 | const judgeSize = _boxSize / 3 224 | const touchEndTime = Date.now() 225 | 226 | // 检测边界拖动 227 | if (this._isMoveBorder(distance)) { 228 | const diffTime = touchEndTime - _touchSartTime 229 | // 满足移动跳转下一页条件 230 | if ( 231 | (diffTime <= longSwipesMs || Math.abs(distance) >= judgeSize) && 232 | distance !== 0 233 | ) { 234 | this._lastPos += distance 235 | } 236 | 237 | // 重置或跳转下一页 238 | const posPage = this._getPageForPos(distance) 239 | this._goToPage(posPage, true) 240 | } 241 | 242 | // this.autoPlay() 243 | } 244 | 245 | // 对外提供跳转页数,检验页数正确性 246 | goToPage = (page) => { 247 | if (page < 0 || page > this._childrenSize - 1) { 248 | return 249 | } 250 | 251 | this._clearAutoPlayTimer() 252 | const posPage = this._getPosPageForCurrentPage(page) 253 | // RN scrollView走自己的处理方法 254 | if (this._isScrollView) { 255 | this._scrollToPage(posPage) 256 | } else { 257 | this._goToPage(posPage) 258 | } 259 | } 260 | 261 | // 对内提供跳转页数,传入定位的页数 262 | _goToPage(posPage: number, hasAnimation?: boolean) { 263 | if (!this.state.isReady) return 264 | 265 | this._posPage = posPage 266 | // 使用传入的下一页值,非计算的下一页值,无限滚动懒加载用 267 | this._lastPos = this._getPosForPage(this._posPage) 268 | // 处理切换动画 269 | this._updateAnimatedQueue(hasAnimation) 270 | const nextPage = this._getCurrnetPage() 271 | // 没有跳转页仅仅重置动画return处理,只有1页的除外,让1页也可以无限循环 272 | if (this._childrenSize > 1 && +nextPage === +this.currentPage) return 273 | 274 | this._onChange() 275 | } 276 | 277 | _onChange(_posPage = this._posPage, isDiff = false, isOnChange = true) { 278 | const { loadIndex: oldLoadIndex } = this.state 279 | const loadIndex = oldLoadIndex.slice() 280 | 281 | this._prevPage = this.currentPage 282 | this.currentPage = this._getCurrnetPage() 283 | if (loadIndex.indexOf(_posPage) === -1) { 284 | loadIndex.push(_posPage) 285 | this._addIndexs.push(_posPage) 286 | // 加载未重置的页 287 | const page = this._getResetPage() 288 | if (page !== this._posPage) { 289 | loadIndex.push(page) 290 | this._addIndexs.push(page) 291 | } 292 | } 293 | 294 | const { onChange } = this.props 295 | // 减少空render次数 296 | if (!isDiff || size(loadIndex) !== size(oldLoadIndex)) { 297 | this.setState({ loadIndex }, () => { 298 | if (isOnChange) { 299 | onChange && onChange(this.currentPage, this._prevPage) 300 | } 301 | }) 302 | } else { 303 | if (isOnChange) { 304 | onChange && onChange(this.currentPage, this._prevPage) 305 | } 306 | this._addIndexs = [] 307 | } 308 | } 309 | 310 | // move时设置动画值 311 | _updateAnimatedValue(nextValue) { 312 | const { infinite } = this.props 313 | if (!infinite) { 314 | // 回弹限制 315 | if (nextValue <= 0 && nextValue >= this._getMaxPos()) { 316 | this.state.pos.setValue(nextValue) 317 | } 318 | } else { 319 | this.state.pos.setValue(nextValue) 320 | } 321 | } 322 | 323 | _updateAnimatedQueue(hasAnimation: boolean = this.props.hasAnimation) { 324 | const { duration } = this.props 325 | const { pos } = this.state 326 | const animations = [] 327 | const toValue = this._lastPos 328 | 329 | if (hasAnimation) { 330 | animations.push( 331 | Animated.timing(pos, { 332 | toValue, 333 | duration, 334 | useNativeDriver: false, 335 | easing: Easing.out(Easing.ease) // default 336 | }) 337 | ) 338 | this._clearAutoPlayTimer() 339 | Animated.parallel(animations).start(() => { 340 | // 所有类型的动画结束后都启用下次的自动播放,其他地方只用关心何时关闭循环 341 | this.autoPlay() 342 | }) 343 | } else { 344 | this.state.pos.setValue(toValue) 345 | } 346 | } 347 | 348 | _getPosForPage(page) { 349 | return -page * this._boxSize 350 | } 351 | 352 | _getPageForPos(distance, _lastPos = this._lastPos) { 353 | const pageDecimal = Math.abs(_lastPos / this._boxSize) 354 | let page 355 | 356 | if (distance < 0) { 357 | page = Math.ceil(pageDecimal) 358 | } else { 359 | page = Math.floor(pageDecimal) 360 | } 361 | 362 | if (page < 0) { 363 | page = 0 364 | } else if (page > this.childrenSize - 1) { 365 | page = this.childrenSize - 1 366 | } 367 | 368 | return page 369 | } 370 | 371 | // 无限轮播拼接children 372 | getInfiniteChildren = () => { 373 | const head = findLast(this.childrenList, (child) => !!child) 374 | const foot = find(this.childrenList, (child) => !!child) 375 | 376 | return [ 377 | React.cloneElement(head, { key: 'page-head' }), 378 | ...this.childrenList, 379 | React.cloneElement(foot, { key: 'page-foot' }) 380 | ] 381 | } 382 | 383 | _clearAutoPlayTimer() { 384 | clearTimeout(this._autoPlayTimer) 385 | } 386 | 387 | componentWillUnmount() { 388 | this._clearAutoPlayTimer() 389 | } 390 | 391 | _checkRenderComponent(key, props = {}) { 392 | const Component = this.props[key] 393 | if (Component) { 394 | const element = Component(props) || null 395 | // 使用cloneElement防止重复创建组件 396 | return element && React.cloneElement(element, { key }) 397 | // return ( 398 | // 399 | // ) 400 | } 401 | return null 402 | } 403 | 404 | _renderPropsComponent(key) { 405 | const { width, height, pos } = this.state 406 | const { vertical } = this.props 407 | 408 | return this._checkRenderComponent(key, { 409 | activeTab: this.currentPage, 410 | goToPage: this.goToPage, 411 | vertical, 412 | width, 413 | height, 414 | pos 415 | }) 416 | } 417 | 418 | _saveListener(animatedValue) { 419 | const listeners = new Set() 420 | const { addListener } = animatedValue 421 | animatedValue.addListener = (listener) => { 422 | let wrapListener = listener 423 | if (!this._isScrollView) { 424 | wrapListener = (params) => { 425 | // 模拟scrollView取反给正向值 426 | const value = -params.value 427 | listener({ value }) 428 | } 429 | } 430 | listeners.add(wrapListener) 431 | addListener.call(animatedValue, wrapListener) 432 | } 433 | 434 | return listeners 435 | } 436 | 437 | _restoreListener(animatedValue, listeners) { 438 | listeners.forEach((listener) => { 439 | animatedValue.addListener(listener) 440 | }) 441 | } 442 | 443 | _getMaxPos() { 444 | return -(this.childrenSize - 1) * this._boxSize 445 | } 446 | 447 | _runMeasurements(width, height) { 448 | const { vertical } = this.props 449 | 450 | this._boxSize = vertical ? height : width 451 | // this._maxPos = -(this.childrenSize - 1) * this._boxSize 452 | this._lastPos = this._getPosForPage(this._posPage) 453 | const pos = new Animated.Value(this._lastPos) 454 | // 恢复pos监听的回掉 455 | this._restoreListener(pos, this._posListener) 456 | 457 | const initialState = { 458 | isReady: true, 459 | width, 460 | height, 461 | pos 462 | } 463 | 464 | this.setState(initialState) 465 | return initialState 466 | } 467 | 468 | // @ts-ignore 469 | _getStyles(isClearCache?: boolean) { 470 | if (isClearCache) this.Styles = null 471 | if (this.Styles) return this.Styles 472 | 473 | const { 474 | props: { vertical, renderPosition, style }, 475 | state: { isReady, width, height }, 476 | _boxSize 477 | } = this 478 | const flexDirection = 479 | flexDirectionMap[renderPosition] || flexDirectionMap.top 480 | let commonStyle: Record = { 481 | containerStyle: { 482 | flexDirection 483 | } 484 | } 485 | 486 | if (vertical) { 487 | commonStyle = { 488 | ...commonStyle, 489 | wrapStyle: { flexDirection: 'column' }, 490 | AnimatedStyle: { flexDirection: 'column' }, 491 | pageStyle: { height: _boxSize, width } 492 | } 493 | } else { 494 | commonStyle = { 495 | ...commonStyle, 496 | wrapStyle: { flexDirection: 'row' }, 497 | AnimatedStyle: { flexDirection: 'row' }, 498 | pageStyle: { width: _boxSize, height } 499 | } 500 | } 501 | 502 | const mergeStyles = getMergeObject(commonStyle, super._getStyles()) 503 | const Styles = getMergeObject(Style, mergeStyles) 504 | 505 | if (isReady) { 506 | Styles.containerStyle = mergeStyle(style, Styles.containerStyle) 507 | } else { 508 | // 不需要设置initialStyle,在android上会造成setState后不展示子视图的问题 509 | // Style.wrapStyle = initialStyle 510 | // Style.AnimatedStyle = initialStyle 511 | Styles.pageStyle = { 512 | flex: 1, 513 | display: 'flex', 514 | overflow: 'hidden' 515 | } 516 | } 517 | 518 | this.Styles = Styles 519 | 520 | return this.Styles 521 | } 522 | 523 | _shouldRenderPage(index) { 524 | const { preRenderRange } = this.props 525 | const hasRange = 526 | index < this._posPage + preRenderRange + 1 && 527 | index > this._posPage - preRenderRange - 1 528 | const hasIndex = this._addIndexs.includes(index) 529 | const isUpdate = hasRange || hasIndex 530 | 531 | return isUpdate 532 | } 533 | 534 | _renderPage() { 535 | const { isReady, loadIndex } = this.state 536 | const { pageStyle } = this._getStyles() 537 | 538 | return this.childrenList.map((page, index) => { 539 | const isRender = loadIndex.includes(index) 540 | let style = pageStyle 541 | if (!isReady && !isRender) { 542 | style = {} 543 | } 544 | 545 | return ( 546 | 551 | {isRender ? page : null} 552 | 553 | ) 554 | }) 555 | } 556 | 557 | _renderContent() { 558 | let superRender = null 559 | if (super._renderContent) { 560 | superRender = super._renderContent() 561 | if (superRender) return superRender 562 | } 563 | const { AnimatedStyle } = this._getStyles() 564 | 565 | return ( 566 | 567 | {this._renderPage()} 568 | 569 | ) 570 | } 571 | 572 | render() { 573 | this.setChildrenAttr() 574 | const { infinite } = this.props 575 | const { isReady } = this.state 576 | 577 | if (infinite) { 578 | this.childrenList = this.getInfiniteChildren() 579 | this.childrenSize = size(this.childrenList) 580 | } 581 | const { containerStyle, wrapStyle } = this._getStyles(true) 582 | 583 | return ( 584 | 585 | {this._renderPropsComponent('renderHeader')} 586 | 587 | {this._renderContent()} 588 | 589 | {this._renderPropsComponent('renderFooter')} 590 | 591 | ) 592 | } 593 | } 594 | --------------------------------------------------------------------------------