├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── src
│ ├── androidTest
│ │ └── java
│ │ │ ├── chen
│ │ │ └── you
│ │ │ │ └── wheelviewtest
│ │ │ │ └── ExampleInstrumentedTest.java
│ │ │ └── you
│ │ │ └── xiaochen
│ │ │ └── wheel
│ │ │ └── ExampleInstrumentedTest.java
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── chen
│ │ │ │ └── you
│ │ │ │ └── wheelviewtest
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── TestDatas.java
│ │ │ │ └── TwoActivity.java
│ │ └── res
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ │ ├── layout
│ │ │ ├── act_test.xml
│ │ │ ├── activity_main.xml
│ │ │ └── activity_two.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ │ └── values
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ └── test
│ │ └── java
│ │ ├── chen
│ │ └── you
│ │ │ └── wheelviewtest
│ │ │ └── ExampleUnitTest.java
│ │ └── you
│ │ └── xiaochen
│ │ └── wheel
│ │ └── ExampleUnitTest.java
└── you.jks
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── imgs
├── GIF111.gif
├── GIF222.gif
├── demo.apk
└── 原理图.png
├── settings.gradle
├── wheelview.apk
└── wheelview
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── chen
│ └── you
│ └── wheel
│ └── ExampleInstrumentedTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── chen
│ │ └── you
│ │ └── wheel
│ │ ├── LinearDrawManager.java
│ │ ├── WheelDrawManager.java
│ │ ├── WheelParams.java
│ │ └── WheelView.java
└── res
│ └── values
│ └── wheelview_attrs.xml
└── test
└── java
└── chen
└── you
└── wheel
└── ExampleUnitTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.DS_Store
3 | /.gradle
4 | /.idea
5 | /build
6 | /gradlew
7 | /gradlew.bat
8 | /local.properties
9 | /captures
10 | .externalNativeBuild
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WheelView-3d
2 | ### WheelView extends ViewGroup, Camera, Matrix 实现WheelView 3d效果
3 |
4 | ## 使用, 1.4.1版本有大改动, 使用1.4.1之前最新的版本号为 1.3.0
5 | ```
6 | allprojects {
7 | repositories {
8 | ...
9 | maven { url 'https://jitpack.io' }
10 | }
11 | }
12 |
13 | dependencies {
14 | implementation 'com.github.youxiaochen:WheelView-3d:1.4.1'
15 | }
16 | ```
17 |
18 | ### 如果要实现比较复杂的任何布局方式整个childView旋转效果可以参考博客 http://www.jianshu.com/p/41e4602deca6
19 |
20 | ### 博客地址 http://www.jianshu.com/p/77656dbb07b2
21 |
22 | #### [测试包下载](imgs/demo.apk)
23 |
24 | 
25 |
26 |
27 |
28 | 
29 |
30 | 
31 |
32 |
33 | ```
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ```
67 |
68 | ### 布局生成WheelView使用方式, 不设置时使用将使用默认值
69 |
70 | ```
71 |
86 |
87 | wv.setAdapter(new WheelView.Adapter() {
88 | @Override
89 | protected String getItem(int position) {
90 | return "position " + position;
91 | }
92 |
93 | @Override
94 | protected int getItemCount() {
95 | return 100;
96 | }
97 | });
98 | ```
99 |
100 | ### 代码生成WheelView及更多使用方式, WheelParams, DrawManager, ItemPainter都有默认值可不设置
101 | ```
102 | WheelParams params = new WheelParams.Builder()
103 | .setOrientation(WheelParams.HORIZONTAL)
104 | .setItemSize(...)
105 | .setTextColor(...)
106 | WheelView wv = new WheelView(context, params);
107 | ...
108 | //代码设置各种属性
109 | //亦可用此方式设置各属性
110 | wv.getWheelParams().newBuilder().setOrientation(...)
111 | wv.setWheelParams(params);
112 | //设置绘制管理, 默认为WheelDrawManager产生3D旋转, 亦可设置LinearDrawManager不旋转, 也可自定义DrawManager扩展
113 | wv.setDrawManager(new WheelDrawManager());
114 | //设置绘制器, 默认为SimpleItemPainter, 也可自定义绘制器扩展
115 | wv.setItemPainter(...)
116 | ```
117 |
118 | ### E-mail-QQ: 86207610@qq.com W: 86207610 Welcome to star
119 |
120 | #### 博客地址 http://www.jianshu.com/p/77656dbb07b2
121 |
122 | #### 如果要实现比较复杂的任何布局方式整个childView旋转效果可以参考博客 http://www.jianshu.com/p/41e4602deca6
123 |
124 |
125 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | /build
3 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 | buildToolsVersion "29.0.2"
6 | defaultConfig {
7 | applicationId "chen.you.wheelviewtest"
8 | minSdkVersion 16
9 | targetSdkVersion 29
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 | signingConfigs {
15 | debug {
16 | keyAlias 'you'
17 | keyPassword '123456'
18 | storeFile file('you.jks')
19 | storePassword '123456'
20 | }
21 | release {
22 | keyAlias 'you'
23 | keyPassword '123456'
24 | storeFile file('you.jks')
25 | storePassword '123456'
26 | }
27 | }
28 |
29 | buildTypes {
30 | debug {
31 | versionNameSuffix "-debug"
32 | minifyEnabled false
33 | zipAlignEnabled false
34 | shrinkResources false
35 | signingConfig signingConfigs.debug
36 | }
37 |
38 | release {
39 | //是否启动混淆
40 | minifyEnabled true
41 | //Zipalign优化
42 | zipAlignEnabled true
43 | // 移除无用的resource文件
44 | shrinkResources true
45 | signingConfig signingConfigs.release
46 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
47 | }
48 |
49 | android.applicationVariants.all { variant ->
50 | variant.outputs.all {
51 | outputFileName = "You_" + "v${variant.versionName}.apk"
52 | }
53 | }
54 | }
55 | }
56 |
57 | dependencies {
58 | implementation fileTree(dir: 'libs', include: ['*.jar'])
59 | implementation 'androidx.appcompat:appcompat:1.1.0'
60 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
61 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
62 | testImplementation 'junit:junit:4.12'
63 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
65 | implementation project(path: ':wheelview')
66 | }
67 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/chen/you/wheelviewtest/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheelviewtest;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("chen.you.wheelviewtest", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/you/xiaochen/wheel/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package you.xiaochen.wheel;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("you.xiaochen.wheel", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/chen/you/wheelviewtest/MainActivity.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheelviewtest;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.view.View;
6 | import android.widget.TextView;
7 |
8 | import androidx.appcompat.app.AppCompatActivity;
9 |
10 | import java.util.ArrayList;
11 | import java.util.Arrays;
12 | import java.util.List;
13 |
14 | import chen.you.wheel.WheelView;
15 |
16 | public class MainActivity extends AppCompatActivity {
17 |
18 | private WheelView wv_city, wv_county, wv_name;
19 |
20 | private CityAdapter cityAdapter;
21 | private CountyAdapter countyAdapter;
22 |
23 | private TextView tv_city, tv_county, tv_number;
24 |
25 | private WheelView wv_number;
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | setContentView(R.layout.activity_main);
31 |
32 | setTitle("游小陈的博客");
33 | wv_city = findViewById(R.id.wv_city);
34 | wv_county = findViewById(R.id.wv_county);
35 | wv_number = findViewById(R.id.wv_number);
36 | wv_name = findViewById(R.id.wv_name);
37 | tv_city = findViewById(R.id.tv_city);
38 | tv_county = findViewById(R.id.tv_county);
39 | tv_number = findViewById(R.id.tv_number);
40 |
41 |
42 | /* 市滑轮控件 */
43 |
44 | cityAdapter = new CityAdapter();
45 | wv_city.setAdapter(cityAdapter);
46 | wv_city.addOnItemSelectedListener(new WheelView.OnItemSelectedListener() {
47 | @Override
48 | public void onItemSelected(WheelView wheelView, int index) {
49 | tv_city.setText("市: "+cityAdapter.getItem(index));
50 | List strs = Arrays.asList(TestDatas.AREAS[index]);
51 | countyAdapter.strs.clear();
52 | countyAdapter.strs.addAll(strs);
53 | countyAdapter.notifyDataSetChanged();
54 | wv_county.setCurrentItem(0);
55 | tv_county.setText("县: "+countyAdapter.getItem(0));
56 | }
57 | });
58 |
59 | /* 区滑轮控件 */
60 |
61 | wv_county.addOnItemSelectedListener(new WheelView.OnItemSelectedListener() {
62 | @Override
63 | public void onItemSelected(WheelView wheelView, int index) {
64 | tv_county.setText("县: "+countyAdapter.getItem(index));
65 | }
66 | });
67 |
68 | countyAdapter = new CountyAdapter();
69 | wv_county.setAdapter(countyAdapter);
70 |
71 | /* 名字适配 */
72 | wv_name.setAdapter(new WheelView.Adapter() {
73 | @Override
74 | public int getItemCount() {
75 | return 20;
76 | }
77 |
78 | @Override
79 | public String getItem(int position) {
80 | return "游小陈";
81 | }
82 | });
83 |
84 | /* 水平滑轮控件 */
85 | wv_number.setAdapter(new WheelView.Adapter() {
86 | @Override
87 | public int getItemCount() {
88 | return 100;
89 | }
90 |
91 | @Override
92 | public String getItem(int position) {
93 | return String.valueOf(position);
94 | }
95 | });
96 | wv_number.addOnItemSelectedListener(new WheelView.OnItemSelectedListener() {
97 | @Override
98 | public void onItemSelected(WheelView wheelView, int index) {
99 | tv_number.setText("水平布局"+index);
100 | }
101 | });
102 | wv_number.setCurrentItem(88);
103 |
104 | findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
105 | @Override
106 | public void onClick(View v) {
107 | startActivity(new Intent(MainActivity.this, TwoActivity.class));
108 | }
109 | });
110 | }
111 |
112 |
113 |
114 | private class CityAdapter extends WheelView.Adapter {
115 | @Override
116 | public int getItemCount() {
117 | return TestDatas.NAMES.length;
118 | }
119 |
120 | @Override
121 | public String getItem(int position) {
122 | return TestDatas.NAMES[position];
123 | }
124 | }
125 |
126 | private class CountyAdapter extends WheelView.Adapter {
127 |
128 | private List strs;
129 |
130 | CountyAdapter() {
131 | strs = new ArrayList<>();
132 | }
133 |
134 | @Override
135 | public int getItemCount() {
136 | return strs.size();
137 | }
138 |
139 | @Override
140 | public String getItem(int position) {
141 | return strs.get(position);
142 | }
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/app/src/main/java/chen/you/wheelviewtest/TestDatas.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheelviewtest;
2 |
3 | /**
4 | * Created by you on 2017/9/20.
5 | */
6 |
7 | public final class TestDatas {
8 |
9 | public static String[] NAMES = {"南昌", "赣州", "九江", "上饶", "宜春", "抚州", "吉安", "景德镇", "萍乡", "鹰潭"};
10 |
11 |
12 | public static String[][] AREAS = {{"东湖区", "东湖区", "青云谱区", "湾里区", "青山湖区", "南昌县", "安义县", "进贤县"},
13 | {"章贡区",
14 | "瑞金市",
15 | "南康市",
16 | "石城县",
17 | "安远县",
18 | "赣县",
19 | "宁都县",
20 | "寻乌县",
21 | "兴国县",
22 | "定南县",
23 | "上犹县",
24 | "于都县",
25 | "龙南县",
26 | "崇义县",
27 | "信丰县",
28 | "全南县",
29 | "大余县",
30 | "会昌县"},
31 | {"浔阳区",
32 | "庐山区",
33 | "瑞昌市",
34 | "九江县",
35 | "星子县",
36 | "武宁县",
37 | "彭泽县",
38 | "永修县",
39 | "修水县",
40 | "湖口县",
41 | "德安县",
42 | "都昌县"},
43 | {"信州区",
44 | "德兴市",
45 | "上饶县",
46 | "广丰县",
47 | "鄱阳县",
48 | "婺源县",
49 | "铅山县",
50 | "余干县",
51 | "横峰县",
52 | "弋阳县",
53 | "玉山县",
54 | "万年县"},
55 | {"袁州区",
56 | "丰城市",
57 | "樟树市",
58 | "高安市",
59 | "铜鼓县",
60 | "靖安县",
61 | "宜丰县",
62 | "奉新县",
63 | "万载县",
64 | "上高县"},
65 | {"临川区",
66 | "南丰县",
67 | "乐安县",
68 | "金溪县",
69 | "南城县",
70 | "东乡县",
71 | "资溪县",
72 | "宜黄县",
73 | "广昌县",
74 | "黎川县",
75 | "崇仁县"},
76 | {"吉州区",
77 | "青原区",
78 | "井冈山市",
79 | "吉安县",
80 | "永丰县",
81 | "永新县",
82 | "新干县",
83 | "泰和县",
84 | "峡江县",
85 | "遂川县",
86 | "安福县",
87 | "吉水县",
88 | "万安县"},
89 | {"珠山区",
90 | "昌江区",
91 | "乐平市",
92 | "浮梁县"},
93 | {"安源区",
94 | "湘东区",
95 | "莲花县",
96 | "上栗县",
97 | "芦溪县"},
98 | {"月湖区",
99 | "贵溪市",
100 | "余江县"}
101 |
102 | };
103 |
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/java/chen/you/wheelviewtest/TwoActivity.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheelviewtest;
2 |
3 | import android.graphics.Color;
4 | import android.os.Bundle;
5 | import android.view.View;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.appcompat.app.AppCompatActivity;
9 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | import chen.you.wheel.LinearDrawManager;
14 | import chen.you.wheel.WheelDrawManager;
15 | import chen.you.wheel.WheelParams;
16 | import chen.you.wheel.WheelView;
17 |
18 | /**
19 | * author: you : 2022/12/8
20 | */
21 | public final class TwoActivity extends AppCompatActivity implements View.OnClickListener {
22 |
23 | private WheelView wv;
24 |
25 | private TestAdapter adapter;
26 |
27 | private List tests = new ArrayList<>();
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.activity_two);
33 | wv = findViewById(R.id.wv_city);
34 | for (int i = 0; i < 100; i++) {
35 | tests.add(String.valueOf(i));
36 | }
37 | adapter = new TestAdapter(tests);
38 | wv.setAdapter(adapter);
39 | wv.setCurrentItem(10);
40 |
41 | findViewById(R.id.bt0).setOnClickListener(this);
42 | findViewById(R.id.bt1).setOnClickListener(this);
43 | findViewById(R.id.bt2).setOnClickListener(this);
44 | findViewById(R.id.bt3).setOnClickListener(this);
45 |
46 | }
47 |
48 | @Override
49 | public void onClick(View v) {
50 | switch (v.getId()) {
51 | case R.id.bt0:
52 | tests.clear();
53 | for (int i = 0; i < 100; i++) {
54 | tests.add("new test " + i);
55 | }
56 | adapter.notifyDataSetChanged();
57 | break;
58 | case R.id.bt1:
59 | // WheelParams params0 = new WheelParams.Builder().setOrientation(WheelParams.HORIZONTAL).setTextCenterColor(Color.RED).build();
60 | WheelParams params = wv.getWheelParams().newBuilder().setOrientation(WheelParams.HORIZONTAL).setTextCenterColor(Color.RED).build();
61 | wv.setWheelParams(params);
62 | break;
63 | case R.id.bt2:
64 | if (wv.getDrawManager() instanceof LinearDrawManager) {
65 | wv.setDrawManager(new WheelDrawManager());
66 | } else {
67 | wv.setDrawManager(new LinearDrawManager());
68 | }
69 | break;
70 | case R.id.bt3:
71 | wv.setAdapter(new WheelView.Adapter() {
72 | @Override
73 | public int getItemCount() {
74 | return 100;
75 | }
76 |
77 | @NonNull
78 | @Override
79 | public String getItem(int position) {
80 | return "position " + position;
81 | }
82 | });
83 | break;
84 | }
85 | }
86 |
87 | static class TestAdapter extends WheelView.Adapter {
88 |
89 | List strs;
90 |
91 | public TestAdapter(List strs) {
92 | this.strs = strs;
93 | }
94 |
95 | @Override
96 | public int getItemCount() {
97 | return strs.size();
98 | }
99 |
100 | @Override
101 | public String getItem(int position) {
102 | return strs.get(position);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/act_test.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
15 |
16 |
20 |
21 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
21 |
22 |
29 |
30 |
37 |
38 |
39 |
40 |
45 |
46 |
50 |
51 |
55 |
56 |
61 |
62 |
63 |
64 |
74 |
75 |
85 |
86 |
96 |
97 |
98 |
99 |
106 |
107 |
117 |
118 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_two.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
23 |
24 |
29 |
30 |
35 |
36 |
41 |
42 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
7 | 60dp
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WheelViewTest
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/chen/you/wheelviewtest/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheelviewtest;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/test/java/you/xiaochen/wheel/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package you.xiaochen.wheel;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/you.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/app/you.jks
--------------------------------------------------------------------------------
/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 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:4.1.0'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 |
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/imgs/GIF111.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/imgs/GIF111.gif
--------------------------------------------------------------------------------
/imgs/GIF222.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/imgs/GIF222.gif
--------------------------------------------------------------------------------
/imgs/demo.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/imgs/demo.apk
--------------------------------------------------------------------------------
/imgs/原理图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/imgs/原理图.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':wheelview'
2 | rootProject.name='WheelViewTest'
3 |
--------------------------------------------------------------------------------
/wheelview.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/wheelview.apk
--------------------------------------------------------------------------------
/wheelview/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | /build
3 |
--------------------------------------------------------------------------------
/wheelview/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 29
5 |
6 | defaultConfig {
7 | minSdkVersion 16
8 | targetSdkVersion 29
9 | }
10 |
11 | compileOptions {
12 | sourceCompatibility JavaVersion.VERSION_1_8
13 | targetCompatibility JavaVersion.VERSION_1_8
14 | }
15 |
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 |
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | testImplementation 'junit:junit:4.12'
28 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
29 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
30 |
31 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/wheelview/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/youxiaochen/WheelView-3d/1785e7dac4ac885f7aa0b60b6af006b70f4584c4/wheelview/consumer-rules.pro
--------------------------------------------------------------------------------
/wheelview/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/wheelview/src/androidTest/java/chen/you/wheel/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("chen.you.wheelview.test", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/wheelview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/wheelview/src/main/java/chen/you/wheel/LinearDrawManager.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 |
6 | import androidx.annotation.NonNull;
7 |
8 | /**
9 | * 线性Canvas装饰类, 不处理旋转, 可只处理alpha
10 | * Created by you on 2017/3/20.
11 | * 作QQ:86207610
12 | */
13 | public class LinearDrawManager extends WheelView.DrawManager {
14 |
15 | //用于计算item偏移值对应的alpha值
16 | private float maxCenterScrollOff;
17 | //中心偏移值即为itemSize / 2
18 | float centerItemScrollOff;
19 |
20 | @Override
21 | protected void setWheelParams(@NonNull WheelParams params) {
22 | super.setWheelParams(params);
23 | maxCenterScrollOff = (params.getShowItemCount() + 1) * params.itemSize;
24 | centerItemScrollOff = params.itemSize / 2.f;
25 | }
26 |
27 | @Override
28 | protected void decorationItem(@NonNull Canvas c, @NonNull Rect itemRect, int position, @NonNull String item) {
29 | float scrollOff; //相对中心的滑动偏移, 根据itemSize和偏移即可计算中离中心的比例和是否为中心item
30 | if (wheelParams.isVertical()) {
31 | scrollOff = wvRect.exactCenterY() - itemRect.exactCenterY();
32 | } else {
33 | scrollOff = wvRect.exactCenterX() - itemRect.exactCenterX();
34 | }
35 |
36 | //渐变处理
37 | int alpha = 255;
38 | if (wheelParams.gradient) {
39 | alpha = Math.max(255 - (int) (Math.abs(scrollOff) * 255 / maxCenterScrollOff), 0);
40 | if (alpha <= 0) return;
41 | }
42 |
43 | //中心计算
44 | boolean isCenterItem = false;
45 | if (centerItemPosition == WheelView.IDLE_POSITION) {
46 | isCenterItem = Math.abs(scrollOff) <= centerItemScrollOff;
47 | if (isCenterItem) {
48 | centerItemPosition = position;
49 | }
50 | }
51 | if (isCenterItem) {
52 | getItemPainter().drawCenterItem(c, itemRect, alpha, item);
53 | } else {
54 | getItemPainter().drawItem(c, itemRect, alpha, item);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/wheelview/src/main/java/chen/you/wheel/WheelDrawManager.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import android.graphics.Camera;
4 | import android.graphics.Canvas;
5 | import android.graphics.Matrix;
6 | import android.graphics.Rect;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | /**
11 | * 3D滚轮旋转Canvas处理类
12 | * Created by you on 2017/3/20.
13 | * 作QQ:86207610
14 | */
15 | public class WheelDrawManager extends WheelView.DrawManager {
16 | /**
17 | * 保留2个相素让RecyclerView的顶部和底部可以多预画一个item, {@link WheelDrawManager.WheelItemShowOrder}
18 | */
19 | private static final int SHOW_ORDER_OFFSET = 2;
20 | //此参数影响左右旋转对齐时的效果,系数越大,越明显(0-1之间)
21 | private static final float DEF_SCALE = 0.75F;
22 | //3D旋转
23 | final Camera camera;
24 | final Matrix matrix;
25 | //每个item平均下来后对应的旋转角度 根据中间分割线上下item和中间总数量计算每个item对应的旋转角度
26 | float itemDegree;
27 | //滑动轴的半径,旋转的偏移需要通过此参数和itemDegree来计算
28 | float wheelRadio;
29 | //中心偏移值即为itemSize / 2
30 | float centerItemScrollOff;
31 |
32 | final WheelItemShowOrder itemShowOrder = new WheelItemShowOrder();
33 |
34 | public WheelDrawManager() {
35 | camera = new Camera();
36 | matrix = new Matrix();
37 | }
38 |
39 | @Override
40 | protected void setWheelParams(@NonNull WheelParams params) {
41 | super.setWheelParams(params);
42 | this.itemDegree = 180.f / (params.itemCount * 2 + 1);
43 | this.wheelRadio = (float) ((params.itemSize / 2.f) / Math.tan(Math.toRadians(itemDegree / 2.f)));
44 | centerItemScrollOff = params.itemSize / 2.f;
45 | }
46 |
47 | @Override
48 | protected void decorationItem(@NonNull Canvas c, @NonNull Rect itemRect, int position, @NonNull String item) {
49 | if (wheelParams.isVertical()) {
50 | decorationVerticalItem(c, position, item);
51 | } else {
52 | decorationHorizontalItem(c, position, item);
53 | }
54 | }
55 |
56 | @Override
57 | protected WheelParams.ItemShowOrder getShowOrder() {
58 | return itemShowOrder;
59 | }
60 |
61 | private void decorationVerticalItem(Canvas c, int position, String item) {
62 | float itemCenterY = itemRect.exactCenterY();
63 | float scrollOffY = itemCenterY - wvRect.exactCenterY();
64 | if (Math.abs(scrollOffY) <= SHOW_ORDER_OFFSET) { //正中心
65 | centerItemPosition = position;
66 | getItemPainter().drawCenterItem(c, itemRect, 255, item);
67 | return;
68 | }
69 | float rotateDegreeX = scrollOffY * itemDegree / wheelParams.itemSize;//垂直布局时要以X轴为中心旋转
70 |
71 | //旋转后的渐变处理
72 | int alpha = 255;
73 | if (wheelParams.gradient) {
74 | alpha = degreeAlpha(rotateDegreeX);
75 | if (alpha <= 0) return;
76 | }
77 |
78 | float rotateSinX = (float) Math.sin(Math.toRadians(rotateDegreeX));
79 | float rotateOffY = scrollOffY - wheelRadio * rotateSinX;//因旋转导致界面视角的偏移
80 | //Log.d("you", "drawVerticalItem degree " + rotateDegreeX);
81 | //计算中心item, 优先最靠近中心区域的为中心点
82 | boolean isCenterItem = false;
83 | if (centerItemPosition == WheelView.IDLE_POSITION) {
84 | isCenterItem = Math.abs(scrollOffY) <= centerItemScrollOff;
85 | if (isCenterItem) {
86 | centerItemPosition = position;
87 | }
88 | }
89 |
90 | c.save();
91 | c.translate(0.0f, -rotateOffY);//因旋转导致界面视角的偏移
92 | camera.save();
93 |
94 | //旋转时离视角的z轴方向也会变化,先移动Z轴再旋转
95 | float z = (float) (wheelRadio * (1 - Math.abs(Math.cos(Math.toRadians(rotateDegreeX)))));
96 | camera.translate(0, 0, z);
97 |
98 | camera.rotateX(-rotateDegreeX);
99 | camera.getMatrix(matrix);
100 | camera.restore();
101 |
102 | //根据对齐方式,计算出垂直布局时X轴移动的位置
103 | float translateX = wvRect.exactCenterX();
104 | if (wheelParams.gravity == WheelParams.LEFT) {
105 | translateX *= 1 + DEF_SCALE;
106 | } else if (wheelParams.gravity == WheelParams.RIGHT) {
107 | translateX *= 1 - DEF_SCALE;
108 | }
109 | matrix.preTranslate(-translateX, -itemCenterY);
110 | matrix.postTranslate(translateX, itemCenterY);
111 | c.concat(matrix);
112 | if (isCenterItem) {
113 | getItemPainter().drawCenterItem(c, itemRect, alpha, item);
114 | } else {
115 | getItemPainter().drawItem(c, itemRect, alpha, item);
116 | }
117 | c.restore();
118 | }
119 |
120 | private void decorationHorizontalItem(Canvas c, int position, String item) {
121 | float itemCenterX = itemRect.exactCenterX();
122 | float scrollOffX = itemCenterX - wvRect.exactCenterX();
123 | if (Math.abs(scrollOffX) <= SHOW_ORDER_OFFSET) { //正中心
124 | centerItemPosition = position;
125 | getItemPainter().drawCenterItem(c, itemRect, 255, item);
126 | return;
127 | }
128 | float rotateDegreeY = scrollOffX * itemDegree / wheelParams.itemSize;//垂直布局时要以Y轴为中心旋转
129 |
130 | //旋转后的渐变处理
131 | int alpha = 255;
132 | if (wheelParams.gradient) {
133 | alpha = degreeAlpha(rotateDegreeY);
134 | if (alpha <= 0) return;
135 | }
136 |
137 | float rotateSinY = (float) Math.sin(Math.toRadians(rotateDegreeY));
138 | float rotateOffX = scrollOffX - wheelRadio * rotateSinY;//因旋转导致界面视角的偏移
139 | //Log.d("you", "drawHorizontalItem degree " + rotateDegreeY);
140 |
141 | boolean isCenterItem = false;
142 | if (centerItemPosition == WheelView.IDLE_POSITION) {
143 | isCenterItem = Math.abs(scrollOffX) <= centerItemScrollOff;
144 | if (isCenterItem) {
145 | centerItemPosition = position;
146 | }
147 | }
148 |
149 | c.save();
150 | c.translate(-rotateOffX, 0.0f);
151 | camera.save();
152 |
153 | float z = (float) (wheelRadio * (1 - Math.abs(Math.cos(Math.toRadians(rotateDegreeY)))));
154 | camera.translate(0, 0, z);
155 | camera.rotateY(rotateDegreeY);
156 | camera.getMatrix(matrix);
157 | camera.restore();
158 | float parentCenterY = wvRect.exactCenterY();
159 | matrix.preTranslate(-itemCenterX, -parentCenterY);
160 | matrix.postTranslate(itemCenterX, parentCenterY);
161 | c.concat(matrix);
162 | if (isCenterItem) {
163 | getItemPainter().drawCenterItem(c, itemRect, alpha, item);
164 | } else {
165 | getItemPainter().drawItem(c, itemRect, alpha, item);
166 | }
167 | c.restore();
168 | }
169 |
170 | /**
171 | * 旋转大于90度时,完全透明
172 | * @param degree 旋转角度
173 | * @return 旋转后的透明度
174 | */
175 | private int degreeAlpha(float degree) {
176 | degree = Math.abs(degree);
177 | if (degree >= 90) return 0;
178 | float al = (90 - degree) / 90;
179 | return (int) (255 * al);
180 | }
181 |
182 | static class WheelItemShowOrder implements WheelParams.ItemShowOrder {
183 | //通过计算,在item3个或者以上的时候,旋转后的WheelView高度会比实际高度小一个item的大小
184 | //计算方式可以详见博客中的原理图,计算出三角形的腰长即为半径
185 | //item在6个或者以上时,旋转后的高度会相差大于2, 因此3-5个时的效果最好,不会留过多的空白区域
186 | //此时防止WheelView旋转后的空白区域过多,可以适当修改大小, 适配器中头和尾添加的itemCount - 1
187 | //showItemCout 只能 -1, ItemDecoration超出屏幕外的不会draw
188 | @Override
189 | public int getShowItemCount(int itemCount) {
190 | return itemCount > 2 ? itemCount - 1 : itemCount;
191 | }
192 |
193 | //RecyclerView高度或者水平时的宽度添加2个像素是为了在减掉显示的一个item时,
194 | // 超出RecyclerView显示区域刚好可以再显示头部一个和尾部一个item
195 | @Override
196 | public int getTotalItemSize(int showItemCount, int itemSize) {
197 | return (showItemCount * 2 + 1) * itemSize + SHOW_ORDER_OFFSET;
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/wheelview/src/main/java/chen/you/wheel/WheelParams.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Color;
7 | import android.util.AttributeSet;
8 |
9 | import androidx.annotation.ColorInt;
10 | import androidx.annotation.IntDef;
11 | import androidx.recyclerview.widget.LinearLayoutManager;
12 | import androidx.recyclerview.widget.RecyclerView;
13 |
14 | import java.lang.annotation.Retention;
15 | import java.lang.annotation.RetentionPolicy;
16 |
17 | /**
18 | * WheelView 相关参数类属性
19 | * Created by you on 2017/3/20.
20 | * 作QQ:86207610
21 | */
22 | public final class WheelParams {
23 | /**
24 | * 垂直与水平布局两种状态
25 | */
26 | public static final int VERTICAL = 1;
27 | public static final int HORIZONTAL = 0;
28 | @IntDef({HORIZONTAL, VERTICAL})
29 | @Retention(RetentionPolicy.SOURCE)
30 | public @interface Orientation {
31 | }
32 | /**
33 | * 垂直布局时的靠左,居中,靠右立体效果
34 | */
35 | public static final int CENTER = 0;
36 | public static final int LEFT = 1;
37 | public static final int RIGHT = 2;
38 | @IntDef({CENTER, LEFT, RIGHT})
39 | @Retention(RetentionPolicy.SOURCE)
40 | public @interface Gravity {
41 | }
42 | /**
43 | * 一些默认参数大小
44 | */
45 | public static final int DEF_ITEM_COUNT = 3;
46 | public static final int DEF_ITEM_SIZE = dp2px(40);
47 | public static final int DEF_TEXT_SIZE = sp2px(18);
48 | public static final int DEF_DIVIDER_SIZE = dp2px(1);
49 |
50 | static int dp2px(float dp) {
51 | final float density = Resources.getSystem().getDisplayMetrics().density;
52 | return (int) (dp * density + 0.5f);
53 | }
54 |
55 | static int sp2px(float sp) {
56 | final float fontDensity = Resources.getSystem().getDisplayMetrics().scaledDensity;
57 | return (int) (sp * fontDensity + 0.5f);
58 | }
59 |
60 | //布局方向
61 | public final @Orientation int orientation;
62 | //item数量
63 | public final int itemCount;
64 | //item大小
65 | public final int itemSize;
66 | //对齐方式
67 | public final @Gravity int gravity;
68 | //item文字大小, 如果中心文字大小需要调整可以用Canvas缩放
69 | public final float textSize;
70 | //item文字颜色
71 | public final @ColorInt int textColor;
72 | //中心文字颜色
73 | public final @ColorInt int textCenterColor;
74 | //分割线大小
75 | public final int dividerSize;
76 | //分割线颜色
77 | public final @ColorInt int dividerColor;
78 | //透明度渐变
79 | public final boolean gradient;
80 | //分割线填充值默认为0, 分割线矩阵大小为itemSize + dividerPadding
81 | public final int dividerPadding;
82 | //实际显示在界面上的itemCount
83 | private int showItemCount;
84 | //ItemShowOrder
85 | private ItemShowOrder itemShowOrder;
86 |
87 | private WheelParams(Builder builder) {
88 | this.orientation = builder.orientation;
89 | this.itemCount = builder.itemCount;
90 | this.itemSize = builder.itemSize;
91 | this.gravity = builder.gravity;
92 | this.textSize = builder.textSize;
93 | this.textColor = builder.textColor;
94 | this.textCenterColor = builder.textCenterColor;
95 | this.gradient = builder.gradient;
96 | this.dividerSize = builder.dividerSize;
97 | this.dividerColor = builder.dividerColor;
98 | this.dividerPadding = builder.dividerPadding;
99 | this.showItemCount = itemCount;
100 | }
101 |
102 | public Builder newBuilder() {
103 | return new Builder(this);
104 | }
105 |
106 | public int getShowItemCount() {
107 | return showItemCount;
108 | }
109 |
110 | @RecyclerView.Orientation public int getLayoutOrientation() {
111 | return orientation == VERTICAL ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL;
112 | }
113 |
114 | boolean isVertical() {
115 | return orientation == VERTICAL;
116 | }
117 |
118 | int getTotalItemSize() {
119 | if (itemShowOrder != null) return itemShowOrder.getTotalItemSize(showItemCount, itemSize);
120 | //中间项 + 上下itemCount
121 | return (showItemCount * 2 + 1) * itemSize;
122 | }
123 |
124 | void setItemShowOrder(ItemShowOrder itemShowOrder) {
125 | this.itemShowOrder = itemShowOrder;
126 | this.showItemCount = itemShowOrder == null ? itemCount : itemShowOrder.getShowItemCount(itemCount);
127 | }
128 |
129 | /**
130 | * 界面显示规则, 3D旋转后由于圆形直径相比非旋转时的大小相差很大, 因此需要对显示数量和大小进行修改
131 | */
132 | public interface ItemShowOrder {
133 |
134 | int getShowItemCount(int itemCount);
135 |
136 | int getTotalItemSize(int showItemCount, int itemSize);
137 | }
138 |
139 | public final static class Builder {
140 | //布局方向
141 | @Orientation int orientation = VERTICAL;
142 | //item数量
143 | int itemCount = DEF_ITEM_COUNT;
144 | //item大小
145 | int itemSize = DEF_ITEM_SIZE;
146 | //对齐方式
147 | @Gravity int gravity = CENTER;
148 | //item文字大小, 如果中心文字大小需要调整可以用Canvas缩放
149 | float textSize = DEF_TEXT_SIZE;
150 | //item文字颜色
151 | @ColorInt int textColor = Color.BLACK;
152 | //中心文字颜色
153 | @ColorInt int textCenterColor = Color.RED;
154 | //透明度渐变
155 | boolean gradient = true;
156 | //分割线大小
157 | int dividerSize = DEF_DIVIDER_SIZE;
158 | //分割线颜色
159 | @ColorInt int dividerColor = Color.RED;
160 | //分割线填充值默认为0, 分割线矩阵大小为itemSize + dividerPadding
161 | int dividerPadding = 0;
162 |
163 | public Builder() {
164 | }
165 |
166 | private Builder(WheelParams params) {
167 | this.orientation = params.orientation;
168 | this.itemCount = params.itemCount;
169 | this.itemSize = params.itemSize;
170 | this.gravity = params.gravity;
171 | this.textSize = params.textSize;
172 | this.textColor = params.textColor;
173 | this.textCenterColor = params.textCenterColor;
174 | this.dividerSize = params.dividerSize;
175 | this.dividerColor = params.dividerColor;
176 | this.gradient = params.gradient;
177 | this.dividerPadding = params.dividerPadding;
178 | }
179 |
180 | Builder(Context context, AttributeSet attrs) {
181 | if (attrs != null) {
182 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WheelView);
183 | orientation = a.getInt(R.styleable.WheelView_wheelOrientation, orientation);
184 | itemCount = a.getInt(R.styleable.WheelView_wheelItemCount, itemCount);
185 | itemSize = a.getDimensionPixelSize(R.styleable.WheelView_wheelItemSize, itemSize);
186 | gravity = a.getInt(R.styleable.WheelView_wheelGravity, gravity);
187 | textSize = a.getDimension(R.styleable.WheelView_wheelTextSize, textSize);
188 | textColor = a.getColor(R.styleable.WheelView_wheelTextColor, textColor);
189 | textCenterColor = a.getColor(R.styleable.WheelView_wheelTextCenterColor, textCenterColor);
190 | gradient = a.getBoolean(R.styleable.WheelView_wheelGradient, gradient);
191 | dividerColor = a.getColor(R.styleable.WheelView_wheelDividerColor, dividerColor);
192 | dividerSize = a.getDimensionPixelOffset(R.styleable.WheelView_wheelDividerSize, dividerSize);
193 | dividerPadding = a.getDimensionPixelSize(R.styleable.WheelView_wheelDividerPadding, dividerPadding);
194 | a.recycle();
195 | }
196 | }
197 |
198 | public Builder setOrientation(@Orientation int orientation) {
199 | this.orientation = orientation;
200 | return this;
201 | }
202 |
203 | public Builder setItemCount(int itemCount) {
204 | this.itemCount = itemCount;
205 | return this;
206 | }
207 |
208 | public Builder setItemSize(int itemSize) {
209 | this.itemSize = itemSize;
210 | return this;
211 | }
212 |
213 | public Builder setGravity(@Gravity int gravity) {
214 | this.gravity = gravity;
215 | return this;
216 | }
217 |
218 | public Builder setTextSize(float textSize) {
219 | this.textSize = textSize;
220 | return this;
221 | }
222 |
223 | public Builder setTextColor(@ColorInt int textColor) {
224 | this.textColor = textColor;
225 | return this;
226 | }
227 |
228 | public Builder setTextCenterColor(@ColorInt int textCenterColor) {
229 | this.textCenterColor = textCenterColor;
230 | return this;
231 | }
232 |
233 | public Builder setGradient(boolean gradient) {
234 | this.gradient = gradient;
235 | return this;
236 | }
237 |
238 | public Builder setDividerSize(int dividerSize) {
239 | this.dividerSize = dividerSize;
240 | return this;
241 | }
242 |
243 | public Builder setDividerColor(@ColorInt int dividerColor) {
244 | this.dividerColor = dividerColor;
245 | return this;
246 | }
247 |
248 | public Builder setDividerPadding(int dividerPadding) {
249 | this.dividerPadding = dividerPadding;
250 | return this;
251 | }
252 |
253 | public WheelParams build() {
254 | if (itemCount <= 0) itemCount = DEF_ITEM_COUNT;
255 | if (itemSize <= 0) itemSize = DEF_ITEM_SIZE;
256 | if (textSize <= 0) textSize = DEF_TEXT_SIZE;
257 | if (dividerSize <= 0) dividerSize = DEF_DIVIDER_SIZE;
258 | return new WheelParams(this);
259 | }
260 | }
261 |
262 | }
263 |
--------------------------------------------------------------------------------
/wheelview/src/main/java/chen/you/wheel/WheelView.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.database.DataSetObserver;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 | import android.graphics.Rect;
9 | import android.text.TextPaint;
10 | import android.util.AttributeSet;
11 | import android.view.Gravity;
12 | import android.view.View;
13 | import android.view.ViewGroup;
14 |
15 | import androidx.annotation.CallSuper;
16 | import androidx.annotation.NonNull;
17 | import androidx.annotation.Nullable;
18 | import androidx.core.view.ViewCompat;
19 | import androidx.recyclerview.widget.LinearLayoutManager;
20 | import androidx.recyclerview.widget.LinearSnapHelper;
21 | import androidx.recyclerview.widget.RecyclerView;
22 |
23 | import java.util.ArrayList;
24 | import java.util.List;
25 |
26 | /**
27 | * 真正的3D WheelView, 内部核心类 {@link DrawManager}, {@link WheelParams}
28 | * Created by you on 2017/3/20.
29 | * 作QQ:86207610
30 | */
31 | public final class WheelView extends ViewGroup {
32 | //无效的位置
33 | public static final int IDLE_POSITION = -1;
34 | //没有指定宽或高时的默认大小
35 | private static final int DEF_SIZE = WheelParams.dp2px(128);
36 | //WheelView相关参数
37 | private WheelParams mWheelParams;
38 | private RecyclerView mRecyclerView;
39 | private LinearLayoutManager mLayoutManager;
40 |
41 | //绘制器管理
42 | private DrawManager mDrawManager;
43 | //RecyclerView adapter
44 | private WheelAdapter mWheelAdapter;
45 | //WheelView Adapter
46 | private Adapter mAdapter;
47 | private WheelViewObserver mObserver;
48 | //滑动监听用于selectedIndex回调
49 | private OnScrollListener mScrollListener;
50 |
51 | //当前选中的项
52 | private int mSelectedPosition = IDLE_POSITION;
53 | //itemSelected
54 | private List mSelectedListeners;
55 | //WheelView是否已经附着到窗体中
56 | private boolean hasAttachedToWindow = false;
57 |
58 | //当前ViewGroup测量的矩阵与子控件要显示的矩阵
59 | private final Rect mContainerRect = new Rect();
60 | private final Rect mChildRect = new Rect();
61 |
62 | public WheelView(@NonNull Context context) {
63 | super(context);
64 | initialize(context, null);
65 | }
66 |
67 | public WheelView(@NonNull Context context, @NonNull WheelParams params) {
68 | this(context, params, new WheelDrawManager(), new SimpleItemPainter());
69 | }
70 |
71 | //用代码生成控件
72 | public WheelView(@NonNull Context context, @NonNull WheelParams params,
73 | @NonNull DrawManager drawManager, @NonNull ItemPainter painter) {
74 | super(context);
75 | initialize(context, params, drawManager, painter);
76 | }
77 |
78 | public WheelView(@NonNull Context context, AttributeSet attrs) {
79 | super(context, attrs);
80 | initialize(context, attrs);
81 | }
82 |
83 | public WheelView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
84 | super(context, attrs, defStyleAttr);
85 | initialize(context, attrs);
86 | }
87 |
88 | private void initialize(@NonNull Context context, AttributeSet attrs) {
89 | WheelParams params = new WheelParams.Builder(context, attrs).build();
90 | initialize(context, params, new WheelDrawManager(), new SimpleItemPainter());
91 | }
92 |
93 | private void initialize(Context context, WheelParams params, DrawManager drawManager, ItemPainter painter) {
94 | this.mWheelParams = params;
95 | mRecyclerView = new RecyclerView(context);
96 | mRecyclerView.setId(ViewCompat.generateViewId());
97 | mRecyclerView.setHasFixedSize(true);
98 |
99 | mLayoutManager = new LinearLayoutManager(context, mWheelParams.getLayoutOrientation(), false);
100 | mRecyclerView.setLayoutManager(mLayoutManager);
101 | //让滑动结束时都能定到中心位置
102 | new LinearSnapHelper().attachToRecyclerView(mRecyclerView);
103 |
104 | mWheelAdapter = new WheelAdapter(mWheelParams);
105 | mRecyclerView.setAdapter(mWheelAdapter);
106 | this.mDrawManager = drawManager;
107 | mDrawManager.setWheelParams(mWheelParams);
108 | mDrawManager.setItemPainter(painter);
109 |
110 | mScrollListener = new OnScrollListener();
111 | super.addView(mRecyclerView, -1, createLayoutParams());
112 | }
113 |
114 | @Override
115 | public void addView(View child, int index, LayoutParams params) {
116 | throw new UnsupportedOperationException("addView(View...) is not supported in WheelView");
117 | }
118 |
119 | @Override
120 | public void removeView(View child) {
121 | throw new UnsupportedOperationException("removeView(View) is not supported in WheelView");
122 | }
123 |
124 | @Override
125 | public void removeViewAt(int index) {
126 | throw new UnsupportedOperationException("removeViewAt(int) is not supported in WheelView");
127 | }
128 |
129 | @Override
130 | public void removeAllViews() {
131 | throw new UnsupportedOperationException("removeAllViews() is not supported in WheelView");
132 | }
133 |
134 | @Override
135 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
136 | int paddingLeftRight = getPaddingLeft() + getPaddingRight();
137 | int paddingTopBottom = getPaddingTop() + getPaddingBottom();
138 | int childState = mRecyclerView.getMeasuredState();
139 | LayoutParams childParams = mRecyclerView.getLayoutParams();
140 | LayoutParams layoutParams = getLayoutParams();
141 | if (mWheelParams.isVertical()) {
142 | //非精准测量给默认值且不是linearlayout的layout_weight或者ConstraintLayout等之类的权重布局
143 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
144 | && (layoutParams != null && layoutParams.width != 0)) {
145 | childParams.width = Math.max(DEF_SIZE, getSuggestedMinimumWidth() - paddingLeftRight);
146 | }
147 | } else {
148 | if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY
149 | && (layoutParams != null && layoutParams.height != 0)) {
150 | childParams.height = Math.max(DEF_SIZE, getSuggestedMinimumHeight() - paddingTopBottom);
151 | }
152 | }
153 | measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
154 | int width = mRecyclerView.getMeasuredWidth() + paddingLeftRight;
155 | int height = mRecyclerView.getMeasuredHeight() + paddingTopBottom;
156 | setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
157 | resolveSizeAndState(height, heightMeasureSpec, childState));
158 | }
159 |
160 | @Override
161 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
162 | int width = mRecyclerView.getMeasuredWidth();
163 | int height = mRecyclerView.getMeasuredHeight();
164 | mContainerRect.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(), b - t - getPaddingBottom());
165 | Gravity.apply(Gravity.TOP | Gravity.START, width, height, mContainerRect, mChildRect);
166 | mRecyclerView.layout(mChildRect.left, mChildRect.top, mChildRect.right, mChildRect.bottom);
167 | }
168 |
169 | @Override
170 | protected void onAttachedToWindow() {
171 | super.onAttachedToWindow();
172 | hasAttachedToWindow = true;
173 | mRecyclerView.addItemDecoration(mDrawManager);
174 | mRecyclerView.addOnScrollListener(mScrollListener);
175 | }
176 |
177 | @Override
178 | protected void onDetachedFromWindow() {
179 | super.onDetachedFromWindow();
180 | mRecyclerView.removeOnScrollListener(mScrollListener);
181 | mRecyclerView.removeItemDecoration(mDrawManager);
182 | hasAttachedToWindow = false;
183 | }
184 |
185 | /**
186 | * 创建WheelView的LayoutParams
187 | */
188 | private LayoutParams createLayoutParams() {
189 | if (mWheelParams.isVertical())
190 | return new LayoutParams(LayoutParams.MATCH_PARENT, mWheelParams.getTotalItemSize());
191 | return new LayoutParams(mWheelParams.getTotalItemSize(), LayoutParams.MATCH_PARENT);
192 | }
193 |
194 | @SuppressLint("NotifyDataSetChanged")
195 | private void onDataSetChanged() {
196 | mWheelAdapter.refreshDataCounts();
197 | mWheelAdapter.notifyDataSetChanged();
198 | }
199 |
200 | /**
201 | * 分发selected事件监听
202 | */
203 | private void dispatchOnSelectIndexChanged(int index) {
204 | if (mSelectedPosition == index) return;
205 | mSelectedPosition = index;
206 | if (mSelectedListeners != null) {
207 | for (OnItemSelectedListener listener : mSelectedListeners) {
208 | listener.onItemSelected(this, index);
209 | }
210 | }
211 | }
212 |
213 | /**
214 | * 设置适配器, 有实现的通用的 {@link WheelAdapter } , {@link Adapter}
215 | */
216 | public void setAdapter(@Nullable Adapter adapter) {
217 | if (mAdapter != null) {
218 | mAdapter.setWheelViewObserver(null);
219 | }
220 | mAdapter = adapter;
221 | if (mAdapter != null) {
222 | if (mObserver == null) {
223 | mObserver = new WheelViewObserver();
224 | }
225 | mAdapter.setWheelViewObserver(mObserver);
226 | this.mSelectedPosition = IDLE_POSITION;
227 | this.mWheelAdapter.adapter = adapter;
228 | onDataSetChanged();
229 | mLayoutManager.scrollToPositionWithOffset(0, 0);
230 | }
231 | }
232 |
233 | /**
234 | * 设置params, 用于代码生成WheelView时
235 | *
236 | * @param wheelParams 新的参数 详见{@link WheelParams.Builder}
237 | */
238 | public void setWheelParams(@NonNull WheelParams wheelParams) {
239 | mRecyclerView.removeItemDecoration(mDrawManager);
240 | mWheelParams = wheelParams;
241 | mDrawManager.setWheelParams(mWheelParams);
242 | mWheelAdapter = new WheelAdapter(mWheelParams);
243 | mWheelAdapter.adapter = mAdapter;
244 | mLayoutManager.setOrientation(mWheelParams.getLayoutOrientation());
245 | mRecyclerView.setAdapter(mWheelAdapter);
246 | if (hasAttachedToWindow) {
247 | mRecyclerView.addItemDecoration(mDrawManager);
248 | }
249 | mRecyclerView.setLayoutParams(createLayoutParams());
250 | }
251 |
252 | /**
253 | * 设置DrawManager
254 | */
255 | @SuppressLint("NotifyDataSetChanged")
256 | public void setDrawManager(@NonNull DrawManager drawManager) {
257 | drawManager.setItemPainter(mDrawManager.itemPainter);
258 | mRecyclerView.removeItemDecoration(mDrawManager);
259 | mDrawManager = drawManager;
260 | mDrawManager.setWheelParams(mWheelParams);
261 | if (hasAttachedToWindow) {
262 | mRecyclerView.addItemDecoration(mDrawManager);
263 | }
264 | mRecyclerView.setLayoutParams(createLayoutParams());
265 | mWheelAdapter.refreshDataCounts();
266 | mWheelAdapter.notifyDataSetChanged();
267 | }
268 |
269 | //设置item绘制器
270 | public void setItemPainter(@NonNull ItemPainter painter) {
271 | mDrawManager.setItemPainter(painter);
272 | invalidate();
273 | }
274 |
275 | /**
276 | * 设置当前item位置
277 | */
278 | public void setCurrentItem(int position) {
279 | mLayoutManager.scrollToPositionWithOffset(position, 0);
280 | }
281 |
282 | public void addOnItemSelectedListener(OnItemSelectedListener listener) {
283 | if (mSelectedListeners == null) mSelectedListeners = new ArrayList<>();
284 | mSelectedListeners.add(listener);
285 | }
286 |
287 | public void removeOnItemSelectedListener(OnItemSelectedListener listener) {
288 | if (mSelectedListeners != null) {
289 | mSelectedListeners.remove(listener);
290 | }
291 | }
292 |
293 | public int getCurrentItem() {
294 | return mDrawManager.centerItemPosition;
295 | }
296 |
297 | @NonNull
298 | public WheelParams getWheelParams() {
299 | return mWheelParams;
300 | }
301 |
302 | @NonNull
303 | public DrawManager getDrawManager() {
304 | return mDrawManager;
305 | }
306 |
307 | @NonNull
308 | public ItemPainter getItemPainter() {
309 | return mDrawManager.itemPainter;
310 | }
311 |
312 | @Nullable
313 | public Adapter getAdapter() {
314 | return mAdapter;
315 | }
316 |
317 | /** -------------------------- inner class --------------------------*/
318 |
319 | /**
320 | * Item selected
321 | */
322 | public interface OnItemSelectedListener {
323 |
324 | void onItemSelected(WheelView wheelView, int index);
325 | }
326 |
327 | /**
328 | * open WheelView Adapter
329 | */
330 | public static abstract class Adapter {
331 |
332 | private DataSetObserver wheelObserver;
333 |
334 | void setWheelViewObserver(DataSetObserver observer) {
335 | synchronized (this) {
336 | wheelObserver = observer;
337 | }
338 | }
339 |
340 | /**
341 | * 可以根据实际需求, 刷新后{@link #setCurrentItem(int) 0}
342 | */
343 | public final void notifyDataSetChanged() {
344 | synchronized (this) {
345 | if (wheelObserver != null) {
346 | wheelObserver.onChanged();
347 | }
348 | }
349 | }
350 |
351 | public abstract int getItemCount();
352 |
353 | @NonNull public abstract String getItem(int position);
354 | }
355 |
356 | /**
357 | * Adapter观察者
358 | */
359 | private class WheelViewObserver extends DataSetObserver {
360 | @Override
361 | public void onChanged() {
362 | onDataSetChanged();
363 | }
364 |
365 | @Override
366 | public void onInvalidated() {
367 | onDataSetChanged();
368 | }
369 | }
370 |
371 | /**
372 | * WheelView滑动监听
373 | */
374 | private class OnScrollListener extends RecyclerView.OnScrollListener {
375 | @Override
376 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
377 | if (newState != RecyclerView.SCROLL_STATE_IDLE) return;
378 | if (mDrawManager.centerItemPosition == IDLE_POSITION) return;
379 | dispatchOnSelectIndexChanged(mDrawManager.centerItemPosition);
380 | }
381 | }
382 |
383 | /**
384 | * Item绘制器
385 | */
386 | public static class ItemPainter {
387 | WheelParams wheelParams;
388 |
389 | @CallSuper
390 | protected void setWheelParams(@NonNull WheelParams params) {
391 | this.wheelParams = params;
392 | }
393 |
394 | protected final WheelParams getWheelParams() {
395 | return wheelParams;
396 | }
397 |
398 | //画item
399 | protected void drawItem(@NonNull Canvas c, @NonNull Rect itemRect, int alpha, @NonNull String item) {
400 | }
401 |
402 | //画中心item
403 | protected void drawCenterItem(@NonNull Canvas c, @NonNull Rect itemRect, int alpha, @NonNull String item) {
404 | }
405 |
406 | //画分割线
407 | protected void drawDivider(@NonNull Canvas c, @NonNull Rect parentRect) {
408 | }
409 | }
410 |
411 | /**
412 | * 绘制器管理, 亦可重写此类实现想要的效果
413 | * 不旋转处理的管理类{@link LinearDrawManager}, Wheel效果的{@link WheelDrawManager}
414 | */
415 | public static abstract class DrawManager extends RecyclerView.ItemDecoration {
416 | //Wheel相关参数
417 | WheelParams wheelParams;
418 | //Item绘制器
419 | ItemPainter itemPainter;
420 | //整个WheelView的显示区域
421 | final Rect wvRect = new Rect();
422 | //item显示区域
423 | final Rect itemRect = new Rect();
424 | //中心位置
425 | int centerItemPosition = IDLE_POSITION;
426 |
427 | @CallSuper
428 | protected void setWheelParams(@NonNull WheelParams params) {
429 | params.setItemShowOrder(getShowOrder());
430 | this.wheelParams = params;
431 | if (itemPainter != null) {
432 | itemPainter.setWheelParams(wheelParams);
433 | }
434 | }
435 |
436 | @CallSuper
437 | void setItemPainter(@NonNull ItemPainter itemPainter) {
438 | if (this.wheelParams != null) {
439 | itemPainter.setWheelParams(wheelParams);
440 | }
441 | this.itemPainter = itemPainter;
442 | }
443 |
444 | public final WheelParams getWheelParams() {
445 | return wheelParams;
446 | }
447 |
448 | public final ItemPainter getItemPainter() {
449 | return itemPainter;
450 | }
451 |
452 | public final int getCenterItemPosition() {
453 | return centerItemPosition;
454 | }
455 |
456 | protected void setCenterItemPosition(int centerItemPosition) {
457 | this.centerItemPosition = centerItemPosition;
458 | }
459 |
460 | @Override
461 | public final void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
462 | @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
463 | }
464 |
465 | @Override
466 | public final void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
467 | }
468 |
469 | @Override
470 | public void onDraw(@NonNull Canvas c, @NonNull RecyclerView rv, @NonNull RecyclerView.State state) {
471 | if (wheelParams == null || itemPainter == null) return;
472 | if (rv.getLayoutManager() == null || !(rv.getAdapter() instanceof WheelAdapter)) return;
473 | int wheelCount = rv.getLayoutManager().getItemCount() - wheelParams.getShowItemCount() * 2;
474 | if (wheelCount <= 0) return;
475 | WheelAdapter adapter = (WheelAdapter) rv.getAdapter();
476 | wvRect.set(rv.getPaddingLeft(), rv.getPaddingTop(),
477 | rv.getWidth() - rv.getPaddingRight(), rv.getHeight() - rv.getPaddingBottom());
478 | preDecoration(c, wvRect);
479 | for (int i = 0; i < rv.getChildCount(); i++) {
480 | View itemView = rv.getChildAt(i);
481 | int adapterPosition = rv.getChildAdapterPosition(itemView) - wheelParams.getShowItemCount();
482 | if (adapterPosition < 0 || adapterPosition >= wheelCount) {
483 | continue; //itemCount为空白项,不考虑 || 超过列表的也是空白项
484 | }
485 | itemRect.set(itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom());
486 | decorationItem(c, itemRect, adapterPosition, adapter.getItem(adapterPosition));
487 | }
488 | decorationOver(c, wvRect);
489 | }
490 |
491 | //不处理
492 | protected WheelParams.ItemShowOrder getShowOrder() {
493 | return null;
494 | }
495 |
496 | //Canvas预装饰
497 | protected void preDecoration(@NonNull Canvas c, @NonNull Rect parentRect) {
498 | centerItemPosition = IDLE_POSITION;
499 | }
500 |
501 | //画item时的画笔装饰
502 | protected abstract void decorationItem(@NonNull Canvas c, @NonNull Rect itemRect, int position, @NonNull String item);
503 |
504 | //画完item时的画笔装饰
505 | protected void decorationOver(@NonNull Canvas c, @NonNull Rect parentRect) {
506 | itemPainter.drawDivider(c, parentRect);
507 | }
508 | }
509 |
510 | //RecyclerView实际显示的adapter
511 | static class WheelAdapter extends RecyclerView.Adapter {
512 | //wheel params
513 | final WheelParams wheelParams;
514 | //wheel adapter
515 | Adapter adapter = null;
516 | //ItemCount为null时重新计算,亦防止重复计算
517 | Integer itemCounts;
518 |
519 | public WheelAdapter(WheelParams wheelParams) {
520 | this.wheelParams = wheelParams;
521 | }
522 |
523 | void refreshDataCounts() {
524 | itemCounts = null;
525 | }
526 |
527 | @Override
528 | public int getItemCount() {
529 | if (itemCounts == null) {
530 | itemCounts = adapter == null ? 0 : adapter.getItemCount() + wheelParams.getShowItemCount() * 2;
531 | }
532 | return itemCounts;
533 | }
534 |
535 | @NonNull
536 | @Override
537 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
538 | View view = new View(parent.getContext());
539 | if (wheelParams.isVertical()) {
540 | view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, wheelParams.itemSize));
541 | } else {
542 | view.setLayoutParams(new LayoutParams(wheelParams.itemSize, LayoutParams.MATCH_PARENT));
543 | }
544 | view.setVisibility(View.INVISIBLE);
545 | return new RecyclerView.ViewHolder(view) {
546 | };
547 | }
548 |
549 | @Override
550 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
551 | }
552 |
553 | @NonNull private String getItem(int position) {
554 | if (adapter != null) return adapter.getItem(position);
555 | return "";
556 | }
557 | }
558 |
559 | /**
560 | * 常用绘制器, 亦可重写此类
561 | */
562 | public static class SimpleItemPainter extends ItemPainter {
563 | //text画笔
564 | private final Paint textPaint = new TextPaint();
565 | //分割线Paint
566 | private final Paint dividerPaint = new Paint();
567 | //画文本居中时文本画笔的中心位置, 画居中文字时
568 | private float textFontCenter;
569 |
570 | @Override
571 | protected void setWheelParams(@NonNull WheelParams params) {
572 | super.setWheelParams(params);
573 | textPaint.setAntiAlias(true);
574 | textPaint.setTextAlign(Paint.Align.CENTER);
575 | textPaint.setTextSize(params.textSize);
576 | Paint.FontMetrics fm = textPaint.getFontMetrics();
577 | textFontCenter = (fm.bottom + fm.top) / 2.0f;
578 |
579 | dividerPaint.setAntiAlias(true);
580 | dividerPaint.setStrokeWidth(params.dividerSize);
581 | dividerPaint.setColor(params.dividerColor);
582 | }
583 |
584 | @Override
585 | protected void drawItem(@NonNull Canvas c, @NonNull Rect itemRect, int alpha, @NonNull String item) {
586 | textPaint.setColor(wheelParams.textColor);
587 | textPaint.setAlpha(alpha);
588 | c.drawText(item, itemRect.exactCenterX(), itemRect.exactCenterY() - textFontCenter, textPaint);
589 | }
590 |
591 | @Override
592 | protected void drawCenterItem(@NonNull Canvas c, @NonNull Rect itemRect, int alpha, @NonNull String item) {
593 | textPaint.setColor(wheelParams.textCenterColor);
594 | textPaint.setAlpha(alpha);
595 | c.drawText(item, itemRect.exactCenterX(), itemRect.exactCenterY() - textFontCenter, textPaint);
596 | }
597 |
598 | @Override
599 | protected void drawDivider(@NonNull Canvas c, @NonNull Rect parentRect) {
600 | if (wheelParams.isVertical()) {
601 | float dividerOff = (parentRect.height() - wheelParams.itemSize) / 2.0f;
602 | float firstY = parentRect.top + dividerOff - wheelParams.dividerPadding;
603 | c.drawLine(parentRect.left, firstY, parentRect.right, firstY, dividerPaint);
604 | float secondY = parentRect.bottom - dividerOff + wheelParams.dividerPadding;
605 | c.drawLine(parentRect.left, secondY, parentRect.right, secondY, dividerPaint);
606 | } else {
607 | float dividerOff = (parentRect.width() - wheelParams.itemSize) / 2.0f;
608 | float firstX = parentRect.left + dividerOff - wheelParams.dividerPadding;
609 | c.drawLine(firstX, parentRect.top, firstX, parentRect.bottom, dividerPaint);
610 | float secondX = parentRect.right - dividerOff + wheelParams.dividerPadding;
611 | c.drawLine(secondX, parentRect.top, secondX, parentRect.bottom, dividerPaint);
612 | }
613 | }
614 | }
615 | }
616 |
--------------------------------------------------------------------------------
/wheelview/src/main/res/values/wheelview_attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/wheelview/src/test/java/chen/you/wheel/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package chen.you.wheel;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------