diff --git a/.gitignore b/.gitignore
index 874df11..8927af0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,35 +1,19 @@
+*.iml
.gradle
/local.properties
-/.idea/workspace.xml
+/.idea/caches
/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
-
-
-# Files for the Dalvik VM
-*.dex
-
-# Java class files
-*.class
-
-# Generated files
-bin/
-gen/
-
-# Gradle files
-.gradle/
-build/
-/*/build/
-
-# Local configuration file (sdk path, etc)
+.externalNativeBuild
+.cxx
local.properties
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-*.log
-
-# Svn
-.svn/
\ No newline at end of file
+/.idea/
+/gradle/wrapper/
+/gradlew.bat
+/gradlew
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index d6179d4..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-CursorWheelLayout
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index 96cc43e..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
deleted file mode 100644
index e7bedf3..0000000
--- a/.idea/copyright/profiles_settings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 97626ba..0000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 6e1114a..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index a846f05..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,162 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Android
-
-
- Android > Lint > Correctness
-
-
- Android > Lint > Correctness > Messages
-
-
- Android > Lint > Internationalization
-
-
- Android > Lint > Security
-
-
- Android > Lint > Usability
-
-
- Android > Lint > Usability > Icons
-
-
- C/C++
-
-
- Class structureJava
-
-
- Control flow issuesJava
-
-
- Error handlingJava
-
-
- General
-
-
- Groovy
-
-
- HTML
-
-
- ImportsJava
-
-
- Internationalization issues
-
-
- Internationalization issuesJava
-
-
- J2ME issuesJava
-
-
- JSON
-
-
- Java
-
-
- Javadoc issuesJava
-
-
- Manifest
-
-
- OtherGroovy
-
-
- Portability issuesJava
-
-
- Potentially confusing code constructsGroovy
-
-
- Probable bugsJava
-
-
- Properties Files
-
-
- Properties FilesJava
-
-
- RELAX NG
-
-
- Resource management issuesJava
-
-
- TestNGJava
-
-
- Threading issuesJava
-
-
- Unused codeC/C++
-
-
- Validity issuesGroovy
-
-
- XML
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index b9eeaab..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 7f68460..0000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CursorWheelLayout.iml b/CursorWheelLayout.iml
index c5ca878..47d5882 100644
--- a/CursorWheelLayout.iml
+++ b/CursorWheelLayout.iml
@@ -8,10 +8,11 @@
-
+
+
@@ -221,4 +222,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/app.iml b/app/app.iml
index fa18371..e0955a4 100644
--- a/app/app.iml
+++ b/app/app.iml
@@ -1,330 +1,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- generateDebugSources
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 6d015a5..c0371f2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,28 +1,53 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+}
android {
- compileSdkVersion 23
- buildToolsVersion "23.0.3"
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
defaultConfig {
- applicationId "github.hellocsl.cursorwheellayout"
+ applicationId "github.hellocsl.cursorwheel"
minSdkVersion 14
- targetSdkVersion 23
- versionCode 1
- versionName "1.0"
+ targetSdkVersion 30
+ versionCode 3
+ versionName "2.0.01"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ dataBinding true
+ }
}
dependencies {
- compile fileTree(include: ['*.jar'], dir: 'libs')
- testCompile 'junit:junit:4.12'
- compile 'com.android.support:appcompat-v7:23.2.1'
- compile project(':library')
- compile 'com.jakewharton:butterknife:7.0.1'
-}
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation project(':library')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.2.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
+
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.java b/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.java
deleted file mode 100644
index f12c058..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package github.hellocsl.cursorwheellayout;
-
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.Button;
-import android.widget.Toast;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-
-import butterknife.Bind;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import github.hellocsl.cursorwheel.CursorWheelLayout;
-import github.hellocsl.cursorwheellayout.adapter.SimpleImageAdapter;
-import github.hellocsl.cursorwheellayout.adapter.SimpleTextAdapter;
-import github.hellocsl.cursorwheellayout.data.ImageData;
-import github.hellocsl.cursorwheellayout.data.MenuItemData;
-import github.hellocsl.cursorwheellayout.widget.SimpleTextCursorWheelLayout;
-
-public class MainActivity extends AppCompatActivity implements CursorWheelLayout.OnMenuSelectedListener {
-
- @Bind(R.id.test_circle_menu_left)
- SimpleTextCursorWheelLayout mTestCircleMenuLeft;
- @Bind(R.id.test_circle_menu_right)
- SimpleTextCursorWheelLayout mTestCircleMenuRight;
- @Bind(R.id.main_button_random_selected)
- Button mMainButtonRadonSelected;
- Random mRandom = new Random();
- @Bind(R.id.test_circle_menu_top)
- CursorWheelLayout mTestCircleMenuTop;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- ButterKnife.bind(this);
- initData();
- }
-
- private void initData() {
- String[] res = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "OFF"};
- List menuItemDatas = new ArrayList();
- for (int i = 0; i < res.length; i++) {
- menuItemDatas.add(new MenuItemData(res[i]));
- }
- SimpleTextAdapter simpleTextAdapter = new SimpleTextAdapter(this, menuItemDatas, Gravity.TOP | Gravity.CENTER_HORIZONTAL);
- mTestCircleMenuLeft.setAdapter(simpleTextAdapter);
- mTestCircleMenuLeft.setOnMenuSelectedListener(this);
- SimpleTextAdapter simpleTextAdapter2 = new SimpleTextAdapter(this, menuItemDatas, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
- mTestCircleMenuRight.setAdapter(simpleTextAdapter2);
- mTestCircleMenuRight.setOnMenuSelectedListener(this);
- List imageDatas = new ArrayList();
- imageDatas.add(new ImageData(R.drawable.ic_bank_bc, "0"));
- imageDatas.add(new ImageData(R.drawable.ic_bank_china, "1"));
- imageDatas.add(new ImageData(R.drawable.ic_bank_guangda, "2"));
- imageDatas.add(new ImageData(R.drawable.ic_bank_guangfa, "3"));
- imageDatas.add(new ImageData(R.drawable.ic_bank_jianshe, "4"));
- imageDatas.add(new ImageData(R.drawable.ic_bank_jiaotong, "5"));
- SimpleImageAdapter simpleImageAdapter = new SimpleImageAdapter(this, imageDatas);
- mTestCircleMenuTop.setAdapter(simpleImageAdapter);
- mTestCircleMenuTop.setOnMenuSelectedListener(new CursorWheelLayout.OnMenuSelectedListener() {
- @Override
- public void onItemSelected(CursorWheelLayout parent, View view, int pos) {
- Toast.makeText(MainActivity.this, "Top Menu selected position:" + pos, Toast.LENGTH_SHORT).show();
- }
- });
- mTestCircleMenuTop.setOnMenuItemClickListener(new CursorWheelLayout.OnMenuItemClickListener() {
- @Override
- public void onItemClick(View view, int pos) {
- Toast.makeText(MainActivity.this, "Top Menu click position:" + pos, Toast.LENGTH_SHORT).show();
-
- }
- });
- }
-
- @Override
- public void onItemSelected(CursorWheelLayout p, View view, int pos) {
- }
-
- @OnClick(R.id.main_button_random_selected)
- void onRandomClick() {
- int index = mRandom.nextInt(10);
- mTestCircleMenuLeft.setSelection(index);
- mTestCircleMenuRight.setSelection(index, false);
- mMainButtonRadonSelected.setText("Random Selected:" + index);
- }
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.kt
new file mode 100644
index 0000000..ff58290
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/MainActivity.kt
@@ -0,0 +1,87 @@
+package github.hellocsl.cursorwheellayout
+
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import github.hellocsl.cursorwheel.CursorWheelLayout
+import github.hellocsl.cursorwheel.CursorWheelLayout.OnMenuSelectedListener
+import github.hellocsl.cursorwheellayout.adapter.SimpleImageAdapter
+import github.hellocsl.cursorwheellayout.adapter.SimpleTextAdapter
+import github.hellocsl.cursorwheellayout.data.ImageData
+import github.hellocsl.cursorwheellayout.data.MenuItemData
+import github.hellocsl.cursorwheellayout.databinding.ActivityMainBinding
+import java.util.*
+
+class MainActivity : AppCompatActivity(), OnMenuSelectedListener {
+ private lateinit var viewBinding: ActivityMainBinding
+
+ private var mRandom = Random()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
+ initData()
+ }
+
+ private fun initData() {
+ val res = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "OFF")
+ val menuItemDatas: MutableList = ArrayList()
+ for (i in res.indices) {
+ menuItemDatas.add(MenuItemData(res[i]))
+ }
+ val simpleTextAdapter =
+ SimpleTextAdapter(this, menuItemDatas, Gravity.TOP or Gravity.CENTER_HORIZONTAL)
+ viewBinding.testCircleMenuLeft.setAdapter(simpleTextAdapter)
+ viewBinding.testCircleMenuLeft.setOnMenuSelectedListener(this)
+
+ val simpleTextAdapter2 =
+ SimpleTextAdapter(this, menuItemDatas, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+ viewBinding.testCircleMenuRight.setAdapter(simpleTextAdapter2)
+ viewBinding.testCircleMenuRight.setOnMenuSelectedListener(this)
+ val imageDatas: MutableList = ArrayList()
+ imageDatas.add(ImageData(R.drawable.ic_bank_bc, "0"))
+ imageDatas.add(ImageData(R.drawable.ic_bank_china, "1"))
+ imageDatas.add(ImageData(R.drawable.ic_bank_guangda, "2"))
+ imageDatas.add(ImageData(R.drawable.ic_bank_guangfa, "3"))
+ imageDatas.add(ImageData(R.drawable.ic_bank_jianshe, "4"))
+ imageDatas.add(ImageData(R.drawable.ic_bank_jiaotong, "5"))
+ val simpleImageAdapter = SimpleImageAdapter(this, imageDatas)
+ viewBinding.testCircleMenuTop.setAdapter(simpleImageAdapter)
+ viewBinding.testCircleMenuTop.setOnMenuSelectedListener(object : OnMenuSelectedListener {
+ override fun onItemSelected(parent: CursorWheelLayout, view: View?, pos: Int) {
+ Toast.makeText(
+ this@MainActivity,
+ "Top Menu selected position:$pos",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ })
+
+ viewBinding.testCircleMenuTop.setOnMenuItemClickListener(object :
+ CursorWheelLayout.OnMenuItemClickListener {
+ override fun onItemClick(view: View?, pos: Int) {
+ Toast.makeText(
+ this@MainActivity,
+ "Top Menu click position:$pos",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ })
+
+ viewBinding.mainButtonRandomSelected.setOnClickListener {
+ onRandomClick()
+ }
+ }
+
+ override fun onItemSelected(p: CursorWheelLayout, view: View?, pos: Int) {}
+
+ private fun onRandomClick() {
+ val index = mRandom.nextInt(10)
+ viewBinding.testCircleMenuLeft.setSelection(index)
+ viewBinding.testCircleMenuRight.setSelection(index, false)
+ viewBinding.mainButtonRandomSelected.text = "Random Selected:$index"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.java b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.java
deleted file mode 100644
index 2e8bf6f..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package github.hellocsl.cursorwheellayout.adapter;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-
-import java.util.List;
-
-import github.hellocsl.cursorwheel.CursorWheelLayout;
-import github.hellocsl.cursorwheellayout.R;
-import github.hellocsl.cursorwheellayout.data.ImageData;
-
-/**
- * Created by chensuilun on 16/4/24.
- */
-public class SimpleImageAdapter extends CursorWheelLayout.CycleWheelAdapter {
- private List mMenuItemDatas;
- private LayoutInflater mLayoutInflater;
- private Context mContext;
-
- public SimpleImageAdapter(Context context, List menuItemDatas) {
- mContext = context;
- mLayoutInflater = LayoutInflater.from(context);
- mMenuItemDatas = menuItemDatas;
- }
-
- @Override
- public int getCount() {
- return mMenuItemDatas == null ? 0 : mMenuItemDatas.size();
- }
-
- @Override
- public View getView(View parent, int position) {
- ImageData item = getItem(position);
- View root = mLayoutInflater.inflate(R.layout.wheel_image_item, null, false);
- ImageView imgView = (ImageView) root.findViewById(R.id.wheel_menu_item_iv);
- imgView.setImageResource(item.mImageRes);
- return root;
- }
-
- @Override
- public ImageData getItem(int position) {
- return mMenuItemDatas.get(position);
- }
-
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.kt
new file mode 100644
index 0000000..43779d9
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleImageAdapter.kt
@@ -0,0 +1,34 @@
+package github.hellocsl.cursorwheellayout.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import github.hellocsl.cursorwheel.CursorWheelLayout.CycleWheelAdapter
+import github.hellocsl.cursorwheellayout.R
+import github.hellocsl.cursorwheellayout.data.ImageData
+
+/**
+ * Created by chensuilun on 16/4/24.
+ */
+class SimpleImageAdapter(
+ mContext: Context, private val mMenuItemDatas: List
+) : CycleWheelAdapter() {
+
+ private val mLayoutInflater: LayoutInflater = LayoutInflater.from(mContext)
+ override fun getCount(): Int {
+ return mMenuItemDatas.size
+ }
+
+ override fun getView(parent: View?, position: Int): View {
+ val (mImageRes) = getItem(position)
+ val root = mLayoutInflater.inflate(R.layout.wheel_image_item, null, false)
+ val imgView = root.findViewById(R.id.wheel_menu_item_iv)
+ imgView.setImageResource(mImageRes)
+ return root
+ }
+
+ override fun getItem(position: Int): ImageData {
+ return mMenuItemDatas[position]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.java b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.java
deleted file mode 100644
index 9048000..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package github.hellocsl.cursorwheellayout.adapter;
-
-import android.content.Context;
-import android.support.v4.app.ActivityCompat;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import java.util.List;
-
-import github.hellocsl.cursorwheel.CursorWheelLayout;
-import github.hellocsl.cursorwheellayout.R;
-import github.hellocsl.cursorwheellayout.data.MenuItemData;
-
-/**
- * Created by chensuilun on 16/4/24.
- */
-public class SimpleTextAdapter extends CursorWheelLayout.CycleWheelAdapter {
- private List mMenuItemDatas;
- private LayoutInflater mLayoutInflater;
- private Context mContext;
- public static final int INDEX_SPEC = 9;
- private int mGravity;
-
- public SimpleTextAdapter(Context context, List menuItemDatas) {
- this(context, menuItemDatas, Gravity.CENTER);
- }
-
- public SimpleTextAdapter(Context context, List menuItemDatas, int gravity) {
- mContext = context;
- mLayoutInflater = LayoutInflater.from(context);
- mMenuItemDatas = menuItemDatas;
- mGravity = gravity;
- }
-
- @Override
- public int getCount() {
- return mMenuItemDatas == null ? 0 : mMenuItemDatas.size();
- }
-
- @Override
- public View getView(View parent, int position) {
- MenuItemData item = getItem(position);
- View root = mLayoutInflater.inflate(R.layout.wheel_menu_item, null, false);
- TextView textView = (TextView) root.findViewById(R.id.wheel_menu_item_tv);
- textView.setVisibility(View.VISIBLE);
- textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- textView.setText(item.mTitle);
- if (textView.getLayoutParams() instanceof FrameLayout.LayoutParams) {
- ((FrameLayout.LayoutParams) textView.getLayoutParams()).gravity = mGravity;
- }
- if (position == INDEX_SPEC) {
- textView.setTextColor(ActivityCompat.getColor(mContext, R.color.red));
- }
- return root;
- }
-
- @Override
- public MenuItemData getItem(int position) {
- return mMenuItemDatas.get(position);
- }
-
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.kt
new file mode 100644
index 0000000..6ee3951
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/adapter/SimpleTextAdapter.kt
@@ -0,0 +1,53 @@
+package github.hellocsl.cursorwheellayout.adapter
+
+import android.content.Context
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.core.app.ActivityCompat
+import github.hellocsl.cursorwheel.CursorWheelLayout.CycleWheelAdapter
+import github.hellocsl.cursorwheellayout.R
+import github.hellocsl.cursorwheellayout.data.MenuItemData
+
+/**
+ * Created by chensuilun on 16/4/24.
+ */
+class SimpleTextAdapter constructor(
+ private val mContext: Context,
+ val mMenuItemDatas: List,
+ val mGravity: Int = Gravity.CENTER
+) : CycleWheelAdapter() {
+
+ private val mLayoutInflater: LayoutInflater = LayoutInflater.from(mContext)
+
+ override fun getCount(): Int {
+ return mMenuItemDatas.size
+ }
+
+ override fun getView(parent: View?, position: Int): View {
+ val (mTitle) = getItem(position)
+ val root = mLayoutInflater.inflate(R.layout.wheel_menu_item, null, false)
+ val textView = root.findViewById(R.id.wheel_menu_item_tv) as TextView
+ textView.visibility = View.VISIBLE
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
+ textView.text = mTitle
+ if (textView.layoutParams is FrameLayout.LayoutParams) {
+ (textView.layoutParams as FrameLayout.LayoutParams).gravity = mGravity
+ }
+ if (position == INDEX_SPEC) {
+ textView.setTextColor(ActivityCompat.getColor(mContext, R.color.red))
+ }
+ return root
+ }
+
+ override fun getItem(position: Int): MenuItemData {
+ return mMenuItemDatas[position]
+ }
+
+ companion object {
+ const val INDEX_SPEC = 9
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.java b/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.java
deleted file mode 100644
index 5a9a29d..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package github.hellocsl.cursorwheellayout.data;
-
-/**
- * Created by chensuilun on 16/4/24.
- */
-public class ImageData {
- public int mImageRes;
- public String mDesc;
-
- public ImageData(int imageRes, String desc) {
- mImageRes = imageRes;
- mDesc = desc;
- }
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.kt
new file mode 100644
index 0000000..3685b22
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/data/ImageData.kt
@@ -0,0 +1,6 @@
+package github.hellocsl.cursorwheellayout.data
+
+/**
+ * Created by chensuilun on 16/4/24.
+ */
+data class ImageData(val mImageRes: Int, val mDesc: String)
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.java b/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.java
deleted file mode 100644
index d032957..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package github.hellocsl.cursorwheellayout.data;
-
-/**
- * describe the menu item
- *
- * Created by chensuilun on 16-4-19.
- */
-public class MenuItemData {
-
- public String mTitle;
-
- public MenuItemData(String title) {
- mTitle = title;
- }
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.kt
new file mode 100644
index 0000000..406cee6
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/data/MenuItemData.kt
@@ -0,0 +1,9 @@
+package github.hellocsl.cursorwheellayout.data
+
+/**
+ * describe the menu item
+ *
+ *
+ * Created by chensuilun on 16-4-19.
+ */
+data class MenuItemData(val mTitle: String)
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.java b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.java
deleted file mode 100644
index 5484c64..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package github.hellocsl.cursorwheellayout.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-import github.hellocsl.cursorwheel.CursorWheelLayout;
-import github.hellocsl.cursorwheellayout.R;
-
-/**
- *
- * Created by chensuilun on 16-4-19.
- */
-public class SimpleTextCursorWheelLayout extends CursorWheelLayout {
- public static final int MENU_COUNT = 10;
- public static final int INDEX_SPEC = 9;
-
- public SimpleTextCursorWheelLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
-
- @Override
- protected void onInnerItemSelected(View v) {
- super.onInnerItemSelected(v);
- if (v == null) {
- return;
- }
- View tv = v.findViewById(R.id.wheel_menu_item_tv);
- tv.animate().scaleX(2).scaleY(2);
- }
-
-
- @Override
- protected void onInnerItemUnselected(View v) {
- super.onInnerItemUnselected(v);
- if (v == null) {
- return;
- }
- View tv = v.findViewById(R.id.wheel_menu_item_tv);
- tv.animate().scaleX(1).scaleY(1);
- }
-
-
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.kt
new file mode 100644
index 0000000..3a72da2
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SimpleTextCursorWheelLayout.kt
@@ -0,0 +1,30 @@
+package github.hellocsl.cursorwheellayout.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import github.hellocsl.cursorwheel.CursorWheelLayout
+import github.hellocsl.cursorwheellayout.R
+
+/**
+ *
+ * Created by chensuilun on 16-4-19.
+ */
+class SimpleTextCursorWheelLayout(context: Context, attrs: AttributeSet?) : CursorWheelLayout(context, attrs) {
+ override fun onInnerItemSelected(v: View?) {
+ super.onInnerItemSelected(v)
+ val tv = v?.findViewById(R.id.wheel_menu_item_tv)
+ tv?.animate()?.scaleX(2f)?.scaleY(2f)
+ }
+
+ override fun onInnerItemUnselected(v: View?) {
+ super.onInnerItemUnselected(v)
+ val tv = v?.findViewById(R.id.wheel_menu_item_tv)
+ tv?.animate()?.scaleX(1f)?.scaleY(1f)
+ }
+
+ companion object {
+ const val MENU_COUNT = 10
+ const val INDEX_SPEC = 9
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.java b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.java
deleted file mode 100644
index bcb4a25..0000000
--- a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.java
+++ /dev/null
@@ -1,460 +0,0 @@
-package github.hellocsl.cursorwheellayout.widget;
-
-import android.animation.ArgbEvaluator;
-import android.animation.ObjectAnimator;
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.ColorInt;
-import android.util.AttributeSet;
-import android.view.SoundEffectConstants;
-import android.view.View;
-
-import github.hellocsl.cursorwheellayout.R;
-
-
-/**
- *
- * Created by chensuilun on 16-4-13.
- */
-public class SwitchButton extends View {
- private static final String TAG = "SwitchButton";
- private static final int PROGRESS_MIN = 0;
- private static final int PROGRESS_MAX = 100;
- private static final int DEFAULT_BOARD_WIDTH = 4; // dp
- private static final int DEFAULT_BOARD_COLOR = 0xFF3c4952;
- private static final int DEFAULT_UNCHECK_REVEAL_COLOR = 0xFF3c4952;
- //DRAWABLE GRAVITY
- public static final int DRAWABLE_GRAVITY_LEFT = 0;
- public static final int DRAWABLE_GRAVITY_RIGHT = 1;
- public static final int DEFAULT_DRAWABLE_GRAVITY = -1;
-
- // private static final int DEFAULT_UNCHECK_REVEAL_COLOR = 0X1D1F22;
- private static final int DEFAULT_CHECK_REVEAL_COLOR = 0xFFFFC52A;
- private static final int DEFAULT_DISABLE_COVER_COLOR = 0x4cffffff;
- private static final int DEFAULT_CHECK_DRAWABLE_ID = R.mipmap.ic_launcher;
- private static final int DEFAULT_UNCHECK_DRAWABLE_ID = R.mipmap.ic_launcher;
-
- private static final int DEFAULT_SIZE = 126; //dp
-
- private int mDrawableGravity = DEFAULT_DRAWABLE_GRAVITY;
- /**
- * 画边界
- */
- private Paint mBroadPaint;
-
- /**
- * 覆盖层
- */
- private Paint mCoverPaint;
- /**
- * 画扩散进度
- */
- private Paint mRevealBgPaint;
- /**
- * 边框颜色
- */
- private
- @ColorInt
- int mBoardColor = DEFAULT_BOARD_COLOR;
-
-
- /**
- * 边框厚度
- */
- private int mBoardWidth;
- /**
- * 选中的时候的颜色
- */
- private int mCheckRevealColor = DEFAULT_BOARD_COLOR;
-
- /**
- * 未选中时候的颜色
- */
- private int mUnCheckRevealColor = DEFAULT_UNCHECK_REVEAL_COLOR;
-
- /**
- * 用于动画 0-100 选中的时候;
- */
- private int mProgress;
-
- private boolean mChecked;
-
- private boolean mBroadcasting;
-
- private OnCheckedChangeListener mOnCheckedChangeListener;
- /**
- * 未选中的时候的资源ID
- */
- private Drawable mUncheckDrawable;
- /**
- * 未选中的时候的资源ID
- */
- private Drawable mCheckedDrawable;
-
- private ArgbEvaluator mArgbEvaluator;
- private float mFraction;
- private ObjectAnimator mCheckAnim;
- private boolean mAttachedToWindow;
-
- public SwitchButton(Context context) {
- this(context, null);
- }
-
- public SwitchButton(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public SwitchButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init(context, attrs);
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- public SwitchButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- init(context, attrs);
- }
-
- private void init(Context context, AttributeSet attrs) {
- setClickable(true);
- mArgbEvaluator = new ArgbEvaluator();
- final float density = context.getResources().getDisplayMetrics().density;
- mBoardWidth = (int) (DEFAULT_BOARD_WIDTH * density + 0.5);
- //TODO 更多自定义属性的获取,这里hardcode先,以后有需要在提取出来
- if (attrs != null) {
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);
- int checkRes = ta.getResourceId(R.styleable.SwitchButton_checkSrc, DEFAULT_CHECK_DRAWABLE_ID);
- mCheckedDrawable = context.getResources().getDrawable(checkRes);
- int uncheckRes = ta.getResourceId(R.styleable.SwitchButton_uncheckSrc, DEFAULT_UNCHECK_DRAWABLE_ID);
- mUncheckDrawable = context.getResources().getDrawable(uncheckRes);
- mBoardWidth = ta.getDimensionPixelOffset(R.styleable.SwitchButton_boardWidth, mBoardWidth);
- mCheckRevealColor = ta.getColor(R.styleable.SwitchButton_checkRevealColor, DEFAULT_CHECK_REVEAL_COLOR);
- mUnCheckRevealColor = ta.getColor(R.styleable.SwitchButton_uncheckRevealColor, -1);
- ta.recycle();
- }
- //init paint
- mBroadPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mBroadPaint.setStyle(Paint.Style.STROKE);
- mBroadPaint.setColor(mBoardColor);
- mBroadPaint.setStrokeWidth(mBoardWidth);
- mBroadPaint.setDither(true);
-
- mCoverPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mCoverPaint.setStyle(Paint.Style.FILL);
- mCoverPaint.setColor(DEFAULT_DISABLE_COVER_COLOR);
- mCoverPaint.setDither(true);
-
-
- mRevealBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mRevealBgPaint.setFilterBitmap(true);
- mRevealBgPaint.setStyle(Paint.Style.FILL);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final float density = getContext().getResources().getDisplayMetrics().density;
- final int defaultSize = (int) (DEFAULT_SIZE * density + 0.5);
- int desiredWidth = getPaddingLeft() + getPaddingRight() + defaultSize;
- int desiredHeight = getPaddingBottom() + getPaddingTop() + defaultSize;
- int widthSpec = resolveSizeAndState(desiredWidth, widthMeasureSpec);
- int heightSpec = resolveSizeAndState(desiredHeight, heightMeasureSpec);
- super.onMeasure(widthSpec, heightSpec);
- }
-
- public static int resolveSizeAndState(int desireSize, int measureSpec) {
- int result = desireSize;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- result = desireSize;
- break;
- case MeasureSpec.AT_MOST:
- if (specSize < desireSize) {
- result = specSize;
- } else {
- result = desireSize;
- }
- break;
- case MeasureSpec.EXACTLY:
- result = specSize;
- break;
- }
- return result;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mAttachedToWindow = true;
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mAttachedToWindow = false;
- }
-
- @Override
- protected int getSuggestedMinimumWidth() {
- final float density = getContext().getResources().getDisplayMetrics().density;
- return (int) (DEFAULT_SIZE * density + 0.5);
- }
-
- @Override
- protected int getSuggestedMinimumHeight() {
- return getSuggestedMinimumWidth();
- }
-
- public void toggle() {
- setChecked(!mChecked);
- }
-
- public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
- mOnCheckedChangeListener = listener;
- }
-
- private final void setProgress(int progress) {
- if (mProgress != progress) {
- this.mProgress = progress;
- mFraction = mProgress * 1.0f / PROGRESS_MAX;
-// if (BuildConfig.DEBUG) {
-// Log.d(TAG, "setProgress: mProgress:" + mProgress + ",mFraction:" + mFraction);
-// }
- invalidate();
- }
- }
-
- public final int getProgress() {
- return mProgress;
- }
-
-
- public static long slastTime;
-
-
- public boolean onDoubClick() {
- boolean flag = false;
- long time = System.currentTimeMillis() - slastTime;
- if (time < 500) {
- flag = true;
- }
- slastTime = System.currentTimeMillis();
- return flag;
- }
-
- @Override
- public boolean performClick() {
- if (onDoubClick()) {
- return false;
- }
- toggle();
- final boolean handled = super.performClick();
- if (!handled) {
- // View only makes a sound effect if the onClickListener was
- // called, so we'll need to make one here instead.
- playSoundEffect(SoundEffectConstants.CLICK);
- }
- return handled;
- }
-
- public void setChecked(boolean checked) {
- if (mChecked != checked) {
- mChecked = checked;
- refreshDrawableState();
- // Avoid infinite recursions if setChecked() is called from a listener
- if (mBroadcasting) {
- return;
- }
- if (mAttachedToWindow) {
- addAnim(checked);
- } else {
- cancelAnim();
- setProgress(checked ? PROGRESS_MAX : PROGRESS_MIN);
- }
-
- mBroadcasting = true;
- if (mOnCheckedChangeListener != null) {
- mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
- }
- mBroadcasting = false;
- }
- }
-
- private void cancelAnim() {
- if (mCheckAnim != null) {
- mCheckAnim.cancel();
- }
- }
-
-
- private void addAnim(boolean isChecked) {
- mCheckAnim = ObjectAnimator.ofInt(this, "progress", isChecked ? PROGRESS_MAX : PROGRESS_MIN);
- mCheckAnim.setDuration(300);
- mCheckAnim.start();
- }
-
- private int generateCurColor() {
- return (int) mArgbEvaluator.evaluate(mFraction, mUnCheckRevealColor, mCheckRevealColor);
- }
-
-
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- canvas.save();
- canvas.translate(getPaddingLeft(), getPaddingTop());
- int width = getWidth() - getPaddingLeft() - getPaddingRight();
- int height = getHeight() - getPaddingBottom() - getPaddingTop();
- int radius = Math.min(width, height) / 2;
- final Drawable buttonDrawable = mChecked ? mCheckedDrawable : mUncheckDrawable;
- if (buttonDrawable != null) {
- //// FIXME: 16-4-20 臨時使用
- if (mDrawableGravity == DRAWABLE_GRAVITY_LEFT) {
- //偏右
- final int drawableHeight = buttonDrawable.getIntrinsicHeight();
- final int drawableWidth = buttonDrawable.getIntrinsicWidth();
- final int right = width / 2 - drawableWidth / 2;
- final int left = right - drawableWidth;
- final int top = (height - drawableHeight) / 2;
- final int bottom = top + drawableHeight;
- buttonDrawable.setBounds(left, top, right, bottom);
- } else if (mDrawableGravity == DRAWABLE_GRAVITY_RIGHT) {
- //偏左
- final int drawableHeight = buttonDrawable.getIntrinsicHeight();
- final int drawableWidth = buttonDrawable.getIntrinsicWidth();
- final int left = width / 2 + drawableWidth / 2;
- final int right = left + drawableWidth;
- final int top = (height - drawableHeight) / 2;
- final int bottom = top + drawableHeight;
- buttonDrawable.setBounds(left, top, right, bottom);
- } else {
- //居中
- final int drawableHeight = buttonDrawable.getIntrinsicHeight();
- final int drawableWidth = buttonDrawable.getIntrinsicWidth();
- final int left = (width - drawableWidth) / 2;
- final int right = left + drawableWidth;
- final int top = (height - drawableHeight) / 2;
- final int bottom = top + drawableHeight;
- buttonDrawable.setBounds(left, top, right, bottom);
- }
-// if (BuildConfig.DEBUG) {
-// Log.v(TAG, "onDraw:button drawable bound" + buttonDrawable.getBounds());
-// }
- }
- if (mUnCheckRevealColor != -1 && !mChecked) {
- mRevealBgPaint.setColor(mUnCheckRevealColor);
- canvas.drawCircle(width / 2, height / 2, radius - mBoardWidth, mRevealBgPaint);
- }
- //draw broad
- if (mBoardWidth > 0) {
- canvas.drawCircle(width / 2, height / 2, radius - mBoardWidth / 2, mBroadPaint);
- }
-
- mRevealBgPaint.setColor(generateCurColor());
- canvas.drawCircle(width / 2, height / 2, radius * mFraction, mRevealBgPaint);
-
- if (buttonDrawable != null) {
- buttonDrawable.draw(canvas);
- }
- if (!isEnabled()) {
- canvas.drawCircle(width / 2, height / 2, radius, mCoverPaint);
- }
- canvas.restore();
- }
-
- public void setDrawableGravity(int drawableGravity) {
- if (mDrawableGravity != drawableGravity) {
- mDrawableGravity = drawableGravity;
- invalidate();
- }
- }
-
- /**
- * Interface definition for a callback to be invoked when the checked state
- * of a compound button changed.
- */
- public static interface OnCheckedChangeListener {
- /**
- * Called when the checked state of a compound button has changed.
- *
- * @param buttonView The compound button view whose state has changed.
- * @param isChecked The new checked state of buttonView.
- */
- void onCheckedChanged(SwitchButton buttonView, boolean isChecked);
- }
-
- /**
- * @author chensuilun
- */
- static class SavedState extends BaseSavedState {
- boolean mChecked;
-
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- /**
- * Constructor called from {@link #CREATOR}
- */
- private SavedState(Parcel in) {
- super(in);
- mChecked = (Boolean) in.readValue(null);
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeValue(mChecked);
- }
-
- @Override
- public String toString() {
- return "CompoundButton.SavedState{"
- + Integer.toHexString(System.identityHashCode(this))
- + " checked=" + mChecked + "}";
- }
-
- public static final Creator CREATOR
- = new Creator() {
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
-
- SavedState ss = new SavedState(superState);
-
- ss.mChecked = isChecked();
- return ss;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- SavedState ss = (SavedState) state;
-
- super.onRestoreInstanceState(ss.getSuperState());
- setChecked(ss.mChecked);
- requestLayout();
- }
-
-}
diff --git a/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.kt b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.kt
new file mode 100644
index 0000000..ca1f525
--- /dev/null
+++ b/app/src/main/java/github/hellocsl/cursorwheellayout/widget/SwitchButton.kt
@@ -0,0 +1,428 @@
+package github.hellocsl.cursorwheellayout.widget
+
+import android.animation.ArgbEvaluator
+import android.animation.ObjectAnimator
+import android.annotation.TargetApi
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.SoundEffectConstants
+import android.view.View
+import androidx.annotation.ColorInt
+import github.hellocsl.cursorwheellayout.R
+
+/**
+ *
+ *
+ * Created by chensuilun on 16-4-13.
+ */
+class SwitchButton : View {
+ private var mDrawableGravity = DEFAULT_DRAWABLE_GRAVITY
+
+ /**
+ * 画边界
+ */
+ private var mBroadPaint: Paint? = null
+
+ /**
+ * 覆盖层
+ */
+ private var mCoverPaint: Paint? = null
+
+ /**
+ * 画扩散进度
+ */
+ private var mRevealBgPaint: Paint? = null
+
+ /**
+ * 边框颜色
+ */
+ @ColorInt
+ private val mBoardColor = DEFAULT_BOARD_COLOR
+
+ /**
+ * 边框厚度
+ */
+ private var mBoardWidth = 0
+
+ /**
+ * 选中的时候的颜色
+ */
+ private var mCheckRevealColor = DEFAULT_BOARD_COLOR
+
+ /**
+ * 未选中时候的颜色
+ */
+ private var mUnCheckRevealColor = DEFAULT_UNCHECK_REVEAL_COLOR
+
+ /**
+ * 用于动画 0-100 选中的时候;
+ */
+ private var mProgress = 0
+ private var mChecked = false
+ private var mBroadcasting = false
+ private var mOnCheckedChangeListener: OnCheckedChangeListener? = null
+
+ /**
+ * 未选中的时候的资源ID
+ */
+ private var mUncheckDrawable: Drawable? = null
+
+ /**
+ * 未选中的时候的资源ID
+ */
+ private var mCheckedDrawable: Drawable? = null
+ private var mArgbEvaluator: ArgbEvaluator? = null
+ private var mFraction = 0f
+ private var mCheckAnim: ObjectAnimator? = null
+ private var mAttachedToWindow = false
+
+ @JvmOverloads
+ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(
+ context,
+ attrs,
+ defStyleAttr
+ ) {
+ init(context, attrs)
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes) {
+ init(context, attrs)
+ }
+
+ private fun init(context: Context, attrs: AttributeSet?) {
+ isClickable = true
+ mArgbEvaluator = ArgbEvaluator()
+ val density = context.resources.displayMetrics.density
+ mBoardWidth = (DEFAULT_BOARD_WIDTH * density + 0.5).toInt()
+ //TODO 更多自定义属性的获取,这里hardcode先,以后有需要在提取出来
+ if (attrs != null) {
+ val ta = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton)
+ val checkRes =
+ ta.getResourceId(R.styleable.SwitchButton_checkSrc, DEFAULT_CHECK_DRAWABLE_ID)
+ mCheckedDrawable = context.resources.getDrawable(checkRes)
+ val uncheckRes =
+ ta.getResourceId(R.styleable.SwitchButton_uncheckSrc, DEFAULT_UNCHECK_DRAWABLE_ID)
+ mUncheckDrawable = context.resources.getDrawable(uncheckRes)
+ mBoardWidth =
+ ta.getDimensionPixelOffset(R.styleable.SwitchButton_boardWidth, mBoardWidth)
+ mCheckRevealColor =
+ ta.getColor(R.styleable.SwitchButton_checkRevealColor, DEFAULT_CHECK_REVEAL_COLOR)
+ mUnCheckRevealColor = ta.getColor(R.styleable.SwitchButton_uncheckRevealColor, -1)
+ ta.recycle()
+ }
+ //init paint
+ mBroadPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mBroadPaint!!.style = Paint.Style.STROKE
+ mBroadPaint!!.color = mBoardColor
+ mBroadPaint!!.strokeWidth = mBoardWidth.toFloat()
+ mBroadPaint!!.isDither = true
+ mCoverPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mCoverPaint!!.style = Paint.Style.FILL
+ mCoverPaint!!.color = DEFAULT_DISABLE_COVER_COLOR
+ mCoverPaint!!.isDither = true
+ mRevealBgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mRevealBgPaint!!.isFilterBitmap = true
+ mRevealBgPaint!!.style = Paint.Style.FILL
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val density = context.resources.displayMetrics.density
+ val defaultSize = (DEFAULT_SIZE * density + 0.5).toInt()
+ val desiredWidth = paddingLeft + paddingRight + defaultSize
+ val desiredHeight = paddingBottom + paddingTop + defaultSize
+ val widthSpec = resolveSizeAndState(desiredWidth, widthMeasureSpec)
+ val heightSpec = resolveSizeAndState(desiredHeight, heightMeasureSpec)
+ super.onMeasure(widthSpec, heightSpec)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ mAttachedToWindow = true
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ mAttachedToWindow = false
+ }
+
+ override fun getSuggestedMinimumWidth(): Int {
+ val density = context.resources.displayMetrics.density
+ return (DEFAULT_SIZE * density + 0.5).toInt()
+ }
+
+ override fun getSuggestedMinimumHeight(): Int {
+ return suggestedMinimumWidth
+ }
+
+ fun toggle() {
+ isChecked = !mChecked
+ }
+
+ fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
+ mOnCheckedChangeListener = listener
+ }
+
+ //if (BuildConfig.DEBUG)
+ //Log.d(TAG, "setProgress: mProgress:" + mProgress + ",mFraction:" + mFraction)
+ var progress: Int
+ get() = mProgress
+ private set(progress) {
+ if (mProgress != progress) {
+ mProgress = progress
+ mFraction = mProgress * 1.0f / PROGRESS_MAX
+ //if (BuildConfig.DEBUG)
+ //Log.d(TAG, "setProgress: mProgress:" + mProgress + ",mFraction:" + mFraction);
+ invalidate()
+ }
+ }
+
+ private fun onDoubClick(): Boolean {
+ var flag = false
+ val time = System.currentTimeMillis() - slastTime
+ if (time < 500) {
+ flag = true
+ }
+ slastTime = System.currentTimeMillis()
+ return flag
+ }
+
+ override fun performClick(): Boolean {
+ if (onDoubClick()) {
+ return false
+ }
+ toggle()
+ val handled = super.performClick()
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK)
+ }
+ return handled
+ }
+
+ private fun cancelAnim() {
+ if (mCheckAnim != null) {
+ mCheckAnim!!.cancel()
+ }
+ }
+
+ private fun addAnim(isChecked: Boolean) {
+ mCheckAnim =
+ ObjectAnimator.ofInt(this, "progress", if (isChecked) PROGRESS_MAX else PROGRESS_MIN)
+ mCheckAnim?.duration = 300
+ mCheckAnim?.start()
+ }
+
+ private fun generateCurColor(): Int {
+ return mArgbEvaluator!!.evaluate(mFraction, mUnCheckRevealColor, mCheckRevealColor) as Int
+ }
+
+ // Avoid infinite recursions if setChecked() is called from a listener
+ private var isChecked: Boolean
+ get() = mChecked
+ set(checked) {
+ if (mChecked != checked) {
+ mChecked = checked
+ refreshDrawableState()
+ // Avoid infinite recursions if setChecked() is called from a listener
+ if (mBroadcasting) {
+ return
+ }
+ if (mAttachedToWindow) {
+ addAnim(checked)
+ } else {
+ cancelAnim()
+ progress = if (checked) PROGRESS_MAX else PROGRESS_MIN
+ }
+ mBroadcasting = true
+ if (mOnCheckedChangeListener != null) {
+ mOnCheckedChangeListener!!.onCheckedChanged(this, mChecked)
+ }
+ mBroadcasting = false
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ canvas.save()
+ canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat())
+ val width = width - paddingLeft - paddingRight
+ val height = height - paddingBottom - paddingTop
+ val radius = width.coerceAtMost(height) / 2
+ val buttonDrawable = if (mChecked) mCheckedDrawable else mUncheckDrawable
+ if (buttonDrawable != null) {
+ //// FIXME: 16-4-20 臨時使用
+ when (mDrawableGravity) {
+ DRAWABLE_GRAVITY_LEFT -> {
+ //偏右
+ val drawableHeight = buttonDrawable.intrinsicHeight
+ val drawableWidth = buttonDrawable.intrinsicWidth
+ val right = width / 2 - drawableWidth / 2
+ val left = right - drawableWidth
+ val top = (height - drawableHeight) / 2
+ val bottom = top + drawableHeight
+ buttonDrawable.setBounds(left, top, right, bottom)
+ }
+ DRAWABLE_GRAVITY_RIGHT -> {
+ //偏左
+ val drawableHeight = buttonDrawable.intrinsicHeight
+ val drawableWidth = buttonDrawable.intrinsicWidth
+ val left = width / 2 + drawableWidth / 2
+ val right = left + drawableWidth
+ val top = (height - drawableHeight) / 2
+ val bottom = top + drawableHeight
+ buttonDrawable.setBounds(left, top, right, bottom)
+ }
+ else -> {
+ //居中
+ val drawableHeight = buttonDrawable.intrinsicHeight
+ val drawableWidth = buttonDrawable.intrinsicWidth
+ val left = (width - drawableWidth) / 2
+ val right = left + drawableWidth
+ val top = (height - drawableHeight) / 2
+ val bottom = top + drawableHeight
+ buttonDrawable.setBounds(left, top, right, bottom)
+ }
+ }
+
+ }
+ if (mUnCheckRevealColor != -1 && !mChecked) {
+ mRevealBgPaint!!.color = mUnCheckRevealColor
+ canvas.drawCircle(
+ width / 2.toFloat(),
+ height / 2.toFloat(),
+ radius - mBoardWidth.toFloat(),
+ mRevealBgPaint!!
+ )
+ }
+ //draw broad
+ if (mBoardWidth > 0) {
+ canvas.drawCircle(
+ width / 2.toFloat(),
+ height / 2.toFloat(),
+ radius - mBoardWidth / 2.toFloat(),
+ mBroadPaint!!
+ )
+ }
+ mRevealBgPaint!!.color = generateCurColor()
+ canvas.drawCircle(
+ width / 2.toFloat(),
+ height / 2.toFloat(),
+ radius * mFraction,
+ mRevealBgPaint!!
+ )
+ buttonDrawable?.draw(canvas)
+ if (!isEnabled) {
+ canvas.drawCircle(
+ width / 2.toFloat(),
+ height / 2.toFloat(),
+ radius.toFloat(),
+ mCoverPaint!!
+ )
+ }
+ canvas.restore()
+ }
+
+ fun setDrawableGravity(drawableGravity: Int) {
+ if (mDrawableGravity != drawableGravity) {
+ mDrawableGravity = drawableGravity
+ invalidate()
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the checked state
+ * of a compound button changed.
+ */
+ interface OnCheckedChangeListener {
+ /**
+ * Called when the checked state of a compound button has changed.
+ *
+ * @param buttonView The compound button view whose state has changed.
+ * @param isChecked The new checked state of buttonView.
+ */
+ fun onCheckedChanged(buttonView: SwitchButton?, isChecked: Boolean)
+ }
+
+ /**
+ * @author chensuilun
+ */
+ internal class SavedState(superState: Parcelable?) : BaseSavedState(superState) {
+ var mChecked = false
+
+ override fun writeToParcel(out: Parcel, flags: Int) {
+ super.writeToParcel(out, flags)
+ out.writeValue(mChecked)
+ }
+
+ override fun toString(): String {
+ return ("CompoundButton.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " checked=" + mChecked + "}")
+ }
+ }
+
+ public override fun onSaveInstanceState(): Parcelable? {
+ val superState = super.onSaveInstanceState()
+ val ss = SavedState(superState)
+ ss.mChecked = isChecked
+ return ss
+ }
+
+ public override fun onRestoreInstanceState(state: Parcelable) {
+ val ss = state as SavedState
+ super.onRestoreInstanceState(ss.superState)
+ isChecked = ss.mChecked
+ requestLayout()
+ }
+
+ companion object {
+ private const val TAG = "SwitchButton"
+ private const val PROGRESS_MIN = 0
+ private const val PROGRESS_MAX = 100
+ private const val DEFAULT_BOARD_WIDTH = 4 // dp
+ private const val DEFAULT_BOARD_COLOR = -0xc3b6ae
+ private const val DEFAULT_UNCHECK_REVEAL_COLOR = -0xc3b6ae
+
+ //DRAWABLE GRAVITY
+ const val DRAWABLE_GRAVITY_LEFT = 0
+ const val DRAWABLE_GRAVITY_RIGHT = 1
+ const val DEFAULT_DRAWABLE_GRAVITY = -1
+
+ // private static final int DEFAULT_UNCHECK_REVEAL_COLOR = 0X1D1F22;
+ private const val DEFAULT_CHECK_REVEAL_COLOR = -0x3ad6
+ private const val DEFAULT_DISABLE_COVER_COLOR = 0x4cffffff
+ private const val DEFAULT_CHECK_DRAWABLE_ID = R.mipmap.ic_launcher
+ private const val DEFAULT_UNCHECK_DRAWABLE_ID = R.mipmap.ic_launcher
+ private const val DEFAULT_SIZE = 126 //dp
+ fun resolveSizeAndState(desireSize: Int, measureSpec: Int): Int {
+ var result = desireSize
+ val specMode = MeasureSpec.getMode(measureSpec)
+ val specSize = MeasureSpec.getSize(measureSpec)
+ when (specMode) {
+ MeasureSpec.UNSPECIFIED -> result = desireSize
+ MeasureSpec.AT_MOST -> result = if (specSize < desireSize) {
+ specSize
+ } else {
+ desireSize
+ }
+ MeasureSpec.EXACTLY -> result = specSize
+ }
+ return result
+ }
+
+ var slastTime: Long = 0
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index e5cd3e4..e787b54 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,82 +1,71 @@
-
+ xmlns:tools="http://schemas.android.com/tools">
+
-
+
-
-
+
+
-
+
-
-
+
-
-
-
-
+
+
-
-
+ android:text="Random Selected" />
+
+
diff --git a/app/src/main/res/layout/wheel_image_item.xml b/app/src/main/res/layout/wheel_image_item.xml
index 90db40b..8094d12 100644
--- a/app/src/main/res/layout/wheel_image_item.xml
+++ b/app/src/main/res/layout/wheel_image_item.xml
@@ -1,11 +1,10 @@
-
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ tools:background="@color/bg_wheel">
-
+ tools:src="@drawable/ic_bank_bc" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/wheel_menu_item.xml b/app/src/main/res/layout/wheel_menu_item.xml
index 6910bce..cc0382e 100644
--- a/app/src/main/res/layout/wheel_menu_item.xml
+++ b/app/src/main/res/layout/wheel_menu_item.xml
@@ -1,10 +1,10 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ tools:background="@color/bg_wheel">
+ tools:visibility="visible" />
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 04e5bc9..862563e 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -6,15 +6,15 @@
- @color/colorPrimary
- @color/colorPrimaryDark
- @color/colorAccent
- - @color/colorAccent_Translucent
+ - @color/colorAccent_Translucent
-
-
-
-
-
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
index 85bf6c6..07d0663 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ ext.kotlin_version = "1.4.10"
repositories {
+ google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.2'
- classpath 'com.novoda:bintray-release:0.3.4'
+ classpath "com.android.tools.build:gradle:4.1.1"
+ classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -15,13 +18,11 @@ buildscript {
allprojects {
repositories {
+ maven { url 'https://jitpack.io' }
+ google()
jcenter()
}
- tasks.withType(Javadoc) {
- options.addStringOption('Xdoclint:none', '-quiet')
- options.addStringOption('encoding', 'UTF-8')
- }
}
task clean(type: Delete) {
diff --git a/gradle.properties b/gradle.properties
index 1d3591c..b11b4d0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,4 +15,9 @@
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
\ No newline at end of file
+# org.gradle.parallel=true
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7882136..bec1862 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/library/build.gradle b/library/build.gradle
index 9408267..cf1577c 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -1,14 +1,19 @@
apply plugin: 'com.android.library'
-apply plugin: 'com.novoda.bintray-release'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+group = 'github.hellocsl.cursorwheel'
+
android {
- compileSdkVersion 23
- buildToolsVersion "23.0.3"
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 14
- targetSdkVersion 23
- versionCode 1
- versionName "1.0"
+ targetSdkVersion 30
+ versionCode 3
+ versionName "2.0.01"
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
buildTypes {
release {
@@ -16,20 +21,23 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ dataBinding true
+ }
}
dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
- testCompile 'junit:junit:4.12'
- compile 'com.android.support:support-annotations:23.2.1'
-}
-
-
-publish {
- userOrg = 'bcsl'
- groupId = 'github.hellocsl'
- artifactId = 'CursorWheelLayout'
- publishVersion = '1.1.0'
- desc = 'An Android Widget for selecting items that rotate on a wheel'
- website = 'https://github.com/BCsl/CursorWheelLayout'
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ testImplementation 'junit:junit:4.13.1'
+ implementation 'com.android.support:support-annotations:28.0.0'
}
+repositories {
+ mavenCentral()
+}
\ No newline at end of file
diff --git a/library/library.iml b/library/library.iml
index 07f3e6c..e0955a4 100644
--- a/library/library.iml
+++ b/library/library.iml
@@ -1,328 +1,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- generateDebugSources
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
index 0891eb8..38329ac 100644
--- a/library/src/main/AndroidManifest.xml
+++ b/library/src/main/AndroidManifest.xml
@@ -1,11 +1,8 @@
+ package="github.hellocsl.cursorwheel">
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.java b/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.java
deleted file mode 100644
index 47b03ac..0000000
--- a/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.java
+++ /dev/null
@@ -1,1194 +0,0 @@
-package github.hellocsl.cursorwheel;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.database.DataSetObservable;
-import android.database.DataSetObserver;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-
-/**
- * The base cycle wheel menu layout with cursor
- *
- * @author chensuilun
- * @attr R.styleable.CursorWheelLayout_wheelSelectedAngle
- * @attr R.styleable.CursorWheelLayout_wheelPaddingRadio
- * @attr R.styleable.CursorWheelLayout_wheelCenterRadio
- * @attr R.styleable.CursorWheelLayout_wheelItemRadio
- * @attr R.styleable.CursorWheelLayout_wheelFlingValue
- * @attr R.styleable.CursorWheelLayout_wheelCursorColor
- * @attr R.styleable.CursorWheelLayout_wheelCursorHeight
- * @attr R.styleable.CursorWheelLayout_wheelItemRotateMode
- * @attr R.styleable.CursorWheelLayout_wheelGuideLineWidth
- * @attr R.styleable.CursorWheelLayout_wheelGuideLineColor
- */
-public class CursorWheelLayout extends ViewGroup {
- private static final String TAG = "CircleMenuLayout";
- /**
- * size of menu item relative to parent
- */
- private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f;
-
- /**
- * size of center item relative to parent
- */
- private static final float RADIO_DEFAULT_CENTER_DIMENSION = 1 / 3f;
- /**
- * ignore the origin padding ,real padding size determine by parent size
- */
- private static final float RADIO_PADDING_LAYOUT = 1 / 12f;
-
-
- private static final int INVALID_POSITION = -1;
-
-
- private static final float DEFAULT_SELECTED_ANGLE = 0;
-
- /**
- * Angle a touch can wander before we think the user is flinging
- */
- private static final int FLINGABLE_VALUE = 300;
-
- /**
- *
- */
- private static final int NOCLICK_VALUE = 3;
- /**
- * default cursor color
- */
- private static final
- @ColorInt
- int DEFAULT_CURSOR_COLOR = 0xFFFFC52A;
-
- /**
- * default wheel background color
- */
- private static final
- @ColorInt
- int DEFAULT_WHEEL_BG_COLOR = 0xe513171c;
-
- public static final
- @ColorInt
- int DEFAULT_GUIDE_LINE_COLOR = 0xff727272;
-
- //DP
- public static final int DEFAULT_TRIANGLE_HEIGHT = 13;
-
- //DP
- public static final int DEFAULT_GUIDE_LINE_WIDTH = 0;
- /**
- * Don't rotate my item.DEFAULT
- */
- public static final int ITEM_ROTATE_MODE_NONE = 0;
-
- public static final int ITEM_ROTATE_MODE_INWARD = 1;
-
- public static final int ITEM_ROTATE_MODE_OUTWARD = 2;
-
-
- /**
- * CircleMenuLayout 's size
- */
- private int mRootDiameter;
-
- /**
- * Angle a touch can wander before we think the user is flinging
- */
- private int mFlingableValue = FLINGABLE_VALUE;
-
-
- private float mPadding;
-
-
- private double mStartAngle = 0;
- /**
- * menu 's source data
- */
- private CycleWheelAdapter mWheelAdapter;
-
- /**
- *
- */
- private int mMenuItemCount;
-
- /**
- *
- */
- private float mTmpAngle;
-
- /**
- *
- */
- private long mDownTime;
-
- /**
- * weather is fling now
- */
- private boolean mIsFling;
- /**
- *
- */
- private boolean mIsDraging;
-
- /**
- */
- private float mLastX;
- private float mLastY;
-
- /**
- * [0,360)
- */
- private double mSelectedAngle = DEFAULT_SELECTED_ANGLE;
- /**
- * The currently selected item's child.
- */
- private View mSelectedView;
- /**
- * The position of the selected View
- */
- private int mSelectedPosition = INVALID_POSITION;
-
- /**
- * The temp selected item's child.
- */
- private View mTempSelectedView;
-
- /**
- * The position of the temp selected item's child.
- */
- private int mTempSelectedPosition = INVALID_POSITION;
-
-
- /**
- * 判断是否需要滚动到最中间,某些时候由于选中角度和布局中心的角度一样,所以不需要在进行一次移动
- */
- private boolean mNeedSlotIntoCenter;
-
- private FlingRunnable mFlingRunnable = new FlingRunnable();
-
- /**
- * draw cursor
- */
- private Paint mCursorPaint;
- /**
- * draw wheel bg
- */
- private Paint mWheelPaint;
- /**
- * path of cursor
- */
- private Path mTrianglePath;
-
- private int mTriangleHeight;
-
-
- private int mGuideLineWidth;
-
- private int mGuideLineColor;
-
- /**
- * callback on menu item being click
- */
- private OnMenuItemClickListener mOnMenuItemClickListener;
-
- /**
- * callback on menu item being selected
- */
- private OnMenuSelectedListener mOnMenuSelectedListener;
-
-
- private boolean mIsFirstLayout = true;
-
- private
- @ColorInt
- int mWheelBgColor;
- private
- @ColorInt
- int mCursorColor;
- private float mMenuRadioDimension;
- private float mCenterRadioDimension;
- private float mPaddingRadio;
-
- private boolean mIsDebug = false;
-
- private Path mWheelBgPath = new Path();
-
- private Matrix mBgMatrix = new Matrix();
-
- private Region mBgRegion = new Region();
-
- private Path mGuidePath = new Path();
-
- private Paint mGuidePaint;
-
-
- private int mItemRotateMode = ITEM_ROTATE_MODE_NONE;
-
-
- public CursorWheelLayout(Context context) {
- this(context, null);
- }
-
- public CursorWheelLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CursorWheelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- initWheel(context, attrs);
- }
-
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- public CursorWheelLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- initWheel(context, attrs);
- }
-
- private void initWheel(Context context, AttributeSet attrs) {
- setPadding(0, 0, 0, 0);
- final float density = context.getResources().getDisplayMetrics().density;
- mTriangleHeight = (int) (DEFAULT_TRIANGLE_HEIGHT * density + 0.5);
- mGuideLineWidth = (int) (DEFAULT_GUIDE_LINE_WIDTH * density + 0.5);
- if (attrs != null) {
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CursorWheelLayout);
- mSelectedAngle = ta.getFloat(R.styleable.CursorWheelLayout_wheelSelectedAngle, DEFAULT_SELECTED_ANGLE);
- if (mSelectedAngle > 360) {
- mSelectedAngle %= 360;
- }
- mStartAngle = mSelectedAngle;
- mWheelBgColor = ta.getColor(R.styleable.CursorWheelLayout_wheelBackgroundColor, DEFAULT_WHEEL_BG_COLOR);
- mCursorColor = ta.getColor(R.styleable.CursorWheelLayout_wheelCursorColor, DEFAULT_CURSOR_COLOR);
- mTriangleHeight = ta.getDimensionPixelOffset(R.styleable.CursorWheelLayout_wheelCursorHeight, mTriangleHeight);
- mMenuRadioDimension = ta.getFloat(R.styleable.CursorWheelLayout_wheelItemRadio, RADIO_DEFAULT_CHILD_DIMENSION);
- mCenterRadioDimension = ta.getFloat(R.styleable.CursorWheelLayout_wheelCenterRadio, RADIO_DEFAULT_CENTER_DIMENSION);
- mPaddingRadio = ta.getFloat(R.styleable.CursorWheelLayout_wheelPaddingRadio, RADIO_PADDING_LAYOUT);
- mGuideLineWidth = ta.getDimensionPixelOffset(R.styleable.CursorWheelLayout_wheelGuideLineWidth, mGuideLineWidth);
- mGuideLineColor = ta.getColor(R.styleable.CursorWheelLayout_wheelGuideLineColor, DEFAULT_GUIDE_LINE_COLOR);
- mItemRotateMode = ta.getInt(R.styleable.CursorWheelLayout_wheelItemRotateMode, ITEM_ROTATE_MODE_NONE);
- ta.recycle();
- }
- init(context);
- }
-
- private void init(Context context) {
- setWillNotDraw(false);
- mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mCursorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
- mCursorPaint.setColor(mCursorColor);
- mCursorPaint.setDither(true);
-
- mWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mWheelPaint.setStyle(Paint.Style.FILL);
- mWheelPaint.setColor(mWheelBgColor);
- mWheelPaint.setDither(true);
-
- mGuidePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mGuidePaint.setStrokeWidth(mGuideLineWidth);
- mGuidePaint.setColor(mGuideLineColor);
- mGuidePaint.setDither(true);
- mGuidePaint.setStyle(Paint.Style.STROKE);
-
- mTrianglePath = new Path();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int desiredWidth = getDefaultWidth();
- int desiredHeight = desiredWidth;
- int widthSpec = resolveSizeAndState(desiredWidth, widthMeasureSpec);
- int heightSpec = resolveSizeAndState(desiredHeight, heightMeasureSpec);
- setMeasuredDimension(widthSpec, heightSpec);
-
- mRootDiameter = Math.max(getMeasuredWidth(), getMeasuredHeight());
-
- final int count = getChildCount();
- // menu item 's size
- int childSize = (int) (mRootDiameter * mMenuRadioDimension);
- // menu item 's MeasureSpec
- int childMode = MeasureSpec.EXACTLY;
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
-
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- int makeMeasureSpec = -1;
-
- if (child.getId() == R.id.id_wheel_menu_center_item) {
- makeMeasureSpec = MeasureSpec.makeMeasureSpec(
- (int) (mRootDiameter * mCenterRadioDimension),
- childMode);
- } else {
- makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
- childMode);
- }
- child.measure(makeMeasureSpec, makeMeasureSpec);
- }
- mPadding = mPaddingRadio * mRootDiameter;
- }
-
- private int resolveSizeAndState(int desireSize, int measureSpec) {
- int result = desireSize;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- result = desireSize;
- break;
- case MeasureSpec.AT_MOST:
- if (specSize < desireSize) {
- result = specSize;
- } else {
- result = desireSize;
- }
- break;
- case MeasureSpec.EXACTLY:
- result = specSize;
- break;
- }
- return result;
- }
-
- public View getCenterItem() {
- return findViewById(R.id.id_wheel_menu_center_item);
- }
-
-
- /**
- * init the triangle path
- */
- private void initTriangle() {
- int layoutRadial = (int) (mRootDiameter / 2.0);
- mTrianglePath.moveTo(layoutRadial - mTriangleHeight, 0);
- mTrianglePath.lineTo(layoutRadial, 0 - mTriangleHeight / 2.0f);
- mTrianglePath.lineTo(layoutRadial, 0 + mTriangleHeight / 2.0f);
- mTrianglePath.close();
- }
-
-
- /**
- * @param mOnMenuItemClickListener
- */
- public void setOnMenuItemClickListener(
- OnMenuItemClickListener mOnMenuItemClickListener) {
- this.mOnMenuItemClickListener = mOnMenuItemClickListener;
- }
-
- public void setOnMenuSelectedListener(OnMenuSelectedListener onMenuSelectedListener) {
- mOnMenuSelectedListener = onMenuSelectedListener;
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int layoutDiameter = mRootDiameter;
- int layoutRadial = (int) (layoutDiameter / 2.0);
-
- final int childCount = getChildCount();
-
- int left, top;
- // size of menu item
- int cWidth = (int) (layoutDiameter * mMenuRadioDimension);
-
- float angleDelay;
- if (getCenterItem() != null) {
- angleDelay = 360 / (getChildCount() - 1);
- } else {
- angleDelay = 360 / (getChildCount());
- }
- //angle diff [0,360)
- double minimumAngleDiff = -1;
- double angleDiff;
- double includedAngle = 0;
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
-
- if (child.getId() == R.id.id_wheel_menu_center_item) {
- continue;
- }
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- mStartAngle %= 360;
- includedAngle = mStartAngle;
-// }
- //menu 's angle relative to 0°
- child.setTag(R.id.id_wheel_view_angle, mStartAngle);
- angleDiff = Math.abs(mSelectedAngle - includedAngle);
- angleDiff = angleDiff >= 180 ? 360 - angleDiff : angleDiff;
- //find the intentional selected item
- if (minimumAngleDiff == -1 || minimumAngleDiff > angleDiff) {
- minimumAngleDiff = angleDiff;
- mTempSelectedView = child;
- if (getCenterItem() != null) {
- mTempSelectedPosition = i - 1;
- } else {
- mTempSelectedPosition = i;
- }
- //allowable error
- mNeedSlotIntoCenter = ((int) minimumAngleDiff) != 0;
- }
- // 计算,中心点到menu item中心的距离
- float tmp = layoutRadial - cWidth / 2 - mPadding;
-
- // {tmp*cos(a)-1/2*width}即menu item相对中心点的横坐标
- left = layoutRadial
- + (int) Math.round(tmp
- * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
- * cWidth);
-
- //{tmp*sin(a)-1/2*height}即menu item相对中心点的纵坐标
- top = layoutRadial
- + (int) Math.round(tmp
- * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
- * cWidth);
-
- child.layout(left, top, left + cWidth, top + cWidth);
- float angel;
- switch (mItemRotateMode) {
- case ITEM_ROTATE_MODE_NONE:
- angel = 0;
- break;
- case ITEM_ROTATE_MODE_INWARD:
- angel = (float) (-90 + mStartAngle);
- break;
- case ITEM_ROTATE_MODE_OUTWARD:
- angel = (float) (90 + mStartAngle);
- break;
- default:
- angel = 0;
- break;
- }
- child.setPivotX(cWidth / 2.0f);
- child.setPivotY(cWidth / 2.0f);
- child.setRotation(angel);
- mStartAngle += angleDelay;
-
- }
- //layout center menu
- View cView = findViewById(R.id.id_wheel_menu_center_item);
- if (cView != null) {
- // 设置center item位置
- int cl = layoutRadial - cView.getMeasuredWidth() / 2;
- int cr = cl + cView.getMeasuredWidth();
- cView.layout(cl, cl, cr, cr);
- }
- if (mIsFirstLayout) {
- mIsFirstLayout = false;
- scrollIntoSlots();
- }
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mBgMatrix.reset();
- initTriangle();
- int radial = (int) (mRootDiameter / 2.0f);
- mWheelBgPath.addCircle(0, 0, radial, Path.Direction.CW);
- }
-
- @Override
- public void requestLayout() {
- mIsFirstLayout = true;
- super.requestLayout();
- }
-
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //Draw Wheel's Background
- canvas.save();
- int radial = (int) (mRootDiameter / 2.0f);
- canvas.translate(radial, radial);
- if (mBgMatrix.isIdentity()) {
- canvas.getMatrix().invert(mBgMatrix);
- }
- canvas.drawPath(mWheelBgPath, mWheelPaint);
- canvas.restore();
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
- canvas.save();
- canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f);
- canvas.rotate((float) (mSelectedAngle), 0, 0);
- canvas.drawPath(mTrianglePath, mCursorPaint);
- canvas.restore();
- if (mIsDebug) {
- canvas.save();
- canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f);
- canvas.drawCircle(0, 0, 10, mCursorPaint);
- float angleDelay;
- int startIndex;
- if (getCenterItem() != null) {
- angleDelay = 360 / (getChildCount() - 1);
- startIndex = 1;
- } else {
- angleDelay = 360 / (getChildCount());
- startIndex = 0;
- }
- for (int i = 0; i < 360; i += angleDelay) {
- canvas.save();
- canvas.rotate(i);
- mCursorPaint.setTextAlign(Paint.Align.RIGHT);
- mCursorPaint.setTextSize(28);
- canvas.drawText(i + "°", mRootDiameter / 2.f, 0, mCursorPaint);
- canvas.restore();
- }
- canvas.restore();
- canvas.save();
- canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f);
- View child = getChildAt(startIndex);
- int startAngel = (int) (((Double) child.getTag(R.id.id_wheel_view_angle) + angleDelay / 2.f) % 360);
- for (int i = startIndex; i < getChildCount(); i++) {
- canvas.save();
- canvas.rotate(startAngel);
- canvas.drawLine(0, 0, mRootDiameter / 2f, 0, mCursorPaint);
- mCursorPaint.setTextAlign(Paint.Align.RIGHT);
- mCursorPaint.setTextSize(38);
- startAngel += angleDelay;
- canvas.restore();
- }
- canvas.restore();
- }
- float angleDelay;
- int startIndex;
- if (getCenterItem() != null) {
- angleDelay = 360 / (getChildCount() - 1);
- startIndex = 1;
- } else {
- angleDelay = 360 / (getChildCount());
- startIndex = 0;
- }
- if (mGuideLineWidth > 0 && getChildCount() - startIndex == mMenuItemCount) {
- canvas.save();
- canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f);
- View child = getChildAt(startIndex);
- if (child != null && child.getTag(R.id.id_wheel_view_angle) != null) {
- int startAngel = (int) (((Double) child.getTag(R.id.id_wheel_view_angle) + angleDelay / 2.f) % 360);
- for (int i = startIndex; i < getChildCount(); i++) {
- canvas.save();
- canvas.rotate(startAngel);
- mGuidePath.reset();
- mGuidePath.moveTo(0, 0);
- mGuidePath.lineTo(mRootDiameter / 2.f, 0);
- canvas.drawPath(mGuidePath, mGuidePaint);
- startAngel += angleDelay;
- canvas.restore();
- }
- }
- canvas.restore();
- }
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- float x = event.getX();
- float y = event.getY();
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- if (!isEventInWheel(x, y)) {
- return false;
- }
- mLastX = x;
- mLastY = y;
- mDownTime = System.currentTimeMillis();
- mTmpAngle = 0;
- mIsDraging = false;
- if (mIsFling) {
- removeCallbacks(mFlingRunnable);
- mIsFling = false;
- return true;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- /**
- * 获得开始的角度
- */
- final float start = getAngle(mLastX, mLastY);
- /**
- * 获得当前的角度
- */
- final float end = getAngle(x, y);
-
- // 如果是一、四象限,则直接(end-start)代表角度改变值(正是上移动负是下拉)
- if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {
- mStartAngle += end - start;
- mTmpAngle += end - start;
- } else
- // 二、三象限,(start - end)代表角度改变值
- {
- mStartAngle += start - end;
- mTmpAngle += start - end;
- }
- mIsDraging = true;
- // 重新布局
- requestLayout();
-
- mLastX = x;
- mLastY = y;
-
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- // 计算,每秒移动的角度
- float anglePerSecond = mTmpAngle * 1000
- / (System.currentTimeMillis() - mDownTime);
- // 如果达到该值认为是快速移动
- if (Math.abs(anglePerSecond) > mFlingableValue && !mIsFling) {
- mFlingRunnable.startUsingVelocity(anglePerSecond);
- return true;
- }
- mIsFling = false;
- mIsDraging = false;
- mFlingRunnable.stop(false);
- scrollIntoSlots();
- // 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击
- if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
- return true;
- }
-
- break;
- }
- return super.dispatchTouchEvent(event);
- }
-
- /**
- * @param x the X coordinate of this event for the touching pointer
- * @param y the Y coordinate of this event for the touching pointer
- * @return Is touching the wheel
- */
- private boolean isEventInWheel(float x, float y) {
- float[] pts = new float[2];
- pts[0] = x;
- pts[1] = y;
- mBgMatrix.mapPoints(pts);
- RectF bounds = new RectF();
- mWheelBgPath.computeBounds(bounds, true);
- mBgRegion.setPath(mWheelBgPath, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom));
- return mBgRegion.contains((int) pts[0], (int) pts[1]);
- }
-
- /**
- * 如果触摸事件交由自己处理,都接受好了
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return true;
- }
-
- /**
- * 根据触摸的位置,计算角度返
- *
- * @param xTouch
- * @param yTouch
- * @return 返回的是普通数字所代表的最小夹角的角度值,如45度就是45而不是1/4×PI;
- * 如果是钝角,如315度,返回结果是-45;
- * 而需要注意的是在同水平方向返回值都为0,垂直方向为90/-90
- */
- private float getAngle(float xTouch, float yTouch) {
- double x = xTouch - (mRootDiameter / 2d);
- double y = yTouch - (mRootDiameter / 2d);
- return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
- }
-
- /**
- * 根据当前位置计算象限
- *
- * @param x
- * @param y
- * @return
- */
- private int getQuadrant(float x, float y) {
- int tmpX = (int) (x - mRootDiameter / 2);
- int tmpY = (int) (y - mRootDiameter / 2);
- if (tmpX >= 0) {
- return tmpY >= 0 ? 4 : 1;
- } else {
- return tmpY >= 0 ? 3 : 2;
- }
-
- }
-
- public void setDebug(boolean debug) {
- mIsDebug = debug;
- }
-
- public void setAdapter(CycleWheelAdapter adapter) {
- if (adapter == null) {
- throw new IllegalArgumentException("Can not set a null adbapter to CursorWheelLayout!!!");
- }
-
- if (mWheelAdapter != null) {
- if (mWheelDataSetObserver != null) {
- mWheelAdapter.unregisterDataSetObserver(mWheelDataSetObserver);
- }
- removeAllViews();
- mWheelDataSetObserver = null;
- }
- mWheelAdapter = adapter;
- mWheelDataSetObserver = new WheelDataSetObserver();
- mWheelAdapter.registerDataSetObserver(mWheelDataSetObserver);
- addMenuItems();
- }
-
- private void onDateSetChanged() {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "onDateSetChanged() called with: " + "");
- }
- mFlingRunnable.stop(false);
- removeAllViews();
- addMenuItems();
- mStartAngle = mSelectedAngle;
- mSelectedPosition = INVALID_POSITION;
- mTempSelectedPosition = INVALID_POSITION;
- mSelectedView = null;
- mTempSelectedView = null;
- mIsDraging = false;
- mIsFling = false;
- requestLayout();
- }
-
-
- /**
- * add menu item to this layout
- */
- private void addMenuItems() {
- if (mWheelAdapter == null || mWheelAdapter.getCount() == 0) {
- throw new IllegalArgumentException("Empty menu source!");
- }
- mMenuItemCount = mWheelAdapter.getCount();
- View view;
- for (int i = 0; i < mMenuItemCount; i++) {
- final int j = i;
- view = mWheelAdapter.getView(this, i);
- //it will be ignore the origin onClickListener
- view.setOnClickListener(new InnerClickListener(i));
- addView(view);
- }
- }
-
- /**
- * @param mPadding
- */
- public void setPadding(float mPadding) {
- this.mPadding = mPadding;
- invalidate();
- }
-
- /**
- * @return
- */
- private int getDefaultWidth() {
- WindowManager wm = (WindowManager) getContext().getSystemService(
- Context.WINDOW_SERVICE);
- DisplayMetrics outMetrics = new DisplayMetrics();
- wm.getDefaultDisplay().getMetrics(outMetrics);
- return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
- }
-
-
- private void endFling(boolean scrollIntoSlots) {
- mIsDraging = false;
- mIsFling = false;
- if (scrollIntoSlots) {
- scrollIntoSlots();
- }
- }
-
- /**
- * Scrolls the items so that the selected item is in its 'slot' (its center
- * is the Wheel's center).
- */
- private void scrollIntoSlots() {
- scrollIntoSlots(true);
- }
-
- /**
- * Scrolls the items so that the selected item is in its 'slot' (its center
- * is the Wheel's center).
- *
- * @param showAnimation Weather show fling animation or not
- */
- private void scrollIntoSlots(boolean showAnimation) {
- if (mIsDraging || mIsFling) {
- return;
- }
-
- if (getChildCount() == 0 || mTempSelectedView == null) {
- return;
- }
-
- if (!mNeedSlotIntoCenter) {
- if (mSelectedView != mTempSelectedView || mSelectedPosition != mTempSelectedPosition) {
- onInnerItemUnselected(mSelectedView);
- mSelectedView = mTempSelectedView;
- onInnerItemSelected(mSelectedView);
- mSelectedPosition = mTempSelectedPosition;
- mTempSelectedView = null;
- mTempSelectedPosition = INVALID_POSITION;
- selectionChangeCallback();
- }
- } else {
- double angle;
- try {
- angle = (double) mTempSelectedView.getTag(R.id.id_wheel_view_angle);
- } catch (NullPointerException e) {
- return;
- }
- if (angle > 360) {
- Log.w(TAG, "scrollIntoSlots:" + angle + " > 360, may be something wrong with calculate angle onLayout");
- }
- double diff = Math.abs(mSelectedAngle - angle);
- diff = diff >= 180 ? 360 - diff : diff;
- double diagonal = (angle + 180) % 360;
- boolean clockWise;
- if (diagonal < angle) {
- clockWise = (mSelectedAngle <= diagonal || mSelectedAngle >= angle);
- } else {
- clockWise = (mSelectedAngle >= angle && mSelectedAngle <= diagonal);
- }
- double sweepAngle = diff * (clockWise ? 1 : -1);
- if (showAnimation) {
- mFlingRunnable.stop(false);
- mFlingRunnable.startUsingAngle(sweepAngle);
- } else {
- mStartAngle += sweepAngle;
- requestLayout();
- }
- }
- }
-
-
- /**
- * do some callback when selected position change
- */
- private final void selectionChangeCallback() {
-// if (BuildConfig.DEBUG) {
-// Log.d(TAG, "selectionChangeCallback() called with: mSelectedPosition" + mSelectedPosition);
-// }
- if (mOnMenuSelectedListener != null) {
- mOnMenuSelectedListener.onItemSelected(this, mSelectedView, mSelectedPosition);
- }
- }
-
-
- /**
- * Responsible for fling behavior.
- *
- * @author chensuilun
- */
- private class FlingRunnable implements Runnable {
-
- private static final int DEFAULT_REFRESH_TIME = 16;
-
- /**
- * 滑动速度
- */
- private float mAngelPerSecond;
- /**
- * 是否以旋转某个角度为目的
- */
- private boolean mStartUsingAngle;
- /**
- * 最终的角度
- */
- private double mEndAngle;
- /**
- * 需要旋转的角度
- */
- private double mSweepAngle;
- /**
- *
- */
- private boolean mBiggerBefore;
- /**
- * 记录下开始转动的时候的startAngle,因为{@link CursorWheelLayout#mStartAngle}属于[0,360),如果直接用来和{@link FlingRunnable#mEndAngle}比较大小就会比较麻烦了
- */
- private double mInitStarAngle;
-
- private void startCommon() {
- // Remove any pending flings
- removeCallbacks(this);
- }
-
- public FlingRunnable() {
-
- }
-
- public void stop(boolean scrollIntoSlots) {
- removeCallbacks(this);
- endFling(scrollIntoSlots);
- }
-
- /**
- * @param velocity
- */
- public void startUsingVelocity(float velocity) {
- mStartUsingAngle = false;
- startCommon();
- this.mAngelPerSecond = velocity;
- post(this);
-
- }
-
- /**
- * @param angle
- */
- public void startUsingAngle(double angle) {
- mStartUsingAngle = true;
- mSweepAngle = angle;
- mInitStarAngle = mStartAngle;
- mEndAngle = mSweepAngle + mInitStarAngle;
- mBiggerBefore = mInitStarAngle >= mEndAngle;
-// if (BuildConfig.DEBUG) {
-// Log.d(TAG, "startUsingAngle() called with: " + "angle = [" + angle + "]" + ",mStartAngle:" + mInitStarAngle + ",mEndAngle:" + mEndAngle);
-// }
- post(this);
- }
-
- public void run() {
- if (mMenuItemCount == 0) {
- stop(true);
- return;
- }
- if (!mStartUsingAngle) {
- if ((int) Math.abs(mAngelPerSecond) < 20) {
- stop(true);
- return;
- }
- mIsFling = true;
- mStartAngle = mStartAngle + (mAngelPerSecond / 30);
- mAngelPerSecond /= 1.0666F;
- } else {
- mStartAngle %= 360;
- if (Math.abs((int) (mEndAngle - mInitStarAngle)) == 0 || (mBiggerBefore && (mInitStarAngle < mEndAngle)) || (!mBiggerBefore && (mInitStarAngle > mEndAngle))) {
- mNeedSlotIntoCenter = false;
- stop(true);
- return;
- }
- mIsFling = true;
- double change = mSweepAngle / 5;
- mInitStarAngle += change;
- mStartAngle += change;
- }
- postDelayed(this, DEFAULT_REFRESH_TIME);
- requestLayout();
- }
- }
-
- /**
- * Interface definition for a callback to be invoked when a view is clicked.
- *
- * @author chensuilun
- */
- public interface OnMenuItemClickListener {
-
- void onItemClick(View view, int pos);
-
- }
-
- /**
- * @author chensuilun
- */
- private class InnerClickListener implements OnClickListener {
- private final int mPosition;
-
- public InnerClickListener(int position) {
- mPosition = position;
- }
-
- @Override
- public void onClick(View v) {
- if (mSelectedView == v || mTempSelectedView == v) {
- return;
- }
- mFlingRunnable.stop(false);
- mIsDraging = false;
- mIsFling = false;
- mTempSelectedPosition = mPosition;
- mTempSelectedView = v;
- mNeedSlotIntoCenter = true;
- scrollIntoSlots();
- if (mOnMenuItemClickListener != null) {
- mOnMenuItemClickListener.onItemClick(v, mPosition);
- }
- }
- }
-
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- /**
- * @param position
- */
- public void setSelection(int position) {
- setSelection(position, true);
- }
-
- public int getItemRotateMode() {
- return mItemRotateMode;
- }
-
- /**
- * @param position
- */
- public void setSelection(final int position, final boolean animation) {
- if (position > mMenuItemCount) {
- throw new IllegalArgumentException("Position:" + position + " is out of index!");
- }
- post(new Runnable() {
- @Override
- public void run() {
- int itemPosition = getCenterItem() == null ? position : position + 1;
- mFlingRunnable.stop(false);
- mIsDraging = false;
- mIsFling = false;
- mTempSelectedPosition = itemPosition;
- mTempSelectedView = getChildAt(itemPosition);
- mNeedSlotIntoCenter = true;
- scrollIntoSlots(animation);
- }
- });
- }
-
-
- public void setSelectedAngle(double selectedAngle) {
- if (selectedAngle < 0) {
- return;
- }
- if (selectedAngle > 360) {
- selectedAngle %= 360;
- }
- mSelectedAngle = selectedAngle;
- requestLayout();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- removeAllViews();
- super.onDetachedFromWindow();
- mFlingRunnable.stop(false);
- mIsFirstLayout = false;
- }
-
- /**
- * to do whatever you want to perform the selected view
- *
- * @param v
- */
- protected void onInnerItemSelected(View v) {
-
- }
-
- /**
- * to do whatever you want to perform the unselected view
- *
- * @param v
- */
- protected void onInnerItemUnselected(View v) {
-
- }
-
-
- /**
- * callback when item selected
- *
- * @author chensuilun
- */
- public interface OnMenuSelectedListener {
-
- void onItemSelected(CursorWheelLayout parent, View view, int pos);
- }
-
- private WheelDataSetObserver mWheelDataSetObserver;
-
- /**
- * @author chensuilun
- */
- public class WheelDataSetObserver extends DataSetObserver {
- @Override
- public void onChanged() {
- super.onChanged();
- onDateSetChanged();
- }
- }
-
- /**
- * An Adapter object acts as a bridge between an {@link CursorWheelLayout} and the
- * underlying data for that view. The Adapter provides access to the data items.
- * The Adapter is also responsible for making a {@link View} for
- * each item in the data set.
- *
- * @author chensuilun
- */
- public static abstract class CycleWheelAdapter {
-
- private final DataSetObservable mDataSetObservable = new DataSetObservable();
-
- public void registerDataSetObserver(DataSetObserver observer) {
- mDataSetObservable.registerObserver(observer);
- }
-
- public void unregisterDataSetObserver(DataSetObserver observer) {
- mDataSetObservable.unregisterObserver(observer);
- }
-
- /**
- * Notifies the attached observers that the underlying data has been changed
- * and any View reflecting the data set should refresh itself.
- */
- public void notifyDataSetChanged() {
- mDataSetObservable.notifyChanged();
- }
-
- /**
- * How many menu items are in the data set represented by this Adapter.
- *
- * @return Count of items.
- */
- public abstract int getCount();
-
- /**
- * Get a View that displays the data at the specified position in the data set.
- *
- * @param parent
- * @param position
- * @return
- */
- public abstract View getView(View parent, int position);
-
- /**
- * Get the data item associated with the specified position in the data set.
- *
- * @param position Position of the item whose data we want within the adapter's
- * data set.
- * @return The data at the specified position.
- */
- public abstract Object getItem(int position);
- }
-
-}
diff --git a/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.kt b/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.kt
new file mode 100644
index 0000000..f98749f
--- /dev/null
+++ b/library/src/main/java/github/hellocsl/cursorwheel/CursorWheelLayout.kt
@@ -0,0 +1,1099 @@
+package github.hellocsl.cursorwheel
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.database.DataSetObservable
+import android.database.DataSetObserver
+import android.graphics.*
+import android.os.Build
+import android.util.AttributeSet
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.annotation.ColorInt
+import kotlin.math.*
+
+/**
+ * The base cycle wheel menu layout with cursor
+ *
+ * @author chensuilun
+ * @attr R.styleable.CursorWheelLayout_wheelSelectedAngle
+ * @attr R.styleable.CursorWheelLayout_wheelPaddingRadio
+ * @attr R.styleable.CursorWheelLayout_wheelCenterRadio
+ * @attr R.styleable.CursorWheelLayout_wheelItemRadio
+ * @attr R.styleable.CursorWheelLayout_wheelFlingValue
+ * @attr R.styleable.CursorWheelLayout_wheelCursorColor
+ * @attr R.styleable.CursorWheelLayout_wheelCursorHeight
+ * @attr R.styleable.CursorWheelLayout_wheelItemRotateMode
+ * @attr R.styleable.CursorWheelLayout_wheelGuideLineWidth
+ * @attr R.styleable.CursorWheelLayout_wheelGuideLineColor
+ */
+open class CursorWheelLayout : ViewGroup {
+ /**
+ * CircleMenuLayout 's size
+ */
+ private var mRootDiameter = 0
+
+ /**
+ * Angle a touch can wander before we think the user is flinging
+ */
+ private val mFlingableValue = FLINGABLE_VALUE
+ private var mPadding = 0f
+ private var mStartAngle = 0.0
+
+ /**
+ * menu 's source data
+ */
+ private var mWheelAdapter: CycleWheelAdapter? = null
+
+ /**
+ *
+ */
+ private var mMenuItemCount = 0
+
+ /**
+ *
+ */
+ private var mTmpAngle = 0f
+
+ /**
+ *
+ */
+ private var mDownTime: Long = 0
+
+ /**
+ * weather is fling now
+ */
+ private var mIsFling = false
+
+ /**
+ *
+ */
+ private var mIsDraging = false
+
+ /**
+ *
+ */
+ private var mLastX = 0f
+ private var mLastY = 0f
+
+ /**
+ * [0,360)
+ */
+ private var mSelectedAngle = DEFAULT_SELECTED_ANGLE.toDouble()
+
+ /**
+ * The currently selected item's child.
+ */
+ private var mSelectedView: View? = null
+
+ /**
+ * The position of the selected View
+ */
+ var selectedPosition = INVALID_POSITION
+ private set
+
+ /**
+ * The temp selected item's child.
+ */
+ private var mTempSelectedView: View? = null
+
+ /**
+ * The position of the temp selected item's child.
+ */
+ private var mTempSelectedPosition = INVALID_POSITION
+
+ /**
+ * 判断是否需要滚动到最中间,某些时候由于选中角度和布局中心的角度一样,所以不需要在进行一次移动
+ */
+ private var mNeedSlotIntoCenter = false
+ private val mFlingRunnable: FlingRunnable = FlingRunnable()
+
+ /**
+ * draw cursor
+ */
+ private var mCursorPaint: Paint? = null
+
+ /**
+ * draw wheel bg
+ */
+ private var mWheelPaint: Paint? = null
+
+ /**
+ * path of cursor
+ */
+ private var mTrianglePath: Path? = null
+ private var mTriangleHeight = 0
+ private var mGuideLineWidth = 0
+ private var mGuideLineColor = 0
+
+ /**
+ * callback on menu item being click
+ */
+ private var mOnMenuItemClickListener: OnMenuItemClickListener? = null
+
+ /**
+ * callback on menu item being selected
+ */
+ private var mOnMenuSelectedListener: OnMenuSelectedListener? = null
+ private var mIsFirstLayout = true
+
+ @ColorInt
+ private var mWheelBgColor = 0
+
+ @ColorInt
+ private var mCursorColor = 0
+ private var mMenuRadioDimension = 0f
+ private var mCenterRadioDimension = 0f
+ private var mPaddingRadio = 0f
+ private var mIsDebug = false
+ private val mWheelBgPath = Path()
+ private val mBgMatrix = Matrix()
+ private val mBgRegion = Region()
+ private val mGuidePath = Path()
+ private var mGuidePaint: Paint? = null
+ private var itemRotateMode = ITEM_ROTATE_MODE_NONE
+
+ @JvmOverloads
+ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(
+ context, attrs, defStyleAttr
+ ) {
+ initWheel(context, attrs)
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes) {
+ initWheel(context, attrs)
+ }
+
+ private fun initWheel(context: Context, attrs: AttributeSet?) {
+ setPadding(0, 0, 0, 0)
+ val density = context.resources.displayMetrics.density
+ mTriangleHeight = (DEFAULT_TRIANGLE_HEIGHT * density + 0.5).toInt()
+ mGuideLineWidth = (DEFAULT_GUIDE_LINE_WIDTH * density + 0.5).toInt()
+ if (attrs != null) {
+ val ta = context.obtainStyledAttributes(attrs, R.styleable.CursorWheelLayout)
+ mSelectedAngle = ta.getFloat(
+ R.styleable.CursorWheelLayout_wheelSelectedAngle,
+ DEFAULT_SELECTED_ANGLE
+ ).toDouble()
+ if (mSelectedAngle > 360) {
+ mSelectedAngle %= 360.0
+ }
+ mStartAngle = mSelectedAngle
+ mWheelBgColor = ta.getColor(
+ R.styleable.CursorWheelLayout_wheelBackgroundColor,
+ DEFAULT_WHEEL_BG_COLOR
+ )
+ mCursorColor =
+ ta.getColor(R.styleable.CursorWheelLayout_wheelCursorColor, DEFAULT_CURSOR_COLOR)
+ mTriangleHeight = ta.getDimensionPixelOffset(
+ R.styleable.CursorWheelLayout_wheelCursorHeight,
+ mTriangleHeight
+ )
+ mMenuRadioDimension = ta.getFloat(
+ R.styleable.CursorWheelLayout_wheelItemRadio,
+ RADIO_DEFAULT_CHILD_DIMENSION
+ )
+ mCenterRadioDimension = ta.getFloat(
+ R.styleable.CursorWheelLayout_wheelCenterRadio,
+ RADIO_DEFAULT_CENTER_DIMENSION
+ )
+ mPaddingRadio =
+ ta.getFloat(R.styleable.CursorWheelLayout_wheelPaddingRadio, RADIO_PADDING_LAYOUT)
+ mGuideLineWidth = ta.getDimensionPixelOffset(
+ R.styleable.CursorWheelLayout_wheelGuideLineWidth,
+ mGuideLineWidth
+ )
+ mGuideLineColor = ta.getColor(
+ R.styleable.CursorWheelLayout_wheelGuideLineColor,
+ DEFAULT_GUIDE_LINE_COLOR
+ )
+ itemRotateMode =
+ ta.getInt(R.styleable.CursorWheelLayout_wheelItemRotateMode, ITEM_ROTATE_MODE_NONE)
+ ta.recycle()
+ }
+ init(context)
+ }
+
+ private fun init(context: Context) {
+ setWillNotDraw(false)
+ mCursorPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mCursorPaint!!.style = Paint.Style.FILL_AND_STROKE
+ mCursorPaint!!.color = mCursorColor
+ mCursorPaint!!.isDither = true
+ mWheelPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mWheelPaint!!.style = Paint.Style.FILL
+ mWheelPaint!!.color = mWheelBgColor
+ mWheelPaint!!.isDither = true
+ mGuidePaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ mGuidePaint!!.strokeWidth = mGuideLineWidth.toFloat()
+ mGuidePaint!!.color = mGuideLineColor
+ mGuidePaint!!.isDither = true
+ mGuidePaint!!.style = Paint.Style.STROKE
+ mTrianglePath = Path()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val desiredWidth = defaultWidth
+ val widthSpec = resolveSizeAndState(desiredWidth, widthMeasureSpec)
+ val heightSpec = resolveSizeAndState(desiredWidth, heightMeasureSpec)
+ setMeasuredDimension(widthSpec, heightSpec)
+ mRootDiameter = measuredWidth.coerceAtLeast(measuredHeight)
+ val count = childCount
+ // menu item 's size
+ val childSize = (mRootDiameter * mMenuRadioDimension).toInt()
+ // menu item 's MeasureSpec
+ val childMode = MeasureSpec.EXACTLY
+ for (i in 0 until count) {
+ val child = getChildAt(i)
+ if (child.visibility == GONE) {
+ continue
+ }
+ var makeMeasureSpec = -1
+ makeMeasureSpec = if (child.id == R.id.id_wheel_menu_center_item) {
+ MeasureSpec.makeMeasureSpec(
+ (mRootDiameter * mCenterRadioDimension).toInt(),
+ childMode
+ )
+ } else {
+ MeasureSpec.makeMeasureSpec(
+ childSize,
+ childMode
+ )
+ }
+ child.measure(makeMeasureSpec, makeMeasureSpec)
+ }
+ mPadding = mPaddingRadio * mRootDiameter
+ }
+
+ private fun resolveSizeAndState(desireSize: Int, measureSpec: Int): Int {
+ var result = desireSize
+ val specMode = MeasureSpec.getMode(measureSpec)
+ val specSize = MeasureSpec.getSize(measureSpec)
+ when (specMode) {
+ MeasureSpec.UNSPECIFIED -> result = desireSize
+ MeasureSpec.AT_MOST -> result = Math.min(specSize, desireSize)
+ MeasureSpec.EXACTLY -> result = specSize
+ }
+ return result
+ }
+
+ val centerItem: View?
+ get() = findViewById(R.id.id_wheel_menu_center_item)
+
+ /**
+ * init the triangle path
+ */
+ private fun initTriangle() {
+ val layoutRadial = (mRootDiameter / 2.0).toInt()
+ mTrianglePath!!.moveTo(layoutRadial - mTriangleHeight.toFloat(), 0f)
+ mTrianglePath!!.lineTo(layoutRadial.toFloat(), 0 - mTriangleHeight / 2.0f)
+ mTrianglePath!!.lineTo(layoutRadial.toFloat(), 0 + mTriangleHeight / 2.0f)
+ mTrianglePath!!.close()
+ }
+
+ /**
+ * @param mOnMenuItemClickListener
+ */
+ fun setOnMenuItemClickListener(
+ mOnMenuItemClickListener: OnMenuItemClickListener?
+ ) {
+ this.mOnMenuItemClickListener = mOnMenuItemClickListener
+ }
+
+ fun setOnMenuSelectedListener(onMenuSelectedListener: OnMenuSelectedListener) {
+ mOnMenuSelectedListener = onMenuSelectedListener
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ val layoutDiameter = mRootDiameter
+ val layoutRadial = (layoutDiameter / 2.0).toInt()
+ val childCount = childCount
+ var left: Int
+ var top: Int
+ // size of menu item
+ val cWidth = (layoutDiameter * mMenuRadioDimension).toInt()
+ val angleDelay: Float = if (centerItem != null) {
+ 360 / (getChildCount() - 1).toFloat()
+ } else {
+ 360 / getChildCount().toFloat()
+ }
+ //angle diff [0,360)
+ var minimumAngleDiff = -1.0
+ var angleDiff: Double
+ var includedAngle = 0.0
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ if (child.id == R.id.id_wheel_menu_center_item) {
+ continue
+ }
+ if (child.visibility == GONE) {
+ continue
+ }
+ mStartAngle %= 360.0
+ includedAngle = mStartAngle
+ // }
+ //menu 's angle relative to 0°
+ child.setTag(R.id.id_wheel_view_angle, mStartAngle)
+ angleDiff = Math.abs(mSelectedAngle - includedAngle)
+ angleDiff = if (angleDiff >= 180) 360 - angleDiff else angleDiff
+ //find the intentional selected item
+ if (minimumAngleDiff == -1.0 || minimumAngleDiff > angleDiff) {
+ minimumAngleDiff = angleDiff
+ mTempSelectedView = child
+ mTempSelectedPosition = if (centerItem != null) {
+ i - 1
+ } else {
+ i
+ }
+ //allowable error
+ mNeedSlotIntoCenter = minimumAngleDiff.toInt() != 0
+ }
+ // 计算,中心点到menu item中心的距离
+ val tmp = layoutRadial - cWidth / 2 - mPadding
+
+ // {tmp*cos(a)-1/2*width}即menu item相对中心点的横坐标
+ left = (layoutRadial
+ + (tmp
+ * cos(Math.toRadians(mStartAngle)) - 1 / 2f
+ * cWidth).roundToInt())
+
+ //{tmp*sin(a)-1/2*height}即menu item相对中心点的纵坐标
+ top = (layoutRadial
+ + (tmp * sin(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth).roundToInt())
+ child.layout(left, top, left + cWidth, top + cWidth)
+ val angel: Float = when (itemRotateMode) {
+ ITEM_ROTATE_MODE_NONE -> 0f
+ ITEM_ROTATE_MODE_INWARD -> (-90 + mStartAngle).toFloat()
+ ITEM_ROTATE_MODE_OUTWARD -> (90 + mStartAngle).toFloat()
+ else -> 0f
+ }
+ child.pivotX = cWidth / 2.0f
+ child.pivotY = cWidth / 2.0f
+ child.rotation = angel
+ mStartAngle += angleDelay.toDouble()
+ }
+ //layout center menu
+ val cView = findViewById(R.id.id_wheel_menu_center_item)
+ if (cView != null) {
+ // 设置center item位置
+ val cl = layoutRadial - cView.measuredWidth / 2
+ val cr = cl + cView.measuredWidth
+ cView.layout(cl, cl, cr, cr)
+ }
+ if (mIsFirstLayout) {
+ mIsFirstLayout = false
+ scrollIntoSlots()
+ }
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ mBgMatrix.reset()
+ initTriangle()
+ val radial = (mRootDiameter / 2.0f).toInt()
+ mWheelBgPath.addCircle(0f, 0f, radial.toFloat(), Path.Direction.CW)
+ }
+
+ override fun requestLayout() {
+ mIsFirstLayout = true
+ super.requestLayout()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ //Draw Wheel's Background
+ canvas.save()
+ val radial = (mRootDiameter / 2.0f).toInt()
+ canvas.translate(radial.toFloat(), radial.toFloat())
+ if (mBgMatrix.isIdentity) {
+ canvas.matrix.invert(mBgMatrix)
+ }
+ canvas.drawPath(mWheelBgPath, mWheelPaint!!)
+ canvas.restore()
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ super.dispatchDraw(canvas)
+ canvas.save()
+ canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f)
+ canvas.rotate(mSelectedAngle.toFloat(), 0f, 0f)
+ canvas.drawPath(mTrianglePath!!, mCursorPaint!!)
+ canvas.restore()
+ if (mIsDebug) {
+ canvas.save()
+ canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f)
+ canvas.drawCircle(0f, 0f, 10f, mCursorPaint!!)
+ val angleDelay: Float
+ val startIndex: Int
+ if (centerItem != null) {
+ angleDelay = 360 / (childCount - 1).toFloat()
+ startIndex = 1
+ } else {
+ angleDelay = 360 / childCount.toFloat()
+ startIndex = 0
+ }
+ run {
+ var i = 0
+ while (i < 360) {
+ canvas.save()
+ canvas.rotate(i.toFloat())
+ mCursorPaint!!.textAlign = Paint.Align.RIGHT
+ mCursorPaint!!.textSize = 28f
+ canvas.drawText("$i°", mRootDiameter / 2f, 0f, mCursorPaint!!)
+ canvas.restore()
+ i += angleDelay.toInt()
+ }
+ }
+ canvas.restore()
+ canvas.save()
+ canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f)
+ val child = getChildAt(startIndex)
+ var startAngel =
+ ((child.getTag(R.id.id_wheel_view_angle) as Double + angleDelay / 2f) % 360).toInt()
+ for (i in startIndex until childCount) {
+ canvas.save()
+ canvas.rotate(startAngel.toFloat())
+ canvas.drawLine(0f, 0f, mRootDiameter / 2f, 0f, mCursorPaint!!)
+ mCursorPaint!!.textAlign = Paint.Align.RIGHT
+ mCursorPaint!!.textSize = 38f
+ startAngel += angleDelay.toInt()
+ canvas.restore()
+ }
+ canvas.restore()
+ }
+ val angleDelay: Float
+ val startIndex: Int
+ if (centerItem != null) {
+ angleDelay = 360 / (childCount - 1).toFloat()
+ startIndex = 1
+ } else {
+ angleDelay = 360 / childCount.toFloat()
+ startIndex = 0
+ }
+ if (mGuideLineWidth > 0 && childCount - startIndex == mMenuItemCount) {
+ canvas.save()
+ canvas.translate(mRootDiameter / 2f, mRootDiameter / 2f)
+ val child = getChildAt(startIndex)
+ if (child?.getTag(R.id.id_wheel_view_angle) != null) {
+ var startAngel =
+ ((child.getTag(R.id.id_wheel_view_angle) as Double + angleDelay / 2f) % 360).toInt()
+ for (i in startIndex until childCount) {
+ canvas.save()
+ canvas.rotate(startAngel.toFloat())
+ mGuidePath.reset()
+ mGuidePath.moveTo(0f, 0f)
+ mGuidePath.lineTo(mRootDiameter / 2f, 0f)
+ canvas.drawPath(mGuidePath, mGuidePaint!!)
+ startAngel += angleDelay.toInt()
+ canvas.restore()
+ }
+ }
+ canvas.restore()
+ }
+ }
+
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+ val x = event.x
+ val y = event.y
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (!isEventInWheel(x, y)) {
+ return false
+ }
+ mLastX = x
+ mLastY = y
+ mDownTime = System.currentTimeMillis()
+ mTmpAngle = 0f
+ mIsDraging = false
+ if (mIsFling) {
+ removeCallbacks(mFlingRunnable)
+ mIsFling = false
+ return true
+ }
+ }
+ MotionEvent.ACTION_MOVE -> {
+ /**
+ * 获得开始的角度
+ */
+ val start = getAngle(mLastX, mLastY)
+
+ /**
+ * 获得当前的角度
+ */
+ val end = getAngle(x, y)
+
+ // 如果是一、四象限,则直接(end-start)代表角度改变值(正是上移动负是下拉)
+ if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {
+ mStartAngle += end - start.toDouble()
+ mTmpAngle += end - start
+ } else // 二、三象限,(start - end)代表角度改变值
+ {
+ mStartAngle += start - end.toDouble()
+ mTmpAngle += start - end
+ }
+ mIsDraging = true
+ // 重新布局
+ requestLayout()
+ mLastX = x
+ mLastY = y
+ }
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
+ // 计算,每秒移动的角度
+ val anglePerSecond = (mTmpAngle * 1000
+ / (System.currentTimeMillis() - mDownTime))
+ // 如果达到该值认为是快速移动
+ if (Math.abs(anglePerSecond) > mFlingableValue && !mIsFling) {
+ mFlingRunnable.startUsingVelocity(anglePerSecond)
+ return true
+ }
+ mIsFling = false
+ mIsDraging = false
+ mFlingRunnable.stop(false)
+ scrollIntoSlots()
+ // 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击
+ if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
+ return true
+ }
+ }
+ }
+ return super.dispatchTouchEvent(event)
+ }
+
+ /**
+ * @param x the X coordinate of this event for the touching pointer
+ * @param y the Y coordinate of this event for the touching pointer
+ * @return Is touching the wheel
+ */
+ private fun isEventInWheel(x: Float, y: Float): Boolean {
+ val pts = FloatArray(2)
+ pts[0] = x
+ pts[1] = y
+ mBgMatrix.mapPoints(pts)
+ val bounds = RectF()
+ mWheelBgPath.computeBounds(bounds, true)
+ mBgRegion.setPath(
+ mWheelBgPath,
+ Region(
+ bounds.left.toInt(),
+ bounds.top.toInt(),
+ bounds.right.toInt(),
+ bounds.bottom.toInt()
+ )
+ )
+ return mBgRegion.contains(pts[0].toInt(), pts[1].toInt())
+ }
+
+ /**
+ * 如果触摸事件交由自己处理,都接受好了
+ */
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return true
+ }
+
+ /**
+ * 根据触摸的位置,计算角度返
+ *
+ * @param xTouch
+ * @param yTouch
+ * @return 返回的是普通数字所代表的最小夹角的角度值,如45度就是45而不是1/4×PI;
+ * 如果是钝角,如315度,返回结果是-45;
+ * 而需要注意的是在同水平方向返回值都为0,垂直方向为90/-90
+ */
+ private fun getAngle(xTouch: Float, yTouch: Float): Float {
+ val x = xTouch - mRootDiameter / 2.0
+ val y = yTouch - mRootDiameter / 2.0
+ return (asin(y / hypot(x, y)) * 180 / Math.PI).toFloat()
+ }
+
+ /**
+ * 根据当前位置计算象限
+ *
+ * @param x
+ * @param y
+ * @return
+ */
+ private fun getQuadrant(x: Float, y: Float): Int {
+ val tmpX = (x - mRootDiameter / 2).toInt()
+ val tmpY = (y - mRootDiameter / 2).toInt()
+ return if (tmpX >= 0) {
+ if (tmpY >= 0) 4 else 1
+ } else {
+ if (tmpY >= 0) 3 else 2
+ }
+ }
+
+ fun setDebug(debug: Boolean) {
+ mIsDebug = debug
+ }
+
+ fun setAdapter(adapter: CycleWheelAdapter?) {
+ requireNotNull(adapter) { "Can not set a null adbapter to CursorWheelLayout!!!" }
+ if (mWheelAdapter != null) {
+ if (mWheelDataSetObserver != null) {
+ mWheelAdapter!!.unregisterDataSetObserver(mWheelDataSetObserver!!)
+ }
+ removeAllViews()
+ mWheelDataSetObserver = null
+ }
+ mWheelAdapter = adapter
+ mWheelDataSetObserver = WheelDataSetObserver()
+ mWheelAdapter!!.registerDataSetObserver(mWheelDataSetObserver!!)
+ addMenuItems()
+ }
+
+ private fun onDateSetChanged() {
+ mFlingRunnable.stop(false)
+ removeAllViews()
+ addMenuItems()
+ mStartAngle = mSelectedAngle
+ selectedPosition = INVALID_POSITION
+ mTempSelectedPosition = INVALID_POSITION
+ mSelectedView = null
+ mTempSelectedView = null
+ mIsDraging = false
+ mIsFling = false
+ requestLayout()
+ }
+
+ /**
+ * add menu item to this layout
+ */
+ private fun addMenuItems() {
+ require(!(mWheelAdapter == null || mWheelAdapter!!.getCount() == 0)) { "Empty menu source!" }
+ mMenuItemCount = mWheelAdapter!!.getCount()
+ var view: View
+ for (i in 0 until mMenuItemCount) {
+ view = mWheelAdapter!!.getView(this, i)
+ //it will be ignore the origin onClickListener
+ view.setOnClickListener(InnerClickListener(i))
+ addView(view)
+ }
+ }
+
+ /**
+ * @param mPadding
+ */
+ fun setPadding(mPadding: Float) {
+ this.mPadding = mPadding
+ invalidate()
+ }
+
+ /**
+ * @return
+ */
+ private val defaultWidth: Int
+ get() {
+ val wm = context.getSystemService(
+ Context.WINDOW_SERVICE
+ ) as WindowManager
+ val outMetrics = DisplayMetrics()
+ wm.defaultDisplay.getMetrics(outMetrics)
+ return outMetrics.widthPixels.coerceAtMost(outMetrics.heightPixels)
+ }
+
+ private fun endFling(scrollIntoSlots: Boolean) {
+ mIsDraging = false
+ mIsFling = false
+ if (scrollIntoSlots) {
+ scrollIntoSlots()
+ }
+ }
+ /**
+ * Scrolls the items so that the selected item is in its 'slot' (its center
+ * is the Wheel's center).
+ *
+ * @param showAnimation Weather show fling animation or not
+ */
+ /**
+ * Scrolls the items so that the selected item is in its 'slot' (its center
+ * is the Wheel's center).
+ */
+ private fun scrollIntoSlots(showAnimation: Boolean = true) {
+ if (mIsDraging || mIsFling) {
+ return
+ }
+ if (childCount == 0 || mTempSelectedView == null) {
+ return
+ }
+ if (!mNeedSlotIntoCenter) {
+ if (mSelectedView !== mTempSelectedView || selectedPosition != mTempSelectedPosition) {
+ onInnerItemUnselected(mSelectedView)
+ mSelectedView = mTempSelectedView
+ onInnerItemSelected(mSelectedView)
+ selectedPosition = mTempSelectedPosition
+ mTempSelectedView = null
+ mTempSelectedPosition = INVALID_POSITION
+ selectionChangeCallback()
+ }
+ } else {
+ val angle: Double = try {
+ mTempSelectedView!!.getTag(R.id.id_wheel_view_angle) as Double
+ } catch (e: NullPointerException) {
+ return
+ }
+ if (angle > 360) {
+ Log.w(
+ TAG,
+ "scrollIntoSlots:$angle > 360, may be something wrong with calculate angle onLayout"
+ )
+ }
+ var diff = Math.abs(mSelectedAngle - angle)
+ diff = if (diff >= 180) 360 - diff else diff
+ val diagonal = (angle + 180) % 360
+ val clockWise: Boolean
+ clockWise = if (diagonal < angle) {
+ mSelectedAngle <= diagonal || mSelectedAngle >= angle
+ } else {
+ mSelectedAngle in angle..diagonal
+ }
+ val sweepAngle = diff * if (clockWise) 1 else -1
+ if (showAnimation) {
+ mFlingRunnable.stop(false)
+ mFlingRunnable.startUsingAngle(sweepAngle)
+ } else {
+ mStartAngle += sweepAngle
+ requestLayout()
+ }
+ }
+ }
+
+ /**
+ * do some callback when selected position change
+ */
+ private fun selectionChangeCallback() {
+ if (mOnMenuSelectedListener != null) {
+ mOnMenuSelectedListener!!.onItemSelected(this, mSelectedView, selectedPosition)
+ }
+ }
+
+ /**
+ * Responsible for fling behavior.
+ *
+ * @author chensuilun
+ */
+ private inner class FlingRunnable : Runnable {
+ /**
+ * 滑动速度
+ */
+ private var mAngelPerSecond = 0f
+
+ /**
+ * 是否以旋转某个角度为目的
+ */
+ private var mStartUsingAngle = false
+
+ /**
+ * 最终的角度
+ */
+ private var mEndAngle = 0.0
+
+ /**
+ * 需要旋转的角度
+ */
+ private var mSweepAngle = 0.0
+
+ /**
+ *
+ */
+ private var mBiggerBefore = false
+
+ /**
+ * 记录下开始转动的时候的startAngle,因为[CursorWheelLayout.mStartAngle]属于[0,360),如果直接用来和[FlingRunnable.mEndAngle]比较大小就会比较麻烦了
+ */
+ private var mInitStarAngle = 0.0
+ private fun startCommon() {
+ // Remove any pending flings
+ removeCallbacks(this)
+ }
+
+ fun stop(scrollIntoSlots: Boolean) {
+ removeCallbacks(this)
+ endFling(scrollIntoSlots)
+ }
+
+ /**
+ * @param velocity
+ */
+ fun startUsingVelocity(velocity: Float) {
+ mStartUsingAngle = false
+ startCommon()
+ mAngelPerSecond = velocity
+ post(this)
+ }
+
+ /**
+ * @param angle
+ */
+ fun startUsingAngle(angle: Double) {
+ mStartUsingAngle = true
+ mSweepAngle = angle
+ mInitStarAngle = mStartAngle
+ mEndAngle = mSweepAngle + mInitStarAngle
+ mBiggerBefore = mInitStarAngle >= mEndAngle
+ // if (BuildConfig.DEBUG) {
+// Log.d(TAG, "startUsingAngle() called with: " + "angle = [" + angle + "]" + ",mStartAngle:" + mInitStarAngle + ",mEndAngle:" + mEndAngle);
+// }
+ post(this)
+ }
+
+ override fun run() {
+ if (mMenuItemCount == 0) {
+ stop(true)
+ return
+ }
+ if (!mStartUsingAngle) {
+ if (abs(mAngelPerSecond) < 20) {
+ stop(true)
+ return
+ }
+ mIsFling = true
+ mStartAngle += mAngelPerSecond / 30
+ mAngelPerSecond /= 1.0666f
+ } else {
+ mStartAngle %= 360.0
+ if (abs((mEndAngle - mInitStarAngle).toInt()) == 0 || mBiggerBefore && mInitStarAngle < mEndAngle || !mBiggerBefore && mInitStarAngle > mEndAngle) {
+ mNeedSlotIntoCenter = false
+ stop(true)
+ return
+ }
+ mIsFling = true
+ val change = mSweepAngle / 5
+ mInitStarAngle += change
+ mStartAngle += change
+ }
+ postDelayed(this, DEFAULT_REFRESH_TIME.toLong())
+ requestLayout()
+ }
+
+ private val DEFAULT_REFRESH_TIME = 16
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view is clicked.
+ *
+ * @author chensuilun
+ */
+ interface OnMenuItemClickListener {
+ fun onItemClick(view: View?, pos: Int)
+ }
+
+ /**
+ * @author chensuilun
+ */
+ private inner class InnerClickListener(private val mPosition: Int) : OnClickListener {
+ override fun onClick(v: View) {
+ if (mSelectedView === v || mTempSelectedView === v) {
+ return
+ }
+ mFlingRunnable.stop(false)
+ mIsDraging = false
+ mIsFling = false
+ mTempSelectedPosition = mPosition
+ mTempSelectedView = v
+ mNeedSlotIntoCenter = true
+ scrollIntoSlots()
+ if (mOnMenuItemClickListener != null) {
+ mOnMenuItemClickListener!!.onItemClick(v, mPosition)
+ }
+ }
+ }
+
+ /**
+ * @param position
+ */
+ fun setSelection(position: Int) {
+ setSelection(position, true)
+ }
+
+ /**
+ * @param position
+ */
+ fun setSelection(position: Int, animation: Boolean) {
+ require(position <= mMenuItemCount) { "Position:$position is out of index!" }
+ post {
+ val itemPosition = if (centerItem == null) position else position + 1
+ mFlingRunnable.stop(false)
+ mIsDraging = false
+ mIsFling = false
+ mTempSelectedPosition = itemPosition
+ mTempSelectedView = getChildAt(itemPosition)
+ mNeedSlotIntoCenter = true
+ scrollIntoSlots(animation)
+ }
+ }
+
+ fun setSelectedAngle(selectedAngle: Double) {
+ var selectedAngle = selectedAngle
+ if (selectedAngle < 0) {
+ return
+ }
+ if (selectedAngle > 360) {
+ selectedAngle %= 360.0
+ }
+ mSelectedAngle = selectedAngle
+ requestLayout()
+ }
+
+ override fun onDetachedFromWindow() {
+ //removeAllViews()
+ super.onDetachedFromWindow()
+ mFlingRunnable.stop(false)
+ mIsFirstLayout = false
+ }
+
+ /**
+ * to do whatever you want to perform the selected view
+ *
+ * @param v
+ */
+ protected open fun onInnerItemSelected(v: View?) {}
+
+ /**
+ * to do whatever you want to perform the unselected view
+ *
+ * @param v
+ */
+ protected open fun onInnerItemUnselected(v: View?) {}
+
+ /**
+ * callback when item selected
+ *
+ * @author chensuilun
+ */
+ interface OnMenuSelectedListener {
+ fun onItemSelected(parent: CursorWheelLayout, view: View?, pos: Int)
+ }
+
+ private var mWheelDataSetObserver: WheelDataSetObserver? = null
+
+ /**
+ * @author chensuilun
+ */
+ inner class WheelDataSetObserver : DataSetObserver() {
+ override fun onChanged() {
+ super.onChanged()
+ onDateSetChanged()
+ }
+ }
+
+ /**
+ * An Adapter object acts as a bridge between an [CursorWheelLayout] and the
+ * underlying data for that view. The Adapter provides access to the data items.
+ * The Adapter is also responsible for making a [View] for
+ * each item in the data set.
+ *
+ * @author chensuilun
+ */
+ abstract class CycleWheelAdapter {
+ private val mDataSetObservable = DataSetObservable()
+ fun registerDataSetObserver(observer: DataSetObserver) {
+ mDataSetObservable.registerObserver(observer)
+ }
+
+ fun unregisterDataSetObserver(observer: DataSetObserver) {
+ mDataSetObservable.unregisterObserver(observer)
+ }
+
+ /**
+ * Notifies the attached observers that the underlying data has been changed
+ * and any View reflecting the data set should refresh itself.
+ */
+ fun notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged()
+ }
+
+ /**
+ * How many menu items are in the data set represented by this Adapter.
+ *
+ * @return Count of items.
+ */
+ abstract fun getCount(): Int
+
+ /**
+ * Get a View that displays the data at the specified position in the data set.
+ *
+ * @param parent
+ * @param position
+ * @return
+ */
+ abstract fun getView(parent: View?, position: Int): View
+
+ /**
+ * Get the data item associated with the specified position in the data set.
+ *
+ * @param position Position of the item whose data we want within the adapter's
+ * data set.
+ * @return The data at the specified position.
+ */
+ abstract fun getItem(position: Int): Any?
+ }
+
+ companion object {
+ private const val TAG = "CircleMenuLayout"
+
+ /**
+ * size of menu item relative to parent
+ */
+ private const val RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f
+
+ /**
+ * size of center item relative to parent
+ */
+ private const val RADIO_DEFAULT_CENTER_DIMENSION = 1 / 3f
+
+ /**
+ * ignore the origin padding ,real padding size determine by parent size
+ */
+ private const val RADIO_PADDING_LAYOUT = 1 / 12f
+ private const val INVALID_POSITION = -1
+ private const val DEFAULT_SELECTED_ANGLE = 0f
+
+ /**
+ * Angle a touch can wander before we think the user is flinging
+ */
+ private const val FLINGABLE_VALUE = 300
+
+ /**
+ *
+ */
+ private const val NOCLICK_VALUE = 3
+
+ /**
+ * default cursor color
+ */
+ @ColorInt
+ private val DEFAULT_CURSOR_COLOR = -0x3ad6
+
+ /**
+ * default wheel background color
+ */
+ @ColorInt
+ private val DEFAULT_WHEEL_BG_COLOR = -0x1aece8e4
+
+ @ColorInt
+ val DEFAULT_GUIDE_LINE_COLOR = -0x8d8d8e
+
+ //DP
+ const val DEFAULT_TRIANGLE_HEIGHT = 13
+
+ //DP
+ const val DEFAULT_GUIDE_LINE_WIDTH = 0
+
+ /**
+ * Don't rotate my item.DEFAULT
+ */
+ const val ITEM_ROTATE_MODE_NONE = 0
+ const val ITEM_ROTATE_MODE_INWARD = 1
+ const val ITEM_ROTATE_MODE_OUTWARD = 2
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/res/values-wheel/attrs-wheel.xml b/library/src/main/res/values-wheel/attrs-wheel.xml
index 526da42..2c48d37 100644
--- a/library/src/main/res/values-wheel/attrs-wheel.xml
+++ b/library/src/main/res/values-wheel/attrs-wheel.xml
@@ -1,20 +1,20 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
\ No newline at end of file