├── . npmignore ├── README.md ├── android ├── build.gradle ├── index.js └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── heng │ └── wheel │ ├── InertiaTimerTask.java │ ├── LoopView.java │ ├── LoopViewGestureListener.java │ ├── MessageHandler.java │ ├── OnItemSelectedListener.java │ ├── OnItemSelectedRunnable.java │ ├── SmoothScrollTimerTask.java │ ├── WheelPackage.java │ └── WheelViewManager.java ├── img ├── 1.jpg ├── 2.jpg └── 3.jpg ├── index.js └── package.json /. npmignore: -------------------------------------------------------------------------------- 1 | img 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PLEASE NOTE, THIS PROJECT IS NO LONGER BEING MAINTAINED 2 | 3 | * * * 4 | 5 | # react-native-wheel [![npm version](https://img.shields.io/npm/v/react-native-wheel.svg?style=flat-square)](https://www.npmjs.com/package/react-native-wheel) Dependency Status 6 | RN的Android滚轮组件 7 | 8 | ### [Combination use](https://github.com/beefe/react-native-picker) 9 | 10 | ## PropTypes 11 | * values 数据源(支持 String int double boolean) 12 | * isLoop 是否循环滚动 13 | * textSize 字体大小 14 | * selectedIndex 默认选中的下标 15 | * velocityFling 滚动速度,建议 15-25 16 | * onItemChange 滚动回调 17 | 18 | ## Install And Use 19 | 20 | #### Npm Install 21 | 22 | ```shell 23 | $ npm install --save react-native-wheel 24 | ``` 25 | 26 | #### Update Gradle Settings 27 | ```gradle 28 | // file: android/settings.gradle 29 | ... 30 | include ':react-native-wheel' 31 | project(':react-native-wheel').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-wheel/android') 32 | ``` 33 | 34 | #### Update app Gradle Build 35 | 36 | ```gradle 37 | // file: android/app/build.gradle 38 | ... 39 | 40 | dependencies { 41 | ... 42 | compile project(':react-native-wheel') 43 | } 44 | ``` 45 | 46 | #### Register React Package 47 | 48 | ```java 49 | // file: android/src/main/java/com.xx/MainApplication.java 50 | ... 51 | 52 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 53 | @Override 54 | protected boolean getUseDeveloperSupport() { 55 | return BuildConfig.DEBUG; 56 | } 57 | 58 | @Override 59 | protected List getPackages() { 60 | return Arrays.asList( 61 | new MainReactPackage(), 62 | new WheelPackage() // Added there 63 | ); 64 | } 65 | }; 66 | 67 | ``` 68 | 69 | #### Use 70 | 71 | ```js 72 | 73 | import React, { Component } from 'react'; 74 | import { 75 | AppRegistry, 76 | StyleSheet, 77 | Text, 78 | View, 79 | ToastAndroid 80 | } from 'react-native'; 81 | 82 | import WheelView from 'react-native-wheel'; 83 | 84 | import Dimensions from 'Dimensions'; 85 | 86 | let SCREEN_WIDTH = Dimensions.get('window').width; 87 | let SCREEN_HEIGHT = Dimensions.get('window').height; 88 | 89 | 90 | let wheelData = [1,'two',false,0.10,'six','seven','eight','nine','ten']; 91 | 92 | let currentIndex; 93 | 94 | class AwesomeProject extends Component { 95 | ok(){ 96 | ToastAndroid.show('select index : ' + currentIndex +' select item : ' + wheelData[currentIndex] ,ToastAndroid.SHORT); 97 | } 98 | _onItemChange(index){ 99 | currentIndex = index; 100 | } 101 | render() { 102 | return ( 103 | 104 | 105 | 确定 106 | 107 | 116 | 117 | ); 118 | } 119 | }; 120 | 121 | var styles = StyleSheet.create({ 122 | container: { 123 | flex: 1, 124 | justifyContent: 'flex-end', 125 | alignItems: 'center', 126 | backgroundColor: '#F5FCFF', 127 | }, 128 | ok: { 129 | margin: 5, 130 | color: '#000000', 131 | fontSize: 18, 132 | }, 133 | wheelview: { 134 | width: SCREEN_WIDTH, 135 | height: SCREEN_HEIGHT/5*2, 136 | }, 137 | }); 138 | 139 | AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); 140 | 141 | ``` 142 | 143 | ## Run Renderings 144 | ![1](/img/1.jpg) 145 | ![2](/img/2.jpg) 146 | ![3](/img/3.jpg) 147 | 148 | ## Reference 149 | https://github.com/weidongjian/androidWheelView 150 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 16 9 | targetSdkVersion 22 10 | } 11 | } 12 | 13 | dependencies { 14 | compile 'com.facebook.react:react-native:0.20.1' 15 | } 16 | -------------------------------------------------------------------------------- /android/index.js: -------------------------------------------------------------------------------- 1 | import React,{ 2 | Component, 3 | PropTypes 4 | } from 'react'; 5 | 6 | import ReactNative,{ 7 | requireNativeComponent 8 | } from 'react-native'; 9 | 10 | 11 | let NativeWheelView = requireNativeComponent('RCTWheelView',WheelView); 12 | 13 | 14 | export default class WheelView extends React.Component{ 15 | static propTypes = { 16 | onItemChange: PropTypes.func, 17 | values: PropTypes.array, 18 | isLoop: PropTypes.bool, 19 | selectedIndex: PropTypes.number, 20 | textSize: PropTypes.number, 21 | itemsVisible: PropTypes.number, 22 | velocityFling: PropTypes.number, 23 | }; 24 | 25 | _onItemChange(event) { 26 | if(this.props.onItemChange){ 27 | this.props.onItemChange(event.nativeEvent.index); 28 | } 29 | }; 30 | 31 | render(){ 32 | return ; 33 | } 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/InertiaTimerTask.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import java.util.TimerTask; 4 | 5 | class InertiaTimerTask extends TimerTask { 6 | 7 | float a; 8 | final float velocityY; 9 | final LoopView loopView; 10 | 11 | InertiaTimerTask(LoopView loopview, float velocityY) { 12 | super(); 13 | loopView = loopview; 14 | this.velocityY = velocityY; 15 | a = Integer.MAX_VALUE; 16 | } 17 | 18 | @Override 19 | public final void run() { 20 | if (a == Integer.MAX_VALUE) { 21 | if (Math.abs(velocityY) > 2000F) { 22 | if (velocityY > 0.0F) { 23 | a = 2000F; 24 | } else { 25 | a = -2000F; 26 | } 27 | } else { 28 | a = velocityY; 29 | } 30 | } 31 | if (Math.abs(a) >= 0.0F && Math.abs(a) <= 20F) { 32 | loopView.cancelFuture(); 33 | loopView.handler.sendEmptyMessage(MessageHandler.WHAT_SMOOTH_SCROLL); 34 | return; 35 | } 36 | int i = (int) ((a * 10F) / 1000F); 37 | LoopView loopview = loopView; 38 | loopview.totalScrollY = loopview.totalScrollY - i; 39 | if (!loopView.isLoop) { 40 | float itemHeight = loopView.lineSpacingMultiplier * loopView.maxTextHeight; 41 | if (loopView.totalScrollY <= (int) ((float) (-loopView.selectedIndex) * itemHeight)) { 42 | a = 40F; 43 | loopView.totalScrollY = (int) ((float) (-loopView.selectedIndex) * itemHeight); 44 | } else if (loopView.totalScrollY >= (int) ((float) (loopView.items.size() - 1 - loopView.selectedIndex) * itemHeight)) { 45 | loopView.totalScrollY = (int) ((float) (loopView.items.size() - 1 - loopView.selectedIndex) * itemHeight); 46 | a = -40F; 47 | } 48 | } 49 | if (a < 0.0F) { 50 | a = a + 20F; 51 | } else { 52 | a = a - 20F; 53 | } 54 | loopView.handler.sendEmptyMessage(MessageHandler.WHAT_INVALIDATE_LOOP_VIEW); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/LoopView.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Rect; 7 | import android.graphics.RectF; 8 | import android.graphics.Typeface; 9 | import android.os.Handler; 10 | import android.view.GestureDetector; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | 14 | import com.facebook.react.bridge.Arguments; 15 | import com.facebook.react.bridge.ReactContext; 16 | import com.facebook.react.bridge.WritableMap; 17 | import com.facebook.react.uimanager.events.RCTEventEmitter; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.ScheduledFuture; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | public class LoopView extends View { 26 | 27 | public enum ACTION { 28 | CLICK, FLING, DRAG 29 | } 30 | 31 | Context context; 32 | 33 | Handler handler; 34 | private GestureDetector gestureDetector; 35 | OnItemSelectedListener onItemSelectedListener; 36 | 37 | ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor(); 38 | private ScheduledFuture mFuture; 39 | 40 | Paint paintOuterText; 41 | Paint paintCenterText; 42 | Paint paintIndicator; 43 | 44 | List items; 45 | 46 | int maxTextWidth; 47 | 48 | int maxTextHeight; 49 | int colorGray = 0xffafafaf; 50 | int colorBlack = 0xff313131; 51 | 52 | int colorLightGray = 0xffc5c5c5; 53 | 54 | float lineSpacingMultiplier; 55 | 56 | 57 | int firstLineY; 58 | int secondLineY; 59 | 60 | int totalScrollY; 61 | int selectedItem; 62 | int preCurrentIndex; 63 | int change; 64 | 65 | final boolean DEFAULT_IS_LOOP = true; 66 | 67 | final int DEFAULT_VISIBLE_COUNT = 9; 68 | 69 | final int DEFAULT_SELECTED_INDEX = 0; 70 | 71 | final int DEFAULT_TEXT_SIZE = 21; 72 | 73 | boolean isLoop = DEFAULT_IS_LOOP; 74 | 75 | int itemsVisible = DEFAULT_VISIBLE_COUNT; 76 | 77 | int selectedIndex; 78 | 79 | int textSize; 80 | 81 | int mViewHeight; 82 | int mViewWidth; 83 | 84 | int halfCircumference; 85 | int radius; 86 | 87 | private int mOffset = 0; 88 | private float previousY; 89 | long startTime = 0; 90 | 91 | private int mVelocityFling; 92 | 93 | public LoopView(Context context) { 94 | super(context); 95 | initLoopView(context); 96 | } 97 | 98 | private void initLoopView(Context context) { 99 | this.context = context; 100 | handler = new MessageHandler(this); 101 | gestureDetector = new GestureDetector(context, new LoopViewGestureListener(this)); 102 | gestureDetector.setIsLongpressEnabled(false); 103 | 104 | lineSpacingMultiplier = 2.0F; 105 | 106 | totalScrollY = 0; 107 | selectedIndex = DEFAULT_SELECTED_INDEX; 108 | 109 | initPaints(); 110 | 111 | setTextSize(DEFAULT_TEXT_SIZE); 112 | 113 | mVelocityFling = 20; 114 | } 115 | 116 | private void initPaints() { 117 | paintOuterText = new Paint(); 118 | paintOuterText.setColor(colorGray); 119 | paintOuterText.setAntiAlias(true); 120 | paintOuterText.setTextAlign(Paint.Align.CENTER); 121 | paintOuterText.setTypeface(Typeface.MONOSPACE); 122 | 123 | paintCenterText = new Paint(); 124 | paintCenterText.setColor(colorBlack); 125 | paintCenterText.setAntiAlias(true); 126 | paintCenterText.setTextScaleX(1.05F); 127 | paintCenterText.setTextAlign(Paint.Align.CENTER); 128 | paintCenterText.setTypeface(Typeface.MONOSPACE); 129 | 130 | paintIndicator = new Paint(); 131 | paintIndicator.setColor(colorLightGray); 132 | paintIndicator.setAntiAlias(true); 133 | 134 | if (android.os.Build.VERSION.SDK_INT >= 11) { 135 | setLayerType(LAYER_TYPE_SOFTWARE, null); 136 | } 137 | } 138 | 139 | @Override 140 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 141 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 142 | remeasure(); 143 | mViewWidth = getMeasuredWidth(); 144 | setMeasuredDimension(mViewWidth, mViewHeight); 145 | } 146 | 147 | private void remeasure() { 148 | if (items == null) { 149 | return; 150 | } 151 | 152 | measureTextWidthHeight(); 153 | 154 | halfCircumference = (int) (maxTextHeight * lineSpacingMultiplier * (itemsVisible - 1)); 155 | mViewHeight = (int) ((halfCircumference * 2) / Math.PI); 156 | radius = (int) (halfCircumference / Math.PI); 157 | firstLineY = (int) ((mViewHeight - lineSpacingMultiplier * maxTextHeight) / 2.0F); 158 | secondLineY = (int) ((mViewHeight + lineSpacingMultiplier * maxTextHeight) / 2.0F); 159 | if (selectedIndex == -1) { 160 | if (isLoop) { 161 | selectedIndex = (items.size() + 1) / 2; 162 | } else { 163 | selectedIndex = 0; 164 | } 165 | } 166 | 167 | preCurrentIndex = selectedIndex; 168 | } 169 | 170 | private void measureTextWidthHeight() { 171 | Rect rect = new Rect(); 172 | for (int i = 0; i < items.size(); i++) { 173 | String s1 = items.get(i); 174 | paintCenterText.getTextBounds(s1, 0, s1.length(), rect); 175 | int textWidth = rect.width(); 176 | if (textWidth > maxTextWidth) { 177 | maxTextWidth = textWidth; 178 | } 179 | int textHeight = rect.height(); 180 | if (textHeight > maxTextHeight) { 181 | maxTextHeight = textHeight; 182 | } 183 | } 184 | } 185 | 186 | @Override 187 | protected void onDraw(Canvas canvas) { 188 | if (items == null) { 189 | return; 190 | } 191 | 192 | String as[] = new String[itemsVisible]; 193 | change = (int) (totalScrollY / (lineSpacingMultiplier * maxTextHeight)); 194 | preCurrentIndex = selectedIndex + change % items.size(); 195 | if (!isLoop) { 196 | if (preCurrentIndex < 0) { 197 | preCurrentIndex = 0; 198 | } 199 | if (preCurrentIndex > items.size() - 1) { 200 | preCurrentIndex = items.size() - 1; 201 | } 202 | } else { 203 | if (preCurrentIndex < 0) { 204 | preCurrentIndex = items.size() + preCurrentIndex; 205 | } 206 | if (preCurrentIndex > items.size() - 1) { 207 | preCurrentIndex = preCurrentIndex - items.size(); 208 | } 209 | } 210 | 211 | int j2 = (int) (totalScrollY % (lineSpacingMultiplier * maxTextHeight)); 212 | // 设置as数组中每个元素的值 213 | int k1 = 0; 214 | while (k1 < itemsVisible) { 215 | int l1 = preCurrentIndex - (itemsVisible / 2 - k1); 216 | if (isLoop) { 217 | if (l1 < 0) { 218 | l1 = l1 + items.size(); 219 | } 220 | if (l1 > items.size() - 1) { 221 | l1 = l1 - items.size(); 222 | } 223 | as[k1] = items.get(l1); 224 | } else if (l1 < 0) { 225 | as[k1] = ""; 226 | } else if (l1 > items.size() - 1) { 227 | as[k1] = ""; 228 | } else { 229 | as[k1] = items.get(l1); 230 | } 231 | k1++; 232 | } 233 | canvas.drawLine(0.0F, firstLineY, mViewWidth, firstLineY, paintIndicator); 234 | canvas.drawLine(0.0F, secondLineY, mViewWidth, secondLineY, paintIndicator); 235 | int j1 = 0; 236 | while (j1 < itemsVisible) { 237 | canvas.save(); 238 | // L(弧长)=α(弧度)* r(半径) (弧度制) 239 | // 求弧度--> (L * π ) / (π * r) (弧长X派/半圆周长) 240 | float itemHeight = maxTextHeight * lineSpacingMultiplier; 241 | double radian = ((itemHeight * j1 - j2) * Math.PI) / halfCircumference; 242 | // 弧度转换成角度(把半圆以Y轴为轴心向右转90度,使其处于第一象限及第四象限 243 | float angle = (float) (90D - (radian / Math.PI) * 180D); 244 | if (angle >= 90F || angle <= -90F) { 245 | canvas.restore(); 246 | } else { 247 | int translateY = (int) (radius - Math.cos(radian) * radius - (Math.sin(radian) * maxTextHeight) / 2D); 248 | canvas.translate(0.0F, translateY); 249 | canvas.scale(1.0F, (float) Math.sin(radian)); 250 | String text = as[j1]; 251 | if (translateY <= firstLineY && maxTextHeight + translateY >= firstLineY) { 252 | // 条目经过第一条线 253 | canvas.save(); 254 | canvas.clipRect(0, 0, mViewWidth, firstLineY - translateY); 255 | canvas.drawText(text, getXY(paintOuterText)[0] , getXY(paintOuterText)[1], paintOuterText); 256 | canvas.restore(); 257 | canvas.save(); 258 | canvas.clipRect(0, firstLineY - translateY, mViewWidth, (int) (itemHeight)); 259 | canvas.drawText(text, getXY(paintCenterText)[0] , getXY(paintCenterText)[1], paintCenterText); 260 | canvas.restore(); 261 | } else if (translateY <= secondLineY && maxTextHeight + translateY >= secondLineY) { 262 | // 条目经过第二条线 263 | canvas.save(); 264 | canvas.clipRect(0, 0, mViewWidth, secondLineY - translateY); 265 | canvas.drawText(text, getXY(paintCenterText)[0] , getXY(paintCenterText)[1], paintCenterText); 266 | canvas.restore(); 267 | canvas.save(); 268 | canvas.clipRect(0, secondLineY - translateY, mViewWidth, (int) (itemHeight)); 269 | canvas.drawText(text, getXY(paintOuterText)[0] , getXY(paintOuterText)[1], paintOuterText); 270 | canvas.restore(); 271 | } else if (translateY >= firstLineY && maxTextHeight + translateY <= secondLineY) { 272 | // 中间条目 273 | canvas.clipRect(0, 0, mViewWidth, (int) (itemHeight)); 274 | canvas.drawText(text, getXY(paintCenterText)[0] , getXY(paintCenterText)[1], paintCenterText); 275 | selectedItem = items.indexOf(as[j1]); 276 | } else { 277 | // 其他条目 278 | canvas.clipRect(0, 0, mViewWidth, (int) (itemHeight)); 279 | canvas.drawText(text, getXY(paintOuterText)[0] , getXY(paintOuterText)[1], paintOuterText); 280 | } 281 | canvas.restore(); 282 | } 283 | j1++; 284 | } 285 | } 286 | 287 | private float[] getXY(Paint paint) { 288 | float [] xy = new float[2]; 289 | xy[0] = mViewWidth / 2; 290 | 291 | Rect rect = new Rect(0, 0, getWidth(), maxTextHeight); 292 | RectF bounds = new RectF(); 293 | bounds.bottom = paint.descent() - paint.ascent(); 294 | bounds.top += (rect.height() - bounds.bottom) / 2.0f; 295 | xy[1] = bounds.top - paint.ascent(); 296 | return xy; 297 | } 298 | 299 | @Override 300 | public boolean onTouchEvent(MotionEvent event) { 301 | boolean eventConsumed = gestureDetector.onTouchEvent(event); 302 | float itemHeight = lineSpacingMultiplier * maxTextHeight; 303 | switch (event.getAction()) { 304 | case MotionEvent.ACTION_DOWN: 305 | startTime = System.currentTimeMillis(); 306 | cancelFuture(); 307 | previousY = event.getRawY(); 308 | if (getParent() != null) { 309 | getParent().requestDisallowInterceptTouchEvent(true); 310 | } 311 | break; 312 | case MotionEvent.ACTION_MOVE: 313 | float dy = previousY - event.getRawY(); 314 | previousY = event.getRawY(); 315 | 316 | totalScrollY = (int) (totalScrollY + dy); 317 | 318 | if (!isLoop) { 319 | float top = -selectedIndex * itemHeight; 320 | float bottom = (items.size() - 1 - selectedIndex) * itemHeight; 321 | 322 | if (totalScrollY < top) { 323 | totalScrollY = (int) top; 324 | } else if (totalScrollY > bottom) { 325 | totalScrollY = (int) bottom; 326 | } 327 | } 328 | break; 329 | 330 | case MotionEvent.ACTION_UP: 331 | case MotionEvent.ACTION_CANCEL: 332 | default: 333 | if (!eventConsumed) { 334 | float y = event.getY(); 335 | double l = Math.acos((radius - y) / radius) * radius; 336 | int circlePosition = (int) ((l + itemHeight / 2) / itemHeight); 337 | 338 | float extraOffset = (totalScrollY % itemHeight + itemHeight) % itemHeight; 339 | mOffset = (int) ((circlePosition - itemsVisible / 2) * itemHeight - extraOffset); 340 | 341 | if ((System.currentTimeMillis() - startTime) > 120) { 342 | smoothScroll(ACTION.DRAG); 343 | } else { 344 | smoothScroll(ACTION.CLICK); 345 | } 346 | } 347 | if (getParent() != null) { 348 | getParent().requestDisallowInterceptTouchEvent(false); 349 | } 350 | break; 351 | } 352 | invalidate(); 353 | return true; 354 | } 355 | 356 | void smoothScroll(ACTION action) { 357 | cancelFuture(); 358 | if (action == ACTION.FLING || action == ACTION.DRAG) { 359 | float itemHeight = lineSpacingMultiplier * maxTextHeight; 360 | mOffset = (int) ((totalScrollY % itemHeight + itemHeight) % itemHeight); 361 | if ((float) mOffset > itemHeight / 2.0F) { 362 | mOffset = (int) (itemHeight - (float) mOffset); 363 | } else { 364 | mOffset = -mOffset; 365 | } 366 | } 367 | mFuture = mExecutor.scheduleWithFixedDelay(new SmoothScrollTimerTask(this, mOffset), 0, 10, TimeUnit.MILLISECONDS); 368 | } 369 | 370 | protected final void scrollBy(float velocityY) { 371 | cancelFuture(); 372 | mFuture = mExecutor.scheduleWithFixedDelay(new InertiaTimerTask(this, velocityY), 0, mVelocityFling, TimeUnit.MILLISECONDS); 373 | } 374 | 375 | public void cancelFuture() { 376 | if (mFuture != null && !mFuture.isCancelled()) { 377 | mFuture.cancel(true); 378 | mFuture = null; 379 | } 380 | } 381 | 382 | public void isLoop(boolean isLoop) { 383 | this.isLoop = isLoop; 384 | } 385 | 386 | public void setTextSize(float size) { 387 | if (size > 0.0F) { 388 | textSize = (int) (context.getResources().getDisplayMetrics().density * size); 389 | paintOuterText.setTextSize(textSize); 390 | paintCenterText.setTextSize(textSize); 391 | } 392 | } 393 | 394 | public void setItemsVisible(int itemsVisible) { 395 | this.itemsVisible = itemsVisible + 2; 396 | } 397 | 398 | public void setSelectedIndex(int selectedIndex) { 399 | this.selectedIndex = selectedIndex; 400 | } 401 | 402 | public void setVelocityFling(int velocityFling) { 403 | this.mVelocityFling = velocityFling; 404 | } 405 | 406 | public void onReceiveNativeEvent(int index) { 407 | WritableMap event = Arguments.createMap(); 408 | event.putInt("index", index); 409 | ReactContext reactContext = (ReactContext) getContext(); 410 | reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( 411 | getId(), 412 | "topChange", 413 | event); 414 | } 415 | 416 | public void setListener(OnItemSelectedListener OnItemSelectedListener) { 417 | this.onItemSelectedListener = OnItemSelectedListener; 418 | } 419 | 420 | public void setItems(List items) { 421 | this.items = items; 422 | remeasure(); 423 | invalidate(); 424 | } 425 | 426 | 427 | public final int getSelectedIndex() { 428 | return selectedItem; 429 | } 430 | 431 | public String getSelectedItem() { 432 | return items.get(selectedItem); 433 | } 434 | 435 | protected final void onItemSelected() { 436 | if (onItemSelectedListener != null) { 437 | postDelayed(new OnItemSelectedRunnable(this), 200L); 438 | } 439 | } 440 | } -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/LoopViewGestureListener.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import android.view.MotionEvent; 4 | 5 | class LoopViewGestureListener extends android.view.GestureDetector.SimpleOnGestureListener { 6 | 7 | final LoopView loopView; 8 | 9 | LoopViewGestureListener(LoopView loopview) { 10 | loopView = loopview; 11 | } 12 | 13 | @Override 14 | public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 15 | loopView.scrollBy(velocityY); 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import android.os.Handler; 4 | import android.os.Message; 5 | 6 | class MessageHandler extends Handler { 7 | 8 | public static final int WHAT_INVALIDATE_LOOP_VIEW = 1000; 9 | public static final int WHAT_SMOOTH_SCROLL = 2000; 10 | public static final int WHAT_ITEM_SELECTED = 3000; 11 | 12 | final LoopView loopview; 13 | 14 | MessageHandler(LoopView loopview) { 15 | this.loopview = loopview; 16 | } 17 | 18 | @Override 19 | public final void handleMessage(Message msg) { 20 | switch (msg.what) { 21 | case WHAT_INVALIDATE_LOOP_VIEW: 22 | loopview.invalidate(); 23 | break; 24 | case WHAT_SMOOTH_SCROLL: 25 | loopview.smoothScroll(LoopView.ACTION.FLING); 26 | break; 27 | case WHAT_ITEM_SELECTED: 28 | loopview.onItemSelected(); 29 | break; 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/OnItemSelectedListener.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | public interface OnItemSelectedListener { 4 | void onItemSelected(int index); 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/OnItemSelectedRunnable.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | class OnItemSelectedRunnable implements Runnable { 4 | final LoopView loopView; 5 | 6 | OnItemSelectedRunnable(LoopView loopview) { 7 | loopView = loopview; 8 | } 9 | 10 | @Override 11 | public final void run() { 12 | loopView.onItemSelectedListener.onItemSelected(loopView.getSelectedIndex()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/SmoothScrollTimerTask.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import java.util.TimerTask; 4 | 5 | class SmoothScrollTimerTask extends TimerTask { 6 | 7 | int realTotalOffset; 8 | int realOffset; 9 | int offset; 10 | final LoopView loopView; 11 | 12 | SmoothScrollTimerTask(LoopView loopview, int offset) { 13 | this.loopView = loopview; 14 | this.offset = offset; 15 | realTotalOffset = Integer.MAX_VALUE; 16 | realOffset = 0; 17 | } 18 | 19 | @Override 20 | public final void run() { 21 | if (realTotalOffset == Integer.MAX_VALUE) { 22 | realTotalOffset = offset; 23 | } 24 | realOffset = (int) ((float) realTotalOffset * 0.1F); 25 | 26 | if (realOffset == 0) { 27 | if (realTotalOffset < 0) { 28 | realOffset = -1; 29 | } else { 30 | realOffset = 1; 31 | } 32 | } 33 | if (Math.abs(realTotalOffset) <= 0) { 34 | loopView.cancelFuture(); 35 | loopView.handler.sendEmptyMessage(MessageHandler.WHAT_ITEM_SELECTED); 36 | } else { 37 | loopView.totalScrollY = loopView.totalScrollY + realOffset; 38 | loopView.handler.sendEmptyMessage(MessageHandler.WHAT_INVALIDATE_LOOP_VIEW); 39 | realTotalOffset = realTotalOffset - realOffset; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/WheelPackage.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 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.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by heng on 15/11/27. 15 | */ 16 | public class WheelPackage implements ReactPackage { 17 | @Override 18 | public List createNativeModules(ReactApplicationContext reactContext) { 19 | return Collections.emptyList(); 20 | } 21 | 22 | @Override 23 | public List> createJSModules() { 24 | return Collections.emptyList(); 25 | } 26 | 27 | @Override 28 | public List createViewManagers(ReactApplicationContext reactContext) { 29 | return Arrays.asList(new WheelViewManager()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/src/main/java/com/heng/wheel/WheelViewManager.java: -------------------------------------------------------------------------------- 1 | package com.heng.wheel; 2 | 3 | import com.facebook.react.bridge.ReadableArray; 4 | import com.facebook.react.uimanager.SimpleViewManager; 5 | import com.facebook.react.uimanager.ThemedReactContext; 6 | import com.facebook.react.uimanager.annotations.ReactProp; 7 | 8 | import java.util.ArrayList; 9 | 10 | /** 11 | */ 12 | public class WheelViewManager extends SimpleViewManager { 13 | 14 | public static final String REACT_CLASS = "RCTWheelView"; 15 | 16 | @Override 17 | public String getName() { 18 | return REACT_CLASS; 19 | } 20 | 21 | @Override 22 | protected LoopView createViewInstance(ThemedReactContext reactContext) { 23 | return new LoopView(reactContext); 24 | } 25 | 26 | @ReactProp(name = "values") 27 | public void setItems(LoopView view, ReadableArray values) { 28 | ArrayList items = new ArrayList<>(); 29 | for (int i = 0; i < values.size(); i++) { 30 | String type = values.getType(i).name(); 31 | switch (type) { 32 | case "Boolean": 33 | items.add(String.valueOf(values.getBoolean(i))); 34 | break; 35 | case "Number": 36 | try { 37 | items.add(String.valueOf(values.getInt(i))); 38 | } catch (Exception e) { 39 | items.add(String.valueOf(values.getDouble(i))); 40 | } 41 | break; 42 | case "String": 43 | items.add(values.getString(i)); 44 | break; 45 | } 46 | } 47 | view.setItems(items); 48 | } 49 | 50 | @ReactProp(name = "isLoop") 51 | public void isLoop(LoopView view, boolean isLoop) { 52 | view.isLoop(isLoop); 53 | } 54 | 55 | @ReactProp(name = "itemsVisible") 56 | public void setItemsVisible(LoopView view, int itemsVisible) { 57 | view.setItemsVisible(itemsVisible); 58 | } 59 | 60 | @ReactProp(name = "velocityFling") 61 | public void setVelocityFling(LoopView view, int velocityFling) { 62 | view.setVelocityFling(velocityFling); 63 | } 64 | 65 | @ReactProp(name = "selectedIndex") 66 | public void setSelectedIndex(LoopView view, int selectedIndex) { 67 | view.setSelectedIndex(selectedIndex); 68 | } 69 | 70 | @ReactProp(name = "textSize", defaultFloat = 16f) 71 | public void setTextSize(LoopView view, float textSize) { 72 | view.setTextSize(textSize); 73 | } 74 | 75 | @ReactProp(name = "onItemChange", defaultBoolean = true) 76 | public void setOnItemChange(final LoopView view, Boolean value) { 77 | view.setListener(new OnItemSelectedListener() { 78 | @Override 79 | public void onItemSelected(int index) { 80 | view.onReceiveNativeEvent(index); 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shexiaoheng/react-native-wheel/5bc9c64a5cbc1ef4e04933c48cb4dbf24d41228d/img/1.jpg -------------------------------------------------------------------------------- /img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shexiaoheng/react-native-wheel/5bc9c64a5cbc1ef4e04933c48cb4dbf24d41228d/img/2.jpg -------------------------------------------------------------------------------- /img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shexiaoheng/react-native-wheel/5bc9c64a5cbc1ef4e04933c48cb4dbf24d41228d/img/3.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React,{ 2 | Component, 3 | PropTypes 4 | } from 'react'; 5 | 6 | import ReactNative,{ 7 | requireNativeComponent 8 | } from 'react-native'; 9 | 10 | 11 | let NativeWheelView = requireNativeComponent('RCTWheelView',WheelView); 12 | 13 | 14 | export default class WheelView extends React.Component{ 15 | static propTypes = { 16 | onItemChange: PropTypes.func, 17 | values: PropTypes.array, 18 | isLoop: PropTypes.bool, 19 | selectedIndex: PropTypes.number, 20 | textSize: PropTypes.number, 21 | itemsVisible: PropTypes.number, 22 | velocityFling: PropTypes.number, 23 | }; 24 | 25 | _onItemChange(event) { 26 | if(this.props.onItemChange){ 27 | this.props.onItemChange(event.nativeEvent.index); 28 | } 29 | }; 30 | 31 | render(){ 32 | return ; 33 | } 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-wheel", 3 | "version": "1.1.3", 4 | "description": "android wheel view for react-native", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/shexiaoheng/react-native-wheel.git" 12 | }, 13 | "keywords": [ 14 | "react-native-wheel" 15 | ], 16 | "author": "heng", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/shexiaoheng/react-native-wheel/issues" 20 | }, 21 | "homepage": "https://github.com/shexiaoheng/react-native-wheel#readme", 22 | "dependencies": {}, 23 | "devDependencies": {} 24 | } 25 | --------------------------------------------------------------------------------