├── .gitattributes ├── .gitignore ├── Graphics ├── bottom_wheel.gif └── center_wheel.gif ├── LICENSE.txt ├── README.md ├── WheelViewLib ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lukedeighton │ │ └── wheelview │ │ ├── Circle.java │ │ ├── WheelView.java │ │ ├── adapter │ │ ├── WheelAdapter.java │ │ └── WheelArrayAdapter.java │ │ └── transformer │ │ ├── FadingSelectionTransformer.java │ │ ├── ScalingItemTransformer.java │ │ ├── SimpleItemTransformer.java │ │ ├── WheelItemTransformer.java │ │ └── WheelSelectionTransformer.java │ └── res │ └── values │ ├── attrs.xml │ └── strings.xml ├── WheelViewSample ├── .gitignore ├── build.gradle ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lukedeighton │ │ └── wheelsample │ │ ├── MainActivity.java │ │ ├── MaterialColor.java │ │ └── TextDrawable.java │ └── res │ ├── drawable-hdpi │ ├── ic_launcher.png │ └── wheel_drawable.xml │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ ├── menu │ └── main.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── material_colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Android Studio 23 | .idea/ 24 | .gradle 25 | gradle 26 | /*/local.properties 27 | /*/out 28 | /*/*/build 29 | build 30 | /*/*/production 31 | *.iml 32 | *.iws 33 | *.ipr 34 | *~ 35 | *.swp -------------------------------------------------------------------------------- /Graphics/bottom_wheel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/Graphics/bottom_wheel.gif -------------------------------------------------------------------------------- /Graphics/center_wheel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/Graphics/center_wheel.gif -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Luke Deighton 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Looking for maintainers 2 | --- 3 | 4 | I'm no longer active on this project but I'll still focus on fixing crashing issues and review any code changes etc. 5 | 6 | WheelView 7 | ========= 8 | 9 | WheelView is an Android library that allows drawables to be placed on a rotatable wheel. It behaves like a Circular ListView where items rotate rather than scroll vertically. It isn't limited by the number of items that can fit on the wheel since it will cycle through each adapter position when the wheel is rotated. It can be rotated at any angle and from any position. 10 | 11 | The WheelView can be used as a way to select one item from a list. The `SelectionAngle` determines what position on the wheel is selected. You can also receive a callback for when an item is clicked, and whether it is selected. Have a look at the sample for a working example! 12 | 13 | ![1] 14 | ![2] 15 | 16 | Note - Frame rate is much better than these poorly converted gifs! 17 | 18 | Setup 19 | ----- 20 | 21 | Include this in build.gradle project dependencies: 22 | ```groovy 23 | dependencies { 24 | compile 'com.github.lukedeighton:wheelview:0.4.2' 25 | } 26 | ``` 27 | 28 | Usage 29 | ----- 30 | 31 | 1) Add a custom view in xml 32 | ```xml 33 | 37 | 38 | 52 | 53 | ``` 54 | 55 | 2) Set a `WheelAdapter` similar to how you would set an adapter with a ListView 56 | ```java 57 | wheelView.setAdapter(new WheelAdapter() { 58 | @Override 59 | public Drawable getDrawable(int position) { 60 | //return drawable here - the position can be seen in the gifs above 61 | } 62 | 63 | @Override 64 | public int getCount() { 65 | //return the count 66 | } 67 | }); 68 | ``` 69 | 70 | Please note that the `WheelAdapter` doesn't behave exactly like a `ListAdapter` since Drawables don't need to be recycled in comparison to Views where inflation is expensive. If you need to refresh the Adapter / Items then call `setAdapter` again. 71 | 72 | Listeners 73 | --------- 74 | 75 | 1) A listener for when the closest item to the `SelectionAngle` changes. 76 | ```java 77 | wheelView.setOnWheelItemSelectedListener(new WheelView.OnWheelItemSelectListener() { 78 | @Override 79 | public void onWheelItemSelected(WheelView parent, Drawable itemDrawable, int position) { 80 | //the adapter position that is closest to the selection angle and it's drawable 81 | } 82 | }); 83 | ``` 84 | 85 | 2) A listener for when an item is clicked. 86 | ```java 87 | wheelView.setOnWheelItemClickListener(new WheelView.OnWheelItemClickListener() { 88 | @Override 89 | public void onWheelItemClick(WheelView parent, int position, boolean isSelected) { 90 | //the position in the adapter and whether it is closest to the selection angle 91 | } 92 | }); 93 | ``` 94 | 95 | 3) A listener for when the wheel's angle is changed. 96 | ```java 97 | wheelView.setOnWheelAngleChangeListener(new WheelView.OnWheelAngleChangeListener() { 98 | @Override 99 | public void onWheelAngleChange(float angle) { 100 | //the new angle of the wheel 101 | } 102 | }); 103 | ``` 104 | 105 | Attributes 106 | ---------- 107 | 108 | The WheelView is highly customisable with many attributes that can be set via xml or programmatically (recommend using xml as programmatically set attributes is half implemented at the moment). Here are the custom attributes that can be declared in xml: 109 | 110 | * wheelDrawable 111 | * wheelColor 112 | * emptyItemDrawable 113 | * emptyItemColor 114 | * selectionDrawable 115 | * selectionColor 116 | * selectionPadding 117 | * selectionAngle 118 | * repeatItems 119 | * wheelRadius 120 | * wheelItemRadius 121 | * rotatableWheelDrawable 122 | * wheelOffsetX 123 | * wheelOffsetY 124 | * wheelItemCount 125 | * wheelItemAngle 126 | * wheelToItemDistance 127 | * wheelPosition 128 | * wheelPadding 129 | * wheelItemTransformer 130 | * selectionTransformer 131 | 132 | WheelItemTransformer 133 | -------------------- 134 | 135 | Determines the draw bounds of the `WheelItem` in relation to the selection angle. 136 | 137 | * `SimpleItemTransformer` - All items are the same size 138 | * `ScalingItemTransformer` - Items grow in size near to the selection angle 139 | 140 | Future Goals 141 | ------------ 142 | 143 | Convert this project to use `LayoutManager` to replace Drawables with Views 144 | 145 | License 146 | ------- 147 | 148 | Apache License Version 2.0 149 | http://apache.org/licenses/LICENSE-2.0.txt 150 | 151 | [1]: ./Graphics/bottom_wheel.gif 152 | [2]: ./Graphics/center_wheel.gif 153 | -------------------------------------------------------------------------------- /WheelViewLib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /WheelViewLib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '28' 6 | defaultConfig { 7 | minSdkVersion 10 8 | targetSdkVersion 28 9 | versionCode 2 10 | versionName '0.4.2' 11 | } 12 | lintOptions { 13 | abortOnError false 14 | } 15 | productFlavors { 16 | } 17 | } 18 | 19 | dependencies { 20 | compile 'com.android.support:appcompat-v7:25.4.0' 21 | compile fileTree(dir: 'libs', include: ['*.jar']) 22 | } 23 | 24 | apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /WheelViewLib/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=WheelView 2 | POM_ARTIFACT_ID=wheelview 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /WheelViewLib/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Program Files (x86)/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} -------------------------------------------------------------------------------- /WheelViewLib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/Circle.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview; 2 | 3 | import android.graphics.Rect; 4 | 5 | public class Circle { 6 | float mCenterX, mCenterY; 7 | float mRadius; 8 | 9 | Circle() {} 10 | 11 | Circle(float centerX, float centerY, float radius) { 12 | mCenterX = centerX; 13 | mCenterY = centerY; 14 | mRadius = radius; 15 | } 16 | 17 | boolean contains(float x, float y) { 18 | x = mCenterX - x; 19 | y = mCenterY - y; 20 | return x * x + y * y <= mRadius * mRadius; 21 | } 22 | 23 | public float getCenterX() { 24 | return mCenterX; 25 | } 26 | 27 | public float getCenterY() { 28 | return mCenterY; 29 | } 30 | 31 | public float getRadius() { 32 | return mRadius; 33 | } 34 | 35 | Rect getBoundingRect() { 36 | return new Rect(Math.round(mCenterX - mRadius), Math.round(mCenterY - mRadius), 37 | Math.round(mCenterX + mRadius), Math.round(mCenterY + mRadius)); 38 | } 39 | 40 | /** 41 | * The Angle from this circle's center to the position x, y 42 | * y is considered to go down (like android view system) 43 | */ 44 | float angleTo(float x, float y) { 45 | return (float) Math.atan2((mCenterY - y), (x - mCenterX)); 46 | } 47 | 48 | float angleToDegrees(float x, float y) { 49 | return (float) Math.toDegrees(angleTo(x, y)); 50 | } 51 | 52 | /** 53 | * Clamps the value to a number between 0 and the upperLimit 54 | */ 55 | static int clamp(int value, int upperLimit) { 56 | if (value < 0) { 57 | return value + (-1 * (int) Math.floor(value / (float) upperLimit)) * upperLimit; 58 | } else { 59 | return value % upperLimit; 60 | } 61 | } 62 | 63 | static float clamp180(float value) { 64 | //TODO clamp(int value, int upperLimit) could use this code? + test it 65 | return (((value + 180f) % 360f + 360f) % 360f) - 180f; 66 | } 67 | 68 | /** 69 | * Returns the shortest angle difference when the inputs range between -180 and 180 (such as from Math.atan2) 70 | */ 71 | static float shortestAngle(float angleA, float angleB) { 72 | float angle = angleA - angleB; 73 | if (angle > 180f) { 74 | angle -=360f; 75 | } else if (angle < -180f) { 76 | angle += 360f; 77 | } 78 | return angle; 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return "Radius: " + mRadius + " X: " + mCenterX + " Y: " + mCenterY; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/WheelView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (C) 2014 Luke Deighton 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.lukedeighton.wheelview; 19 | 20 | import android.content.Context; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Drawable; 25 | import android.graphics.drawable.ShapeDrawable; 26 | import android.graphics.drawable.shapes.OvalShape; 27 | import android.os.SystemClock; 28 | import android.support.annotation.DrawableRes; 29 | import android.support.annotation.NonNull; 30 | import android.util.AttributeSet; 31 | import android.view.InflateException; 32 | import android.view.MotionEvent; 33 | import android.view.VelocityTracker; 34 | import android.view.View; 35 | import android.view.ViewGroup; 36 | 37 | import com.lukedeighton.wheelview.adapter.WheelAdapter; 38 | import com.lukedeighton.wheelview.transformer.FadingSelectionTransformer; 39 | import com.lukedeighton.wheelview.transformer.ScalingItemTransformer; 40 | import com.lukedeighton.wheelview.transformer.WheelItemTransformer; 41 | import com.lukedeighton.wheelview.transformer.WheelSelectionTransformer; 42 | 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | 46 | //TODO onWheelItemSelected callback for when the wheel has settled (0 angular velocity), and one when it is passed 47 | //TODO empty - physics to spring away - prevent movement out from edge 48 | //TODO sticky selection - always settle on a state that completely selects an item 49 | //TODO circular clip option? 50 | //TODO Saving State during screen rotate etc. SavedState extends BaseSavedState 51 | //TODO can items be rendered as views or use recyclerView - use viewgroup? 52 | //TODO onWheelItemVisibilityChange needs to factor in when items are cycled within view bounds and should that have another callback? 53 | //TODO option to get wheel state (either flinging or dragging) 54 | //TODO item radius works separately ? uses min angle etc. to figure out in the layout event 55 | //TODO setWheelVelocity method 56 | //TODO util method to animate to a wheel position? 57 | 58 | public class WheelView extends View { 59 | 60 | private static final Rect sTempRect = new Rect(); 61 | 62 | private static final float VELOCITY_FRICTION_COEFFICIENT = 0.015f; 63 | private static final float CONSTANT_FRICTION_COEFFICIENT = 0.0028f; 64 | private static final float ANGULAR_VEL_COEFFICIENT = 22f; 65 | private static final float MAX_ANGULAR_VEL = 0.3f; 66 | 67 | private static final int LEFT_MASK = 0x01; 68 | private static final int RIGHT_MASK = 0x02; 69 | private static final int TOP_MASK = 0x04; 70 | private static final int BOTTOM_MASK = 0x08; 71 | 72 | private static final int NEVER_USED = 0; 73 | 74 | //The touch factors decrease the drag movement towards the center of the wheel. It is there so 75 | //that dragging the wheel near the center doesn't cause the wheel's angle to change 76 | //drastically. It is squared to provide a linear function once multiplied by 1/r^2 77 | private static final int TOUCH_FACTOR_SIZE = 20; 78 | private static final float TOUCH_DRAG_COEFFICIENT = 0.8f; 79 | 80 | private static final float[] TOUCH_FACTORS; 81 | static { 82 | int size = TOUCH_FACTOR_SIZE; 83 | TOUCH_FACTORS = new float[size]; 84 | int maxIndex = size - 1; 85 | float numerator = size * size; 86 | for (int i = 0; i < size; i++) { 87 | int factor = maxIndex - i + 1; 88 | TOUCH_FACTORS[i] = (1 - factor * factor / numerator) * TOUCH_DRAG_COEFFICIENT; 89 | } 90 | } 91 | 92 | private static final float CLICK_MAX_DRAGGED_ANGLE = 0.7f; 93 | 94 | private static final CacheItem EMPTY_CACHE_ITEM = new CacheItem(true); 95 | 96 | private VelocityTracker mVelocityTracker; 97 | private Vector mForceVector = new Vector(); 98 | private Vector mRadiusVector = new Vector(); 99 | private float mAngle; 100 | private float mAngularVelocity; 101 | private long mLastUpdateTime; 102 | private boolean mRequiresUpdate; 103 | private int mRawSelectedPosition; 104 | private float mLastWheelTouchX; 105 | private float mLastWheelTouchY; 106 | 107 | private CacheItem[] mItemCacheArray; 108 | private Drawable mWheelDrawable; 109 | private Drawable mEmptyItemDrawable; 110 | private Drawable mSelectionDrawable; 111 | 112 | private boolean mIsRepeatable; 113 | private boolean mIsWheelDrawableRotatable = true; 114 | 115 | /** 116 | * The item angle is the angle covered per item on the wheel and is in degrees. 117 | * The {@link #mItemAnglePadding} is included in the item angle. 118 | */ 119 | private float mItemAngle; 120 | 121 | /** 122 | * Angle padding is in degrees and reduces the wheel's items size during layout 123 | */ 124 | private float mItemAnglePadding; 125 | 126 | /** 127 | * Selection Angle is the angle at which an item is considered selected. 128 | * The {@link #mOnItemSelectListener} is called when the 'most selected' item changes. 129 | */ 130 | private float mSelectionAngle; 131 | 132 | private int mSelectionPadding; 133 | 134 | private int mWheelPadding; 135 | private int mWheelToItemDistance; 136 | private int mItemRadius; 137 | private int mWheelRadius; 138 | private int mOffsetX; 139 | private int mOffsetY; 140 | private int mItemCount; 141 | 142 | private int mWheelPosition; 143 | private int mLeft, mTop, mWidth, mHeight; 144 | private Rect mViewBounds = new Rect(); 145 | private Circle mWheelBounds; 146 | 147 | /** 148 | * Wheel item bounds are always pre-rotation and based on the {@link #mSelectionAngle} 149 | */ 150 | private List mWheelItemBounds; 151 | 152 | /** 153 | * The ItemState contain the rotated position 154 | */ 155 | private List mItemStates; 156 | private int mAdapterItemCount; 157 | 158 | private boolean mIsDraggingWheel; 159 | private float mLastTouchAngle; 160 | private ItemState mClickedItem; 161 | private float mDraggedAngle; 162 | 163 | private OnWheelItemClickListener mOnItemClickListener; 164 | private OnWheelAngleChangeListener mOnAngleChangeListener; 165 | private OnWheelItemSelectListener mOnItemSelectListener; 166 | private OnWheelItemVisibilityChangeListener mOnItemVisibilityChangeListener; 167 | private WheelItemTransformer mItemTransformer; 168 | private WheelSelectionTransformer mSelectionTransformer; 169 | private WheelAdapter mAdapter; 170 | 171 | public WheelView(Context context) { 172 | super(context); 173 | initWheelView(); 174 | } 175 | 176 | public WheelView(Context context, AttributeSet attrs) { 177 | this(context, attrs, 0); 178 | } 179 | 180 | public WheelView(Context context, AttributeSet attrs, int defStyle) { 181 | super(context, attrs, defStyle); 182 | initWheelView(); 183 | 184 | //TODO possible pattern to follow from android source 185 | /* final int N = a.getIndexCount(); 186 | for (int i = 0; i < N; i++) { 187 | int attr = a.getIndex(i); 188 | switch (attr) { 189 | case com.android.internal.R.styleable.View_background: 190 | background = a.getDrawable(attr); 191 | break; */ 192 | 193 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WheelView, defStyle, 0); 194 | 195 | Drawable d = a.getDrawable(R.styleable.WheelView_emptyItemDrawable); 196 | if (d != null) { 197 | setEmptyItemDrawable(d); 198 | } else if (a.hasValue(R.styleable.WheelView_emptyItemColor)) { 199 | int color = a.getColor(R.styleable.WheelView_emptyItemColor, NEVER_USED); 200 | setEmptyItemColor(color); 201 | } 202 | 203 | d = a.getDrawable(R.styleable.WheelView_wheelDrawable); 204 | if (d != null) { 205 | setWheelDrawable(d); 206 | } else if (a.hasValue(R.styleable.WheelView_wheelColor)){ 207 | int color = a.getColor(R.styleable.WheelView_wheelColor, NEVER_USED); 208 | setWheelColor(color); 209 | } 210 | 211 | d = a.getDrawable(R.styleable.WheelView_selectionDrawable); 212 | if (d != null) { 213 | setSelectionDrawable(d); 214 | } else if (a.hasValue(R.styleable.WheelView_selectionColor)) { 215 | int color = a.getColor(R.styleable.WheelView_selectionColor, NEVER_USED); 216 | setSelectionColor(color); 217 | } 218 | 219 | mSelectionPadding = a.getDimensionPixelSize(R.styleable.WheelView_selectionPadding, 0); 220 | mIsRepeatable = a.getBoolean(R.styleable.WheelView_repeatItems, false); 221 | mIsWheelDrawableRotatable = a.getBoolean(R.styleable.WheelView_rotatableWheelDrawable, true); 222 | mSelectionAngle = a.getFloat(R.styleable.WheelView_selectionAngle, 0f); 223 | setWheelRadius(a.getLayoutDimension(R.styleable.WheelView_wheelRadius, 0 /* TODO Wrap_content */)); 224 | mOffsetX = a.getDimensionPixelSize(R.styleable.WheelView_wheelOffsetX, 0); 225 | mOffsetY = a.getDimensionPixelSize(R.styleable.WheelView_wheelOffsetY, 0); 226 | mWheelToItemDistance = a.getLayoutDimension(R.styleable.WheelView_wheelToItemDistance, ViewGroup.LayoutParams.MATCH_PARENT); 227 | 228 | int itemCount = a.getInteger(R.styleable.WheelView_wheelItemCount, 0); 229 | 230 | //TODO maybe just remove angle padding? 231 | mItemAnglePadding = a.getFloat(R.styleable.WheelView_wheelItemAnglePadding, 0f); //TODO angle works with the ItemRadius 232 | 233 | if (itemCount != 0) { 234 | setWheelItemCount(itemCount); 235 | } else { 236 | float itemAngle = a.getFloat(R.styleable.WheelView_wheelItemAngle, 0f); 237 | if (itemAngle != 0f) { 238 | setWheelItemAngle(itemAngle); 239 | } 240 | } 241 | 242 | mItemRadius = a.getDimensionPixelSize(R.styleable.WheelView_wheelItemRadius, 0); 243 | 244 | if (mItemCount == 0 && mWheelToItemDistance > 0 && mWheelRadius > 0) { 245 | mItemAngle = calculateAngle(mWheelRadius, mWheelToItemDistance) + mItemAnglePadding; 246 | setWheelItemAngle(mItemAngle); 247 | } 248 | 249 | String itemTransformerStr = a.getString(R.styleable.WheelView_wheelItemTransformer); 250 | if (itemTransformerStr != null) { 251 | mItemTransformer = validateAndInstantiate(itemTransformerStr, WheelItemTransformer.class); 252 | } 253 | 254 | String selectionTransformerStr = a.getString(R.styleable.WheelView_selectionTransformer); 255 | if (selectionTransformerStr != null) { 256 | mSelectionTransformer = validateAndInstantiate(selectionTransformerStr, WheelSelectionTransformer.class); 257 | } 258 | 259 | mWheelPadding = a.getDimensionPixelSize(R.styleable.WheelView_wheelPadding, 0); 260 | 261 | mWheelPosition = a.getInt(R.styleable.WheelView_wheelPosition, 0); 262 | if (!a.hasValue(R.styleable.WheelView_selectionAngle)) { 263 | //TODO use gravity to default the selection angle if not already specified 264 | } 265 | 266 | a.recycle(); 267 | } 268 | 269 | @SuppressWarnings("unchecked") 270 | private T validateAndInstantiate(String clazzName, Class clazz) { 271 | String errorMessage; 272 | T instance; 273 | try { 274 | Class xmlClazz = Class.forName(clazzName); 275 | if (clazz.isAssignableFrom(xmlClazz)) { 276 | try { 277 | errorMessage = null; 278 | instance = (T) xmlClazz.newInstance(); 279 | } catch (InstantiationException e) { 280 | errorMessage = "No argumentless constructor for " + xmlClazz.getSimpleName(); 281 | instance = null; 282 | } catch (IllegalAccessException e) { 283 | errorMessage = "The argumentless constructor is not public for " + xmlClazz.getSimpleName(); 284 | instance = null; 285 | } 286 | } else { 287 | errorMessage = "Class inflated from xml (" + xmlClazz.getSimpleName() + ") does not implement " + clazz.getSimpleName(); 288 | instance = null; 289 | } 290 | } catch (ClassNotFoundException e) { 291 | errorMessage = clazzName + " class was not found when inflating from xml"; 292 | instance = null; 293 | } 294 | 295 | if (errorMessage != null) { 296 | throw new InflateException(errorMessage); 297 | } else { 298 | return instance; 299 | } 300 | } 301 | 302 | private boolean hasMask(int value, int mask) { 303 | return (value & mask) == mask; 304 | } 305 | 306 | public boolean isPositionLeft() { 307 | return hasMask(mWheelPosition, LEFT_MASK); 308 | } 309 | 310 | public boolean isPositionRight() { 311 | return hasMask(mWheelPosition, RIGHT_MASK); 312 | } 313 | 314 | public boolean isPositionTop() { 315 | return hasMask(mWheelPosition, TOP_MASK); 316 | } 317 | 318 | public boolean isPositionBottom() { 319 | return hasMask(mWheelPosition, BOTTOM_MASK); 320 | } 321 | 322 | public void initWheelView() { 323 | //TODO I only really need to init with default values if there are non defined from attributes... 324 | mItemTransformer = new ScalingItemTransformer(); 325 | mSelectionTransformer = new FadingSelectionTransformer(); 326 | } 327 | 328 | public interface OnWheelItemClickListener { 329 | void onWheelItemClick(WheelView parent, int position, boolean isSelected); 330 | } 331 | 332 | public void setOnWheelItemClickListener(OnWheelItemClickListener listener) { 333 | mOnItemClickListener = listener; 334 | } 335 | 336 | public OnWheelItemClickListener getOnWheelItemClickListener() { 337 | return mOnItemClickListener; 338 | } 339 | 340 | /** 341 | * A listener for when a wheel item is selected. 342 | */ 343 | public interface OnWheelItemSelectListener { 344 | /** 345 | * @param parent WheelView that calls this listener 346 | * @param itemDrawable - The Drawable of the wheel item that is closest to the selection angle 347 | * (or closest to the selection angle) 348 | * @param position of the adapter that is closest to the selection angle 349 | */ 350 | void onWheelItemSelected(WheelView parent, Drawable itemDrawable, int position); 351 | 352 | //TODO onWheelItemSettled? 353 | } 354 | 355 | public void setOnWheelItemSelectedListener(OnWheelItemSelectListener listener) { 356 | mOnItemSelectListener = listener; 357 | } 358 | 359 | public OnWheelItemSelectListener getOnWheelItemSelectListener() { 360 | return mOnItemSelectListener; 361 | } 362 | 363 | public interface OnWheelItemVisibilityChangeListener { 364 | void onItemVisibilityChange(WheelAdapter adapter, int position, boolean isVisible); 365 | } 366 | 367 | /* TODO public */ void setOnWheelItemVisibilityChangeListener(OnWheelItemVisibilityChangeListener listener) { 368 | mOnItemVisibilityChangeListener = listener; 369 | } 370 | 371 | public OnWheelItemVisibilityChangeListener getOnItemVisibilityChangeListener() { 372 | return mOnItemVisibilityChangeListener; 373 | } 374 | 375 | /** 376 | * A listener for when the wheel's angle has changed. 377 | */ 378 | public interface OnWheelAngleChangeListener { 379 | /** 380 | * Receive a callback when the wheel's angle has changed. 381 | */ 382 | void onWheelAngleChange(float angle); 383 | } 384 | 385 | public void setOnWheelAngleChangeListener(OnWheelAngleChangeListener listener) { 386 | mOnAngleChangeListener = listener; 387 | } 388 | 389 | public OnWheelAngleChangeListener getOnWheelAngleChangeListener() { 390 | return mOnAngleChangeListener; 391 | } 392 | 393 | public void setAdapter(WheelAdapter wheelAdapter) { 394 | mAdapter = wheelAdapter; 395 | int count = mAdapter.getCount(); 396 | mItemCacheArray = new CacheItem[count]; 397 | mAdapterItemCount = count; 398 | invalidate(); 399 | } 400 | 401 | public WheelAdapter getAdapter() { 402 | return mAdapter; 403 | } 404 | 405 | public void setWheelItemTransformer(WheelItemTransformer itemTransformer) { 406 | if (itemTransformer == null) throw new IllegalArgumentException("WheelItemTransformer cannot be null"); 407 | mItemTransformer = itemTransformer; 408 | } 409 | 410 | public void setWheelSelectionTransformer(WheelSelectionTransformer transformer) { 411 | mSelectionTransformer = transformer; 412 | } 413 | 414 | /** 415 | *

When true the wheel drawable is rotated as well as the wheel items. 416 | * For performance it is better to not rotate the wheel drawable if possible. 417 | *

The default value is true 418 | */ 419 | public void setWheelDrawableRotatable(boolean isWheelDrawableRotatable) { 420 | mIsWheelDrawableRotatable = isWheelDrawableRotatable; 421 | invalidate(); 422 | } 423 | 424 | /** 425 | * @return {@code true} if the wheel drawable rotates. 426 | */ 427 | public boolean isWheelDrawableRotatable() { 428 | return mIsWheelDrawableRotatable; 429 | } 430 | 431 | /** 432 | * Set Repeatable Adapter to true will continuously cycle through the set of adapter items. 433 | */ 434 | public void setRepeatableAdapter(boolean isRepeatable) { 435 | mIsRepeatable = isRepeatable; 436 | } 437 | 438 | /** 439 | * @return {@code true} if the adapter items continuously cycle around the wheel. 440 | */ 441 | public boolean isRepeatableAdapter() { 442 | return mIsRepeatable; 443 | } 444 | 445 | public void setWheelItemAngle(float angle) { 446 | mItemAngle = angle + mItemAnglePadding; 447 | mItemCount = calculateItemCount(mItemAngle); 448 | //TODO mItemRadius = calculateWheelItemRadius(mItemAngle); 449 | 450 | if (mWheelBounds != null) { 451 | invalidate(); 452 | } 453 | 454 | //TODO 455 | } 456 | 457 | public float getWheelItemAngle() { 458 | return mItemAngle; 459 | } 460 | 461 | private float calculateItemAngle(int itemCount) { 462 | return 360f / itemCount; 463 | } 464 | 465 | private int calculateItemCount(float angle) { 466 | return (int) (360f / angle); 467 | } 468 | 469 | public void setWheelItemAnglePadding(float anglePadding) { 470 | mItemAnglePadding = anglePadding; 471 | 472 | //TODO 473 | } 474 | 475 | public float getWheelItemAnglePadding() { 476 | return mItemAnglePadding; 477 | } 478 | 479 | public void setSelectionAngle(float angle) { 480 | mSelectionAngle = Circle.clamp180(angle); 481 | 482 | if (mWheelBounds != null) { 483 | layoutWheelItems(); 484 | } 485 | } 486 | 487 | public float getSelectionAngle() { 488 | return mSelectionAngle; 489 | } 490 | 491 | public void setSelectionPadding(int padding) { 492 | mSelectionPadding = padding; 493 | } 494 | 495 | public int getSelectionPadding() { 496 | return mSelectionPadding; 497 | } 498 | 499 | public void setWheelToItemDistance(int distance) { 500 | mWheelToItemDistance = distance; 501 | } 502 | 503 | public float getWheelToItemDistance() { 504 | return mWheelToItemDistance; 505 | } 506 | 507 | public void setWheelItemRadius(int radius) { 508 | mItemRadius = radius; 509 | } 510 | 511 | /* TODO 512 | public void setWheelItemRadius(float radius, int itemCount) { 513 | mItemRadius = radius; 514 | mItemAngle = calculateItemAngle(itemCount); 515 | mItemCount = itemCount; 516 | } */ 517 | 518 | public float getWheelItemRadius() { 519 | return mItemRadius; 520 | } 521 | 522 | /** 523 | * Sets the wheel radius in pixels. 524 | */ 525 | public void setWheelRadius(int radius) { 526 | if (radius < -1) throw new IllegalArgumentException("Invalid Wheel Radius: " + radius); 527 | 528 | mWheelRadius = radius; 529 | } 530 | 531 | /** 532 | * Gets the radius of the wheel in pixels 533 | */ 534 | public float getWheelRadius() { 535 | return mWheelRadius; 536 | } 537 | 538 | /** 539 | * Sets the number of items to be displayed on the wheel. 540 | */ 541 | public void setWheelItemCount(int count) { 542 | mItemCount = count; 543 | mItemAngle = calculateItemAngle(count); 544 | 545 | if (mWheelBounds != null) { 546 | invalidate(); 547 | //TODO ? 548 | } 549 | } 550 | 551 | /** 552 | * @return the count of wheel items that are displayed on the wheel. 553 | */ 554 | public float getWheelItemCount() { 555 | return mItemCount; 556 | } 557 | 558 | public void setWheelOffsetX(int offsetX) { 559 | mOffsetX = offsetX; 560 | //TODO 561 | } 562 | 563 | public float getWheelOffsetX() { 564 | return mOffsetX; 565 | } 566 | 567 | public void setWheelOffsetY(int offsetY) { 568 | mOffsetY = offsetY; 569 | //TODO 570 | } 571 | 572 | public float getWheelOffsetY() { 573 | return mOffsetY; 574 | } 575 | 576 | /* 577 | public void setWheelPosition(int position) { 578 | //TODO possible solution to animate or instantly? 579 | }*/ 580 | 581 | /** 582 | * Find the largest circle to fit within the item angle. 583 | * The point of intersection occurs at a tangent to the wheel item. 584 | */ 585 | private float calculateWheelItemRadius(float angle) { 586 | return (float) (mWheelToItemDistance * Math.sin(Math.toRadians((double) ((angle - mItemAnglePadding) / 2f)))); 587 | } 588 | 589 | private float calculateAngle(float innerRadius, float outerRadius) { 590 | return 2f * (float) Math.toDegrees(Math.asin((double) (innerRadius / outerRadius))); 591 | } 592 | 593 | @Override 594 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 595 | int width = right - left; 596 | int height = bottom - top; 597 | 598 | if (mWidth != width || mHeight != height || mLeft != left || mTop != top) { 599 | layoutWheel(0, 0, width, height); 600 | } 601 | 602 | super.onLayout(changed, left, top, right, bottom); 603 | } 604 | 605 | @Override 606 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 607 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 608 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 609 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 610 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 611 | 612 | //if we are not to measure exactly then check what size we would like to be 613 | int desiredWidth; 614 | if (widthMode != MeasureSpec.EXACTLY) { 615 | if (mWheelRadius >= 0) { 616 | desiredWidth = mWheelRadius * 2 + getPaddingLeft() + getPaddingRight(); 617 | } else { 618 | desiredWidth = widthSize; 619 | } 620 | } else { 621 | desiredWidth = -1; 622 | } 623 | 624 | int desiredHeight; 625 | if (heightMode != MeasureSpec.EXACTLY) { 626 | if (mWheelRadius >= 0) { 627 | desiredHeight = mWheelRadius * 2 + getPaddingTop() + getPaddingBottom(); 628 | } else { 629 | desiredHeight = heightSize; 630 | } 631 | } else { 632 | desiredHeight = -1; 633 | } 634 | 635 | desiredWidth = Math.max(desiredWidth, getSuggestedMinimumWidth()); 636 | desiredHeight = Math.max(desiredHeight, getSuggestedMinimumHeight()); 637 | 638 | int width = resolveSizeAndState(desiredWidth, widthMeasureSpec); 639 | int height = resolveSizeAndState(desiredHeight, heightMeasureSpec); 640 | 641 | setMeasuredDimension(width, height); 642 | } 643 | 644 | //Taken and modified from Android Source for API < 11 645 | public static int resolveSizeAndState(int size, int measureSpec) { 646 | int result = size; 647 | int specMode = MeasureSpec.getMode(measureSpec); 648 | int specSize = MeasureSpec.getSize(measureSpec); 649 | switch (specMode) { 650 | case MeasureSpec.UNSPECIFIED: 651 | result = size; 652 | break; 653 | case MeasureSpec.AT_MOST: 654 | if (specSize < size) { 655 | result = specSize; 656 | } else { 657 | result = size; 658 | } 659 | break; 660 | case MeasureSpec.EXACTLY: 661 | result = specSize; 662 | break; 663 | } 664 | return result; 665 | } 666 | 667 | private void layoutWheel(int left, int top, int width, int height) { 668 | if (width == 0 || height == 0) return; 669 | 670 | mLeft = left; 671 | mTop = top; 672 | mWidth = width; 673 | mHeight = height; 674 | 675 | mViewBounds.set(left, top, left + width, top + height); 676 | setWheelBounds(width, height); 677 | 678 | layoutWheelItems(); 679 | } 680 | 681 | private void setWheelBounds(int width, int height) { 682 | float relativeVertical = 0.5f, relativeHorizontal = 0.5f; 683 | if (isPositionLeft()) relativeHorizontal -= 0.5f; 684 | if (isPositionRight()) relativeHorizontal += 0.5f; 685 | if (isPositionTop()) relativeVertical -= 0.5f; 686 | if (isPositionBottom()) relativeVertical += 0.5f; 687 | 688 | final int centerX = (int) (mOffsetX + width * relativeHorizontal); 689 | final int centerY = (int) (mOffsetY + height * relativeVertical); 690 | 691 | int wheelRadius = measureWheelRadius(mWheelRadius, width, height); 692 | mWheelBounds = new Circle(centerX, centerY, wheelRadius); 693 | 694 | if (mWheelDrawable != null) { 695 | mWheelDrawable.setBounds(mWheelBounds.getBoundingRect()); 696 | } 697 | } 698 | 699 | private int measureWheelRadius(int radius, int width, int height) { 700 | if (radius == ViewGroup.LayoutParams.MATCH_PARENT) { 701 | return Math.min(width - getPaddingLeft() - getPaddingRight(), 702 | height - getPaddingTop() - getPaddingBottom()) / 2; 703 | } else { 704 | return radius; 705 | } 706 | } 707 | 708 | private void layoutWheelItems() { 709 | mItemStates = new ArrayList(mItemCount); 710 | for (int i = 0; i < mItemCount; i++) { 711 | mItemStates.add(new ItemState()); 712 | } 713 | 714 | if (mWheelItemBounds == null) { 715 | mWheelItemBounds = new ArrayList(mItemCount); 716 | } else if (!mWheelItemBounds.isEmpty()) { 717 | mWheelItemBounds.clear(); 718 | } 719 | 720 | if (mWheelToItemDistance == ViewGroup.LayoutParams.MATCH_PARENT) { 721 | mWheelToItemDistance = (int) (mWheelBounds.mRadius - mItemRadius - mWheelPadding); 722 | } 723 | 724 | float itemAngleRadians = (float) Math.toRadians(mItemAngle); 725 | float offsetRadians = (float) Math.toRadians(-mSelectionAngle); 726 | for (int i = 0; i < mItemCount; i++) { 727 | float angle = itemAngleRadians * i + offsetRadians; 728 | float x = mWheelBounds.mCenterX + mWheelToItemDistance * (float) Math.cos(angle); 729 | float y = mWheelBounds.mCenterY + mWheelToItemDistance * (float) Math.sin(angle); 730 | mWheelItemBounds.add(new Circle(x, y, mItemRadius)); 731 | } 732 | 733 | invalidate(); 734 | } 735 | 736 | /** 737 | * You should set the wheel drawable not to rotate for a performance benefit. 738 | * See the method {@link #setWheelDrawableRotatable(boolean)} 739 | */ 740 | public void setWheelColor(int color) { 741 | setWheelDrawable(createOvalDrawable(color)); 742 | } 743 | 744 | /** 745 | * Sets the wheel's drawable that can also rotate with the items. 746 | * 747 | * @see #setWheelDrawableRotatable(boolean) 748 | * @see #setWheelDrawable(int) 749 | */ 750 | public void setWheelDrawable(@DrawableRes int resId) { 751 | setWheelDrawable(getResources().getDrawable(resId)); 752 | } 753 | 754 | /** 755 | *

756 | * Sets the wheel's drawable that can also rotate with the items. 757 | *

758 | *

759 | * Note if the drawable has infinite lines of symmetry then you should set the wheel drawable to 760 | * not rotate, see {@link #setWheelDrawableRotatable(boolean)}. In other words, if the drawable 761 | * doesn't look any different whilst it is rotating, you should improve the performance by 762 | * disabling the drawable from rotating. 763 | *

764 | * 765 | * @see #setWheelDrawableRotatable(boolean) 766 | */ 767 | public void setWheelDrawable(Drawable drawable) { 768 | mWheelDrawable = drawable; 769 | 770 | if (mWheelBounds != null) { 771 | mWheelDrawable.setBounds(mWheelBounds.getBoundingRect()); 772 | invalidate(); 773 | } 774 | } 775 | 776 | /** 777 | * Sets the empty item drawable that is drawn when outside of the adapter range. 778 | * 779 | * @see #isEmptyItemPosition(int) 780 | */ 781 | public void setEmptyItemColor(int color) { 782 | setEmptyItemDrawable(createOvalDrawable(color)); 783 | } 784 | 785 | /** 786 | * Sets the empty item drawable that is drawn when outside of the adapter range. 787 | * 788 | * @see #isEmptyItemPosition(int) 789 | */ 790 | public void setEmptyItemDrawable(@DrawableRes int resId) { 791 | setEmptyItemDrawable(getResources().getDrawable(resId)); 792 | } 793 | 794 | /** 795 | * Sets the empty item drawable that is drawn when outside of the adapter range. 796 | * 797 | * @see #isEmptyItemPosition(int) 798 | */ 799 | public void setEmptyItemDrawable(Drawable drawable) { 800 | mEmptyItemDrawable = drawable; 801 | EMPTY_CACHE_ITEM.mDrawable = drawable; 802 | 803 | if (mWheelBounds != null) { 804 | invalidate(); 805 | } 806 | } 807 | 808 | /** 809 | * Sets the selection drawable to be a circular color 810 | * 811 | * @see #setSelectionDrawable(int) 812 | * @see #setSelectionDrawable(Drawable) 813 | */ 814 | public void setSelectionColor(int color) { 815 | setSelectionDrawable(createOvalDrawable(color)); 816 | } 817 | 818 | /** 819 | * Sets the selection drawable from a Drawable Resource. 820 | * 821 | * @see #setSelectionColor(int) 822 | * @see #setSelectionDrawable(Drawable) 823 | */ 824 | public void setSelectionDrawable(@DrawableRes int resId) { 825 | setSelectionDrawable(getResources().getDrawable(resId)); 826 | } 827 | 828 | /** 829 | * Set the selection drawable that is drawn behind the selected item. 830 | * 831 | * @see #setSelectionDrawable(int) 832 | * @see #setSelectionColor(int) 833 | */ 834 | public void setSelectionDrawable(Drawable drawable) { 835 | mSelectionDrawable = drawable; 836 | invalidate(); 837 | } 838 | 839 | /** 840 | * @return The Drawable that is drawn behind the selected item. 841 | */ 842 | public Drawable getSelectionDrawable() { 843 | return mSelectionDrawable; 844 | } 845 | 846 | /** 847 | * @return the empty item drawable used when rendering positions outside of the adapter range. 848 | * 849 | * @see #isEmptyItemPosition(int) 850 | */ 851 | public Drawable getEmptyItemDrawable() { 852 | return mEmptyItemDrawable; 853 | } 854 | 855 | /** 856 | * @return the wheel's drawable 857 | */ 858 | public Drawable getWheelDrawable() { 859 | return mWheelDrawable; 860 | } 861 | 862 | /** 863 | * @return the absolute angle for the item at the given position 864 | */ 865 | public float getAngleForPosition(int rawPosition) { 866 | return rawPosition * mItemAngle; 867 | } 868 | 869 | /** 870 | *

871 | * Changes the wheel angle so that the item at the provided position becomes selected. 872 | *

873 | *

874 | * Note that this does not change the selection angle, instead it will rotate the wheel 875 | * to the angle where the provided position becomes selected. 876 | *

877 | * 878 | * @param rawPosition the raw position (can take negative numbers) 879 | * 880 | * @see #setMidSelected() 881 | */ 882 | public void setSelected(int rawPosition) { 883 | //must rotate the wheel in the opposite direction so that the given position becomes selected 884 | setAngle(-1f * getAngleForPosition(rawPosition)); 885 | } 886 | 887 | /** 888 | * Changes the wheel angle so that the item in the middle of the adapter becomes selected. 889 | * 890 | * @see #setSelected(int) 891 | */ 892 | public void setMidSelected() { 893 | if (mAdapter == null || mAdapterItemCount == 0) 894 | throw new IllegalStateException("Cannot select position with no adapter items"); 895 | 896 | setSelected(mAdapterItemCount / 2); 897 | } 898 | 899 | /** 900 | * The raw selected position (can be negative and isn't cyclic) 901 | * 902 | * @see #getAngle() 903 | * @see #getSelectedPosition() 904 | */ 905 | public int getRawSelectedPosition() { 906 | return mRawSelectedPosition; 907 | } 908 | 909 | /** 910 | * Set the angle of the wheel instantaneously. 911 | * Note this does not animate to the provided angle. 912 | * 913 | * @param angle given in degrees and can be any value (not only between 0 and 360) 914 | */ 915 | public void setAngle(float angle) { 916 | mAngle = angle; 917 | 918 | updateSelectedPosition(); 919 | 920 | if (mOnAngleChangeListener != null) { 921 | mOnAngleChangeListener.onWheelAngleChange(mAngle); 922 | } 923 | 924 | invalidate(); 925 | } 926 | 927 | /** 928 | * Checks to see if the selectedPosition has changed. 929 | */ 930 | private void updateSelectedPosition() { 931 | int position = (int) ((-mAngle + -0.5 * Math.signum(mAngle) * mItemAngle) / mItemAngle); 932 | setSelectedPosition(position); 933 | } 934 | 935 | /** 936 | * @return {@code true} if this adapter position is empty. 937 | * 938 | * This is only possible with non-repeatable items. 939 | */ 940 | public boolean isEmptyItemPosition(int position) { 941 | return !mIsRepeatable && (position < 0 || position >= mAdapterItemCount); 942 | } 943 | 944 | private void setSelectedPosition(int position) { 945 | if (mRawSelectedPosition == position) return; 946 | 947 | mRawSelectedPosition = position; 948 | 949 | if (mOnItemSelectListener != null && !isEmptyItemPosition(position)) { 950 | int adapterPos = getSelectedPosition(); 951 | mOnItemSelectListener.onWheelItemSelected(this, getWheelItemDrawable(adapterPos), adapterPos); 952 | } 953 | } 954 | 955 | /** 956 | * @param position of the item in the Adapter 957 | * @return The Drawable at the specific position in the Adapter 958 | */ 959 | public Drawable getWheelItemDrawable(int position) { 960 | if (mAdapter == null || mAdapterItemCount == 0) return null; 961 | 962 | CacheItem cacheItem = getCacheItem(position); 963 | if (!cacheItem.mDirty) return cacheItem.mDrawable; 964 | 965 | return cacheItem.mDrawable = mAdapter.getDrawable(position); 966 | } 967 | 968 | /** 969 | * Invalidate the drawable at the specific position so that the next Draw call 970 | * will refresh the Drawable at this given position in the adapter. 971 | */ 972 | public void invalidateWheelItemDrawable(int position) { 973 | int adapterPos = rawPositionToAdapterPosition(position); 974 | if (isEmptyItemPosition(adapterPos)) return; 975 | 976 | CacheItem cacheItem = mItemCacheArray[adapterPos]; 977 | if (cacheItem != null) cacheItem.mDirty = true; 978 | invalidate(); 979 | } 980 | 981 | /** 982 | * Invalidate all wheel items. Note - If you need to change the number of items 983 | * in the adapter then you will need to use {@link #setAdapter} 984 | * 985 | * @see #invalidateWheelItemDrawable 986 | */ 987 | public void invalidateWheelItemDrawables() { 988 | for (int i = 0; i < mAdapterItemCount; i++) { 989 | invalidateWheelItemDrawable(i); 990 | } 991 | } 992 | 993 | private Drawable createOvalDrawable(int color) { 994 | ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape()); 995 | shapeDrawable.getPaint().setColor(color); 996 | return shapeDrawable; 997 | } 998 | 999 | /** 1000 | * @return the adapter position that is closest to the selection 1001 | * 1002 | * @see #getRawSelectedPosition() 1003 | * @see #getAngle() 1004 | */ 1005 | public int getSelectedPosition() { 1006 | return rawPositionToAdapterPosition(mRawSelectedPosition); 1007 | } 1008 | 1009 | /** 1010 | * @return the wheel angle in degrees. 1011 | * 1012 | * @see #getRawSelectedPosition() 1013 | * @see #getSelectedPosition() 1014 | */ 1015 | public float getAngle() { 1016 | return mAngle; 1017 | } 1018 | 1019 | private void addAngle(float degrees) { 1020 | setAngle(mAngle + degrees); 1021 | } 1022 | 1023 | @Override 1024 | public boolean onTouchEvent(@NonNull MotionEvent event) { 1025 | final float x = event.getX(); 1026 | final float y = event.getY(); 1027 | 1028 | if (!mWheelBounds.contains(x, y)) { 1029 | if (mIsDraggingWheel) { 1030 | flingWheel(); 1031 | } 1032 | return true; 1033 | } 1034 | 1035 | switch (event.getAction() & MotionEvent.ACTION_MASK) { 1036 | case MotionEvent.ACTION_DOWN: 1037 | if (!mIsDraggingWheel) { 1038 | startWheelDrag(event, x, y); 1039 | } 1040 | 1041 | mClickedItem = getClickedItem(x, y); 1042 | break; 1043 | case MotionEvent.ACTION_UP: 1044 | if (mOnItemClickListener != null && mClickedItem != null 1045 | && mClickedItem == getClickedItem(x, y) 1046 | && mDraggedAngle < CLICK_MAX_DRAGGED_ANGLE) { 1047 | boolean isSelected = Math.abs(mClickedItem.mRelativePos) < 1f; 1048 | mOnItemClickListener.onWheelItemClick(this, 1049 | mClickedItem.mAdapterPosition, isSelected); 1050 | } 1051 | case MotionEvent.ACTION_CANCEL: 1052 | if (mIsDraggingWheel) { 1053 | flingWheel(); 1054 | } 1055 | 1056 | if (mVelocityTracker != null) { 1057 | mVelocityTracker.recycle(); 1058 | mVelocityTracker = null; 1059 | } 1060 | break; 1061 | case MotionEvent.ACTION_MOVE: 1062 | if (!mIsDraggingWheel) { 1063 | startWheelDrag(event, x, y); 1064 | return true; 1065 | } 1066 | 1067 | mVelocityTracker.addMovement(event); 1068 | mLastWheelTouchX = x; 1069 | mLastWheelTouchY = y; 1070 | setRadiusVector(x, y); 1071 | 1072 | float wheelRadiusSquared = mWheelBounds.getRadius() * mWheelBounds.getRadius(); 1073 | float touchRadiusSquared = mRadiusVector.x * mRadiusVector.x + mRadiusVector.y * mRadiusVector.y; 1074 | float touchFactor = TOUCH_FACTORS[(int) (touchRadiusSquared / wheelRadiusSquared * TOUCH_FACTORS.length)]; 1075 | float touchAngle = mWheelBounds.angleToDegrees(x, y); 1076 | float draggedAngle = -1f * Circle.shortestAngle(touchAngle, mLastTouchAngle) * touchFactor; 1077 | addAngle(draggedAngle); 1078 | mLastTouchAngle = touchAngle; 1079 | mDraggedAngle += draggedAngle; 1080 | 1081 | if (mRequiresUpdate) { 1082 | mRequiresUpdate = false; 1083 | } 1084 | break; 1085 | } 1086 | return true; 1087 | } 1088 | 1089 | private void startWheelDrag(MotionEvent event, float x, float y) { 1090 | mIsDraggingWheel = true; 1091 | mDraggedAngle = 0f; 1092 | 1093 | if (mVelocityTracker == null) { 1094 | mVelocityTracker = VelocityTracker.obtain(); 1095 | } else { 1096 | mVelocityTracker.clear(); 1097 | } 1098 | mVelocityTracker.addMovement(event); 1099 | 1100 | mAngularVelocity = 0f; 1101 | mLastTouchAngle = mWheelBounds.angleToDegrees(x, y); 1102 | } 1103 | 1104 | private void flingWheel() { 1105 | mIsDraggingWheel = false; 1106 | 1107 | mVelocityTracker.computeCurrentVelocity(1); 1108 | 1109 | //torque = r X F 1110 | mForceVector.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 1111 | setRadiusVector(mLastWheelTouchX, mLastWheelTouchY); 1112 | float torque = mForceVector.crossProduct(mRadiusVector); 1113 | 1114 | //dw/dt = torque / I = torque / mr^2 1115 | float wheelRadiusSquared = mWheelBounds.getRadius() * mWheelBounds.getRadius(); 1116 | float angularAccel = torque / wheelRadiusSquared; 1117 | 1118 | //estimate an angular velocity based on the strength of the angular acceleration 1119 | float angularVel = angularAccel * ANGULAR_VEL_COEFFICIENT; 1120 | 1121 | //clamp the angular velocity 1122 | if (angularVel > MAX_ANGULAR_VEL) angularVel = MAX_ANGULAR_VEL; 1123 | else if (angularVel < -MAX_ANGULAR_VEL) angularVel = -MAX_ANGULAR_VEL; 1124 | mAngularVelocity = angularVel; 1125 | 1126 | mLastUpdateTime = SystemClock.uptimeMillis(); 1127 | mRequiresUpdate = true; 1128 | 1129 | invalidate(); 1130 | } 1131 | 1132 | private void setRadiusVector(float x, float y) { 1133 | float rVectorX = mWheelBounds.mCenterX - x; 1134 | float rVectorY = mWheelBounds.mCenterY - y; 1135 | mRadiusVector.set(rVectorX, rVectorY); 1136 | } 1137 | 1138 | /** 1139 | * Converts the raw position to a position within the adapter bounds. 1140 | * 1141 | * @see #rawPositionToWheelPosition(int, int) 1142 | * @see #rawPositionToWheelPosition(int) 1143 | */ 1144 | public int rawPositionToAdapterPosition(int position) { 1145 | return mIsRepeatable ? Circle.clamp(position, mAdapterItemCount) : position; 1146 | } 1147 | 1148 | /** 1149 | * Converts the raw position to a position within the wheel item bounds. 1150 | * 1151 | * @see #rawPositionToAdapterPosition(int) 1152 | * @see #rawPositionToWheelPosition(int, int) 1153 | */ 1154 | public int rawPositionToWheelPosition(int position) { 1155 | return rawPositionToWheelPosition(position, rawPositionToAdapterPosition(position)); 1156 | } 1157 | 1158 | /** 1159 | * Converts the raw position to a position within the wheel item bounds. 1160 | * 1161 | * @see #rawPositionToAdapterPosition(int) 1162 | * @see #rawPositionToWheelPosition(int) 1163 | */ 1164 | public int rawPositionToWheelPosition(int position, int adapterPosition) { 1165 | int circularOffset = mIsRepeatable ? ((int) Math.floor((position / 1166 | (float) mAdapterItemCount)) * (mAdapterItemCount - mItemCount)) : 0; 1167 | return Circle.clamp(adapterPosition + circularOffset, mItemCount); 1168 | } 1169 | 1170 | /** 1171 | * Estimates the wheel's new angle and angular velocity 1172 | */ 1173 | private void update(float deltaTime) { 1174 | float vel = mAngularVelocity; 1175 | float velSqr = vel*vel; 1176 | if (vel > 0f) { 1177 | //TODO the damping is not based on time 1178 | mAngularVelocity -= velSqr * VELOCITY_FRICTION_COEFFICIENT + CONSTANT_FRICTION_COEFFICIENT; 1179 | if (mAngularVelocity < 0f) mAngularVelocity = 0f; 1180 | } else if (vel < 0f) { 1181 | mAngularVelocity -= velSqr * -VELOCITY_FRICTION_COEFFICIENT - CONSTANT_FRICTION_COEFFICIENT; 1182 | if (mAngularVelocity > 0f) mAngularVelocity = 0f; 1183 | } 1184 | 1185 | if (mAngularVelocity != 0f) { 1186 | addAngle(mAngularVelocity * deltaTime); 1187 | } else { 1188 | mRequiresUpdate = false; 1189 | } 1190 | } 1191 | 1192 | private void updateWheelStateIfReq() { 1193 | if (!mRequiresUpdate) return; 1194 | 1195 | long currentTime = SystemClock.uptimeMillis(); 1196 | long timeDiff = currentTime - mLastUpdateTime; 1197 | mLastUpdateTime = currentTime; 1198 | update(timeDiff); 1199 | } 1200 | 1201 | @Override 1202 | protected void onDraw(Canvas canvas) { 1203 | updateWheelStateIfReq(); 1204 | 1205 | if (mWheelBounds == null) return; //issue with layoutWheel not being called before draw call 1206 | 1207 | if (mWheelDrawable != null) { 1208 | drawWheel(canvas); 1209 | } 1210 | 1211 | if (mAdapter != null && mAdapterItemCount > 0) { 1212 | drawWheelItems(canvas); 1213 | } 1214 | } 1215 | 1216 | private void drawWheel(Canvas canvas) { 1217 | if (mIsWheelDrawableRotatable) { 1218 | canvas.save(); 1219 | canvas.rotate(mAngle, mWheelBounds.mCenterX, mWheelBounds.mCenterY); 1220 | mWheelDrawable.draw(canvas); 1221 | canvas.restore(); 1222 | } else { 1223 | mWheelDrawable.draw(canvas); 1224 | } 1225 | } 1226 | 1227 | private void drawWheelItems(Canvas canvas) { 1228 | double angleInRadians = Math.toRadians(mAngle); 1229 | double cosAngle = Math.cos(angleInRadians); 1230 | double sinAngle = Math.sin(angleInRadians); 1231 | float centerX = mWheelBounds.mCenterX; 1232 | float centerY = mWheelBounds.mCenterY; 1233 | 1234 | int wheelItemOffset = mItemCount / 2; 1235 | int offset = mRawSelectedPosition - wheelItemOffset; 1236 | int length = mItemCount + offset; 1237 | for (int i = offset; i < length; i++) { 1238 | int adapterPosition = rawPositionToAdapterPosition(i); 1239 | int wheelItemPosition = rawPositionToWheelPosition(i, adapterPosition); 1240 | 1241 | Circle itemBounds = mWheelItemBounds.get(wheelItemPosition); 1242 | float radius = itemBounds.mRadius; 1243 | 1244 | //translate before rotating so that origin is at the wheel's center 1245 | float x = itemBounds.mCenterX - centerX; 1246 | float y = itemBounds.mCenterY - centerY; 1247 | 1248 | //rotate 1249 | float x1 = (float) (x * cosAngle - y * sinAngle); 1250 | float y1 = (float) (x * sinAngle + y * cosAngle); 1251 | 1252 | //translate back after rotation 1253 | x1 += centerX; 1254 | y1 += centerY; 1255 | 1256 | ItemState itemState = mItemStates.get(wheelItemPosition); 1257 | updateItemState(itemState, adapterPosition, x1, y1, radius); 1258 | mItemTransformer.transform(itemState, sTempRect); 1259 | 1260 | //Empty positions can only occur from having "non repeatable" items 1261 | CacheItem cacheItem = getCacheItem(adapterPosition); 1262 | 1263 | //don't draw if outside of the view bounds 1264 | if (Rect.intersects(sTempRect, mViewBounds)) { 1265 | if (cacheItem.mDirty && !cacheItem.mIsEmpty) { 1266 | cacheItem.mDrawable = mAdapter.getDrawable(adapterPosition); 1267 | cacheItem.mDirty = false; 1268 | } 1269 | 1270 | if (!cacheItem.mIsVisible) { 1271 | cacheItem.mIsVisible = true; 1272 | if (mOnItemVisibilityChangeListener != null) { 1273 | mOnItemVisibilityChangeListener.onItemVisibilityChange(mAdapter, adapterPosition, true); 1274 | } 1275 | } 1276 | 1277 | if (i == mRawSelectedPosition && mSelectionDrawable != null && !isEmptyItemPosition(i)) { 1278 | mSelectionDrawable.setBounds(sTempRect.left - mSelectionPadding, sTempRect.top - mSelectionPadding, 1279 | sTempRect.right + mSelectionPadding, sTempRect.bottom + mSelectionPadding); 1280 | mSelectionTransformer.transform(mSelectionDrawable, itemState); 1281 | mSelectionDrawable.draw(canvas); 1282 | } 1283 | 1284 | Drawable drawable = cacheItem.mDrawable; 1285 | if (drawable != null) { 1286 | drawable.setBounds(sTempRect); 1287 | drawable.draw(canvas); 1288 | } 1289 | } else { 1290 | if (cacheItem != null && cacheItem.mIsVisible) { 1291 | cacheItem.mIsVisible = false; 1292 | if (mOnItemVisibilityChangeListener != null) { 1293 | mOnItemVisibilityChangeListener.onItemVisibilityChange(mAdapter, adapterPosition, false); 1294 | } 1295 | } 1296 | } 1297 | } 1298 | } 1299 | 1300 | /** 1301 | * The ItemState is used to provide extra information when transforming the selection drawable 1302 | * or item bounds. 1303 | */ 1304 | public static class ItemState { 1305 | WheelView mWheelView; 1306 | Circle mBounds; 1307 | float mAngleFromSelection; 1308 | float mRelativePos; 1309 | int mAdapterPosition; //TODO 1310 | 1311 | private ItemState() { 1312 | mBounds = new Circle(); 1313 | } 1314 | 1315 | public WheelView getWheelView() { 1316 | return mWheelView; 1317 | } 1318 | 1319 | public float getAngleFromSelection() { 1320 | return mAngleFromSelection; 1321 | } 1322 | 1323 | public Circle getBounds() { 1324 | return mBounds; 1325 | } 1326 | 1327 | public float getRelativePosition() { 1328 | return mRelativePos; 1329 | } 1330 | } 1331 | 1332 | private void updateItemState(ItemState itemState, int adapterPosition, 1333 | float x, float y, float radius) { 1334 | float itemAngle = mWheelBounds.angleToDegrees(x, y); 1335 | float angleFromSelection = Circle.shortestAngle(itemAngle, mSelectionAngle); 1336 | float relativePos = angleFromSelection / mItemAngle * 2f; 1337 | 1338 | itemState.mAngleFromSelection = angleFromSelection; 1339 | itemState.mRelativePos = relativePos; 1340 | itemState.mBounds.mCenterX = x; 1341 | itemState.mBounds.mCenterY = y; 1342 | itemState.mAdapterPosition = adapterPosition; 1343 | 1344 | //TODO The radius is always known - doesn't really need this? 1345 | itemState.mBounds.mRadius = radius; 1346 | } 1347 | 1348 | private ItemState getClickedItem(float touchX, float touchY) { 1349 | for (ItemState state : mItemStates) { 1350 | Circle itemBounds = state.mBounds; 1351 | if (itemBounds.contains(touchX, touchY)) return state; 1352 | } 1353 | return null; 1354 | } 1355 | 1356 | static class CacheItem { 1357 | boolean mDirty; 1358 | boolean mIsVisible; 1359 | boolean mIsEmpty; 1360 | Drawable mDrawable; 1361 | 1362 | CacheItem() { 1363 | mDirty = true; 1364 | } 1365 | 1366 | CacheItem(boolean isEmpty) { 1367 | this(); 1368 | mIsEmpty = isEmpty; 1369 | } 1370 | } 1371 | 1372 | private CacheItem getCacheItem(int position) { 1373 | if (isEmptyItemPosition(position)) return EMPTY_CACHE_ITEM; 1374 | 1375 | CacheItem cacheItem = mItemCacheArray[position]; 1376 | if (cacheItem == null) { 1377 | cacheItem = new CacheItem(); 1378 | mItemCacheArray[position] = cacheItem; 1379 | } 1380 | return cacheItem; 1381 | } 1382 | 1383 | /** 1384 | * A simple class to represent a vector with an add and cross product method. Used only to 1385 | * calculate the Wheel's angular velocity in {@link #flingWheel()} 1386 | */ 1387 | static class Vector { 1388 | float x, y; 1389 | 1390 | Vector() {} 1391 | 1392 | void set(float x, float y) { 1393 | this.x = x; 1394 | this.y = y; 1395 | } 1396 | 1397 | float crossProduct(Vector vector) { 1398 | return this.x * vector.y - this.y * vector.x; 1399 | } 1400 | 1401 | @Override 1402 | public String toString() { 1403 | return "Vector: (" + this.x + ", " + this.y + ")"; 1404 | } 1405 | } 1406 | } 1407 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/adapter/WheelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.adapter; 2 | 3 | import android.graphics.drawable.Drawable; 4 | import com.lukedeighton.wheelview.WheelView; 5 | 6 | /** 7 | *

8 | * Provide drawables for the {@link WheelView} to draw on the wheel. 9 | *

10 | * 11 | *

12 | * Note that {@link WheelAdapter} doesn't behave exactly like a typical Adapter from Android source. 13 | * There are some limitations to using drawables rather than views, but it also means you do not 14 | * need to worry about recycling drawables as it is not as expensive as view inflation. 15 | *

16 | * 17 | *

18 | * It may be possible to properly implement an Adapter with recycling Views but for now this will do. 19 | *

20 | */ 21 | public interface WheelAdapter { 22 | 23 | /** 24 | * @param position the adapter position, between 0 and {@link #getCount()}. 25 | * @return the drawable to be drawn on the wheel at this adapter position. 26 | */ 27 | Drawable getDrawable(int position); 28 | 29 | /** 30 | * @return the number of items in the adapter. 31 | */ 32 | int getCount(); 33 | } 34 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/adapter/WheelArrayAdapter.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.adapter; 2 | 3 | import java.util.List; 4 | 5 | public abstract class WheelArrayAdapter implements WheelAdapter { 6 | private List mItems; 7 | 8 | public WheelArrayAdapter(List items) { 9 | mItems = items; 10 | } 11 | 12 | public T getItem(int position) { 13 | return mItems.get(position); 14 | } 15 | 16 | @Override 17 | public int getCount() { 18 | return mItems.size(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/transformer/FadingSelectionTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.transformer; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import com.lukedeighton.wheelview.WheelView; 6 | 7 | public class FadingSelectionTransformer implements WheelSelectionTransformer { 8 | 9 | @Override 10 | public void transform(Drawable drawable, WheelView.ItemState itemState) { 11 | float relativePosition = Math.abs(itemState.getRelativePosition()); 12 | int alpha = (int) ((1f - Math.pow(relativePosition, 2.5f)) * 255f); 13 | 14 | //clamp to between 0 and 255 15 | if (alpha > 255) alpha = 255; 16 | else if (alpha < 0) alpha = 0; 17 | 18 | drawable.setAlpha(alpha); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/transformer/ScalingItemTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.transformer; 2 | 3 | import android.graphics.Rect; 4 | 5 | import com.lukedeighton.wheelview.Circle; 6 | import com.lukedeighton.wheelview.WheelView; 7 | 8 | public class ScalingItemTransformer implements WheelItemTransformer { 9 | @Override 10 | public void transform(WheelView.ItemState itemState, Rect itemBounds) { 11 | float scale = itemState.getAngleFromSelection() * 0.014f; 12 | scale = Math.min(1.12f, 1.15f - Math.min(0.25f, Math.abs(scale))); 13 | Circle bounds = itemState.getBounds(); 14 | float radius = bounds.getRadius() * scale; 15 | float x = bounds.getCenterX(); 16 | float y = bounds.getCenterY(); 17 | itemBounds.set(Math.round(x - radius), Math.round(y - radius), Math.round(x + radius), Math.round(y + radius)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/transformer/SimpleItemTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.transformer; 2 | 3 | import android.graphics.Rect; 4 | 5 | import com.lukedeighton.wheelview.Circle; 6 | import com.lukedeighton.wheelview.WheelView; 7 | 8 | public class SimpleItemTransformer implements WheelItemTransformer { 9 | @Override 10 | public void transform(WheelView.ItemState itemState, Rect itemBounds) { 11 | Circle bounds = itemState.getBounds(); 12 | float radius = bounds.getRadius(); 13 | float x = bounds.getCenterX(); 14 | float y = bounds.getCenterY(); 15 | itemBounds.set(Math.round(x - radius), Math.round(y - radius), Math.round(x + radius), Math.round(y + radius)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/transformer/WheelItemTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.transformer; 2 | 3 | import android.graphics.Rect; 4 | 5 | import com.lukedeighton.wheelview.WheelView; 6 | 7 | public interface WheelItemTransformer { 8 | /** 9 | * You have control over the Items draw bounds. By supplying your own WheelItemTransformer 10 | * you must call set bounds on the itemBounds. 11 | */ 12 | void transform(WheelView.ItemState itemState, Rect itemBounds); 13 | } 14 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/java/com/lukedeighton/wheelview/transformer/WheelSelectionTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelview.transformer; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | import com.lukedeighton.wheelview.WheelView; 6 | 7 | public interface WheelSelectionTransformer { 8 | void transform(Drawable drawable, WheelView.ItemState itemState); 9 | } 10 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /WheelViewLib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WheelView 3 | 4 | -------------------------------------------------------------------------------- /WheelViewSample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /WheelViewSample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | android { 3 | compileSdkVersion 28 4 | buildToolsVersion '28.0.0' 5 | defaultConfig { 6 | minSdkVersion 10 7 | targetSdkVersion 28 8 | versionCode 1 9 | versionName '1.0' 10 | } 11 | buildTypes { 12 | release { 13 | minifyEnabled true 14 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 15 | } 16 | } 17 | productFlavors { 18 | } 19 | } 20 | dependencies { 21 | compile fileTree(include: ['*.jar'], dir: 'libs') 22 | compile 'com.android.support:appcompat-v7:25.4.0' 23 | compile project(':WheelViewLib') 24 | } 25 | -------------------------------------------------------------------------------- /WheelViewSample/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Program Files (x86)/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} -------------------------------------------------------------------------------- /WheelViewSample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/java/com/lukedeighton/wheelsample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelsample; 2 | 3 | import android.app.Activity; 4 | import android.graphics.drawable.Drawable; 5 | import android.graphics.drawable.LayerDrawable; 6 | import android.graphics.drawable.ShapeDrawable; 7 | import android.graphics.drawable.shapes.OvalShape; 8 | import android.os.Bundle; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.widget.Toast; 12 | 13 | import com.lukedeighton.wheelview.WheelView; 14 | import com.lukedeighton.wheelview.adapter.WheelArrayAdapter; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | public class MainActivity extends Activity { 21 | 22 | private static final int ITEM_COUNT = 20; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | 29 | final WheelView wheelView = (WheelView) findViewById(R.id.wheelview); 30 | 31 | //create data for the adapter 32 | List> entries = new ArrayList>(ITEM_COUNT); 33 | for (int i = 0; i < ITEM_COUNT; i++) { 34 | Map.Entry entry = MaterialColor.random(this, "\\D*_500$"); 35 | entries.add(entry); 36 | } 37 | 38 | //populate the adapter, that knows how to draw each item (as you would do with a ListAdapter) 39 | wheelView.setAdapter(new MaterialColorAdapter(entries)); 40 | 41 | //a listener for receiving a callback for when the item closest to the selection angle changes 42 | wheelView.setOnWheelItemSelectedListener(new WheelView.OnWheelItemSelectListener() { 43 | @Override 44 | public void onWheelItemSelected(WheelView parent, Drawable itemDrawable, int position) { 45 | //get the item at this position 46 | Map.Entry selectedEntry = ((MaterialColorAdapter) parent.getAdapter()).getItem(position); 47 | parent.setSelectionColor(getContrastColor(selectedEntry)); 48 | } 49 | }); 50 | 51 | wheelView.setOnWheelItemClickListener(new WheelView.OnWheelItemClickListener() { 52 | @Override 53 | public void onWheelItemClick(WheelView parent, int position, boolean isSelected) { 54 | String msg = String.valueOf(position) + " " + isSelected; 55 | Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show(); 56 | } 57 | }); 58 | 59 | //initialise the selection drawable with the first contrast color 60 | wheelView.setSelectionColor(getContrastColor(entries.get(0))); 61 | 62 | /* 63 | new Handler().postDelayed(new Runnable() { 64 | @Override 65 | public void run() { 66 | //wheelView.setSelectionAngle(-wheelView.getAngleForPosition(5)); 67 | wheelView.setMidSelected(); 68 | } 69 | }, 3000); */ 70 | } 71 | 72 | //get the materials darker contrast 73 | private int getContrastColor(Map.Entry entry) { 74 | String colorName = MaterialColor.getColorName(entry); 75 | return MaterialColor.getContrastColor(colorName); 76 | } 77 | 78 | @Override 79 | public boolean onCreateOptionsMenu(Menu menu) { 80 | getMenuInflater().inflate(R.menu.main, menu); 81 | return true; 82 | } 83 | 84 | @Override 85 | public boolean onOptionsItemSelected(MenuItem item) { 86 | int id = item.getItemId(); 87 | if (id == R.id.action_settings) { 88 | return true; 89 | } 90 | return super.onOptionsItemSelected(item); 91 | } 92 | 93 | static class MaterialColorAdapter extends WheelArrayAdapter> { 94 | MaterialColorAdapter(List> entries) { 95 | super(entries); 96 | } 97 | 98 | @Override 99 | public Drawable getDrawable(int position) { 100 | Drawable[] drawable = new Drawable[] { 101 | createOvalDrawable(getItem(position).getValue()), 102 | new TextDrawable(String.valueOf(position)) 103 | }; 104 | return new LayerDrawable(drawable); 105 | } 106 | 107 | private Drawable createOvalDrawable(int color) { 108 | ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape()); 109 | shapeDrawable.getPaint().setColor(color); 110 | return shapeDrawable; 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /WheelViewSample/src/main/java/com/lukedeighton/wheelsample/MaterialColor.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelsample; 2 | 3 | import android.content.Context; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Random; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | public class MaterialColor { 15 | private static Random sRandom = new Random(); 16 | private static HashMap sMaterialHashMap; 17 | private static Pattern sColorPattern = Pattern.compile("_[aA]?+\\d+"); 18 | 19 | private static HashMap getMaterialColors(Context context) { 20 | Field[] fields = R.color.class.getFields(); 21 | HashMap materialHashMap = new HashMap(fields.length); 22 | for(Field field : fields) { 23 | if (field.getType() != int.class) continue; 24 | 25 | String fieldName = field.getName(); //prone to errors but okay for a sample! 26 | if (fieldName.startsWith("abc") || fieldName.startsWith("material")) continue; 27 | 28 | try { 29 | int resId = field.getInt(null); 30 | materialHashMap.put(fieldName, context.getResources().getColor(resId)); 31 | } catch (IllegalAccessException e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | 36 | return materialHashMap; 37 | } 38 | 39 | public static Map.Entry random(Context context, String regex) { 40 | if (sMaterialHashMap == null) { 41 | sMaterialHashMap = getMaterialColors(context); 42 | } 43 | 44 | Pattern pattern = Pattern.compile(regex); 45 | List> materialColors = new ArrayList>(); 46 | for(Map.Entry entry : sMaterialHashMap.entrySet()) { 47 | if (!pattern.matcher(entry.getKey()).matches()) continue; 48 | materialColors.add(entry); 49 | } 50 | 51 | int rndIndex = sRandom.nextInt(materialColors.size()); 52 | return materialColors.get(rndIndex); 53 | } 54 | 55 | public static int getContrastColor(String colourName) { 56 | return sMaterialHashMap.get(colourName + "_700"); 57 | } 58 | 59 | public static String getColorName(Map.Entry entry) { 60 | String color = entry.getKey(); 61 | Matcher matcher = sColorPattern.matcher(color); 62 | if (matcher.find()) { 63 | return color.substring(0, matcher.start()); 64 | } 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/java/com/lukedeighton/wheelsample/TextDrawable.java: -------------------------------------------------------------------------------- 1 | package com.lukedeighton.wheelsample; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | 11 | public class TextDrawable extends Drawable { 12 | 13 | private final String text; 14 | private final Paint paint; 15 | 16 | public TextDrawable(String text) { 17 | 18 | this.text = text; 19 | 20 | this.paint = new Paint(); 21 | paint.setColor(Color.WHITE); 22 | paint.setTextSize(52f); 23 | paint.setAntiAlias(true); 24 | paint.setFakeBoldText(true); 25 | paint.setShadowLayer(12f, 0, 0, Color.BLACK); 26 | paint.setStyle(Paint.Style.FILL); 27 | paint.setTextAlign(Paint.Align.LEFT); 28 | } 29 | 30 | @Override 31 | public void draw(Canvas canvas) { 32 | Rect bounds = getBounds(); 33 | canvas.drawText(text, bounds.centerX() - 15f /*just a lazy attempt to centre the text*/ * text.length(), bounds.centerY() + 15f, paint); 34 | } 35 | 36 | @Override 37 | public void setAlpha(int alpha) { 38 | paint.setAlpha(alpha); 39 | } 40 | 41 | @Override 42 | public void setColorFilter(ColorFilter cf) { 43 | paint.setColorFilter(cf); 44 | } 45 | 46 | @Override 47 | public int getOpacity() { 48 | return PixelFormat.TRANSLUCENT; 49 | } 50 | 51 | public String getText() { 52 | return text; 53 | } 54 | } -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/WheelViewSample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/drawable-hdpi/wheel_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/WheelViewSample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/WheelViewSample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/WheelViewSample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/values/material_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fde0dc 4 | #f9bdbb 5 | #f69988 6 | #f36c60 7 | #e84e40 8 | #e51c23 9 | #dd191d 10 | #d01716 11 | #c41411 12 | #b0120a 13 | #ff7997 14 | #ff5177 15 | #ff2d6f 16 | #e00032 17 | 18 | 19 | #fce4ec 20 | #f8bbd0 21 | #f48fb1 22 | #f06292 23 | #ec407a 24 | #e91e63 25 | #d81b60 26 | #c2185b 27 | #ad1457 28 | #880e4f 29 | #ff80ab 30 | #ff4081 31 | #f50057 32 | #c51162 33 | 34 | 35 | #f3e5f5 36 | #e1bee7 37 | #cd93d8 38 | #ba68c8 39 | #ab47bc 40 | #9c27b0 41 | #8e24aa 42 | #7b1fa2 43 | #6a1b9a 44 | #4a148c 45 | #ea80fc 46 | #e040fb 47 | #d500f9 48 | #aa00ff 49 | 50 | 51 | #ede7f6 52 | #d1c4e9 53 | #b39ddb 54 | #9575cd 55 | #7e57c2 56 | #673ab7 57 | #5e35b1 58 | #512da8 59 | #4527a0 60 | #311b92 61 | #b388ff 62 | #7c4dff 63 | #651fff 64 | #6200ea 65 | 66 | 67 | #e8eaf6 68 | #c5cae9 69 | #9fa8da 70 | #7986cb 71 | #5c6bc0 72 | #3f51b5 73 | #3949ab 74 | #303f9f 75 | #283593 76 | #1a237e 77 | #8c9eff 78 | #536dfe 79 | #3d5afe 80 | #304ffe 81 | 82 | 83 | #e7e9fd 84 | #d0d9ff 85 | #afbfff 86 | #91a7ff 87 | #738ffe 88 | #5677fc 89 | #4e6cef 90 | #455ede 91 | #3b50ce 92 | #2a36b1 93 | #a6baff 94 | #6889ff 95 | #4d73ff 96 | #4d69ff 97 | 98 | 99 | #e1f5f3 100 | #b3e5fc 101 | #81d4fa 102 | #4fc3f7 103 | #29b6f6 104 | #03a9f4 105 | #039be5 106 | #0288d1 107 | #0277bd 108 | #01579b 109 | #80d8ff 110 | #40c4ff 111 | #00b0ff 112 | #0091ea 113 | 114 | 115 | #e0f7fa 116 | #b2ebf2 117 | #80deea 118 | #4dd0e1 119 | #26c6da 120 | #00bcd4 121 | #00acc1 122 | #0097a7 123 | #00838f 124 | #006064 125 | #84ffff 126 | #18ffff 127 | #00e5ff 128 | #00b8d4 129 | 130 | 131 | #e0f2f1 132 | #b2dfdb 133 | #80cbc4 134 | #4db6ac 135 | #26a69a 136 | #009688 137 | #00897b 138 | #00796b 139 | #00695c 140 | #004d40 141 | #a7ffeb 142 | #64ffda 143 | #1de9b6 144 | #00bfa5 145 | 146 | 147 | #d0f8ce 148 | #a3e9a4 149 | #72d572 150 | #42bd41 151 | #2baf2b 152 | #259b24 153 | #0a8f08 154 | #0a7e07 155 | #056f00 156 | #0d5302 157 | #a2f78d 158 | #5af158 159 | #14e715 160 | #12c700 161 | 162 | 163 | #f1f8e9 164 | #dcedc8 165 | #c5e1a5 166 | #aed581 167 | #9ccc65 168 | #8bc34a 169 | #7cb342 170 | #689f38 171 | #558b2f 172 | #33691e 173 | #ccff90 174 | #b2ff59 175 | #76ff03 176 | #64dd17 177 | 178 | 179 | #f9fbe7 180 | #f0f4c3 181 | #e6ee9c 182 | #dce775 183 | #d4e157 184 | #cddc39 185 | #c0ca33 186 | #afb42b 187 | #9e9d24 188 | #827717 189 | #f4ff81 190 | #eeff41 191 | #c6ff00 192 | #aeea00 193 | 194 | 195 | #fffde7 196 | #fff9c4 197 | #fff59d 198 | #fff176 199 | #ffee58 200 | #ffeb3b 201 | #fdd835 202 | #fbc02d 203 | #f9a825 204 | #f57f17 205 | #ffff8d 206 | #ffff00 207 | #ffea00 208 | #ffd600 209 | 210 | 211 | #fff8e1 212 | #ffecb3 213 | #ffe082 214 | #ffd54f 215 | #ffca28 216 | #ffc107 217 | #ffb300 218 | #ffa000 219 | #ff8f00 220 | #ff6f00 221 | #ffe57f 222 | #ffd740 223 | #ffc400 224 | #ffab00 225 | 226 | 227 | #fff3e0 228 | #ffe0b2 229 | #ffcc80 230 | #ffb74d 231 | #ffa726 232 | #ff9800 233 | #fb8c00 234 | #f57c00 235 | #ef6c00 236 | #e65100 237 | #ffd180 238 | #ffab40 239 | #ff9100 240 | #ff6d00 241 | 242 | 243 | #fbe9e7 244 | #ffccbc 245 | #ffab91 246 | #ff8a65 247 | #ff7043 248 | #ff5722 249 | #f4511e 250 | #e64a19 251 | #d84315 252 | #bf360c 253 | #ff9e80 254 | #ff6e40 255 | #ff3d00 256 | #dd2c00 257 | 258 | 259 | #efebe9 260 | #d7ccc8 261 | #bcaaa4 262 | #a1887f 263 | #8d6e63 264 | #795548 265 | #6d4c41 266 | #5d4037 267 | #4e342e 268 | #3e2723 269 | 270 | 271 | #ffffff 272 | #fafafa 273 | #f5f5f5 274 | #eeeeee 275 | #e0e0e0 276 | #bdbdbd 277 | #9e9e9e 278 | #757575 279 | #616161 280 | #424242 281 | #212121 282 | #ffffff 283 | 284 | 285 | #eceff1 286 | #cfd8dc 287 | #b0bef5 288 | #90a4ae 289 | #78909c 290 | #607d8b 291 | #546e7a 292 | #455a64 293 | #37474f 294 | #263238 295 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WheelTest 5 | Hello world! 6 | Settings 7 | 8 | 9 | -------------------------------------------------------------------------------- /WheelViewSample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.1.3' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | google() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=0.4.2 2 | VERSION_CODE=3 3 | GROUP=com.github.lukedeighton 4 | 5 | POM_DESCRIPTION=An Android Widget for selecting items that rotate on a wheel 6 | POM_URL=https://github.com/LukeDeighton/WheelView 7 | POM_SCM_URL=https://github.com/LukeDeighton/WheelView 8 | POM_SCM_CONNECTION=scm:git@github.com:LukeDeighton/WheelView.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:LukeDeighton/WheelView.git 10 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 11 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=lukedeighton 14 | POM_DEVELOPER_NAME=Luke Deighton 15 | POM_DEVELOPER_EMAIL=lukedeighton1@gmail.com -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeDeighton/WheelView/0949e04df02f7ad43e1c0384542fa4c9bebe5161/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jun 28 21:25:55 BST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':WheelViewLib', ':WheelViewSample' 2 | --------------------------------------------------------------------------------