├── .gitattributes ├── .gitignore ├── BottomNavigationView ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── bottomnavigationview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── bottomnavigationview │ │ │ └── widget │ │ │ └── BottomNavigationView.kt │ └── res │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ └── style.xml │ └── test │ └── java │ └── com │ └── example │ └── bottomnavigationview │ └── ExampleUnitTest.kt ├── CircleSeekBar ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── circleseekbar │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── circleseekbar │ │ │ └── widget │ │ │ └── CircleSeekBar.kt │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── values.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── circleseekbar │ └── ExampleUnitTest.kt ├── CleverSeekBar ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── cleverseekbar │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── cleverseekbar │ │ │ └── widget │ │ │ ├── CleverSeekBar.kt │ │ │ └── CleverSeekBars.java │ └── res │ │ └── values │ │ └── attrs.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── cleverseekbar │ └── ExampleUnitTest.kt ├── Core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── core │ │ │ ├── LiveData.kt │ │ │ ├── animation │ │ │ └── Views.kt │ │ │ ├── common │ │ │ ├── Contexts.kt │ │ │ └── ViewBindings.kt │ │ │ └── ui │ │ │ ├── adapter │ │ │ ├── FastRecyclerViewAdapter.kt │ │ │ ├── FastViewBindingRecyclerViewAdapter.kt │ │ │ ├── ViewAdapters.java │ │ │ └── ViewBindingAdapters.java │ │ │ └── base │ │ │ └── BaseRecyclerViewAdapter.kt │ └── res │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ └── values.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── core │ └── ExampleUnitTest.kt ├── GroundGlassView ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── groundglass │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── groundglass │ │ │ ├── dao │ │ │ ├── AndroidStockBlurImpl.kt │ │ │ ├── AndroidXBlurImpl.kt │ │ │ ├── BlurImpl.kt │ │ │ ├── EmptyBlurImpl.kt │ │ │ └── SupportLibraryBlurImpl.kt │ │ │ └── widget │ │ │ └── GroundGlassView.kt │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── values.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── groundglass │ └── ExampleUnitTest.kt ├── LICENSE ├── OverScrollView ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── overscroll │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── overscroll │ │ └── OverScrollView.kt │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── overscroll │ └── ExampleUnitTest.kt ├── README.md ├── TopBar ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── topbar │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ └── topbar │ │ │ └── widget │ │ │ └── TopBar.kt │ └── res │ │ ├── layout │ │ └── top_bar.xml │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ └── values.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── topbar │ └── ExampleUnitTest.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── lumyuan │ │ └── ux │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── lumyuan │ │ │ └── ux │ │ │ ├── KTBasicActivity.kt │ │ │ ├── MainActivity.java │ │ │ └── ui │ │ │ ├── PagerAdapterForFragment.kt │ │ │ ├── XViewPager.java │ │ │ └── fragments │ │ │ └── BlankFragment.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_home.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_mine.xml │ │ └── ic_module.xml │ │ ├── layout │ │ ├── activity_kt_basic.xml │ │ ├── activity_main.xml │ │ ├── fragment_blank.xml │ │ └── item_basic.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── io │ └── github │ └── lumyuan │ └── ux │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenMatch.properties ├── screenMatch_example_dimens.xml └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | 22 | # Keystore files 23 | *.jks 24 | *.keystore 25 | 26 | # Google Services (e.g. APIs or Firebase) 27 | google-services.json 28 | 29 | # Android Profiling 30 | *.hprof 31 | -------------------------------------------------------------------------------- /BottomNavigationView/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /BottomNavigationView/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.bottomnavigationview' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | targetSdk 33 13 | 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | kotlinOptions { 30 | jvmTarget = '11' 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation 'androidx.core:core-ktx:1.9.0' 37 | implementation 'androidx.appcompat:appcompat:1.6.0' 38 | implementation 'com.google.android.material:material:1.7.0' 39 | testImplementation 'junit:junit:4.13.2' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 42 | 43 | implementation project(':Core') 44 | 45 | } 46 | 47 | task makeJar(type:Copy){ 48 | delete 'build/libs/demo.jar' 49 | from('build/intermediates/packaged-classes/debug/') 50 | into('build/libs/') 51 | include('classes.jar') 52 | rename('classes.jar','demo.jar') 53 | } 54 | makeJar.dependsOn(build) -------------------------------------------------------------------------------- /BottomNavigationView/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/BottomNavigationView/consumer-rules.pro -------------------------------------------------------------------------------- /BottomNavigationView/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /BottomNavigationView/src/androidTest/java/com/example/bottomnavigationview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.bottomnavigationview 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.bottomnavigationview.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /BottomNavigationView/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /BottomNavigationView/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFFFF 4 | #FF0099FF 5 | #FF1C1B1F 6 | -------------------------------------------------------------------------------- /BottomNavigationView/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BottomNavigationView/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FF0099FF 5 | #FFFFFFFF 6 | -------------------------------------------------------------------------------- /BottomNavigationView/src/main/res/values/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /BottomNavigationView/src/test/java/com/example/bottomnavigationview/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.bottomnavigationview 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /CircleSeekBar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /CircleSeekBar/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.circleseekbar' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_11 26 | targetCompatibility JavaVersion.VERSION_11 27 | } 28 | kotlinOptions { 29 | jvmTarget = '11' 30 | } 31 | } 32 | 33 | dependencies { 34 | 35 | implementation 'androidx.core:core-ktx:1.9.0' 36 | implementation 'androidx.appcompat:appcompat:1.6.0' 37 | implementation 'com.google.android.material:material:1.7.0' 38 | testImplementation 'junit:junit:4.13.2' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 41 | 42 | implementation project(":Core") 43 | } -------------------------------------------------------------------------------- /CircleSeekBar/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/CircleSeekBar/consumer-rules.pro -------------------------------------------------------------------------------- /CircleSeekBar/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /CircleSeekBar/src/androidTest/java/io/github/lumyuan/ux/circleseekbar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.circleseekbar 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.circleseekbar.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /CircleSeekBar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CircleSeekBar/src/main/java/io/github/lumyuan/ux/circleseekbar/widget/CircleSeekBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.circleseekbar.widget 2 | 3 | import android.animation.ValueAnimator 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.graphics.Canvas 7 | import android.graphics.Paint 8 | import android.graphics.Paint.Cap 9 | import android.graphics.Rect 10 | import android.graphics.RectF 11 | import android.graphics.Shader 12 | import android.util.AttributeSet 13 | import android.view.View 14 | import android.view.animation.AccelerateDecelerateInterpolator 15 | import android.view.animation.DecelerateInterpolator 16 | import android.view.animation.OvershootInterpolator 17 | import io.github.lumyuan.ux.circleseekbar.R 18 | import io.github.lumyuan.ux.core.common.dip2px 19 | import java.text.DecimalFormat 20 | import kotlin.math.abs 21 | 22 | class CircleSeekBar: View { 23 | 24 | /* 最小宽度,单位为dp */ 25 | private val MIN_WIDTH = 50f 26 | 27 | /* 最小高度,单位为dp */ 28 | private val MIN_HEIGHT = 50f 29 | 30 | /* 默认模式 */ 31 | var MODE_DEFAULT = 0 32 | 33 | /* 笔画模式 */ 34 | var MODE_STROKE = 0 35 | 36 | /* 填充模式 */ 37 | var MODE_FILL = 1 38 | 39 | /* 笔画&填充模式 */ 40 | var MODE_FILL_AND_STROKE = 2 41 | 42 | /* 进度格式化默认值 */ 43 | private val PROGRESS_FORMAT_DEFAULT = "#.00" 44 | 45 | /* 进度默认最大值 */ 46 | private val MAX_PROGRESS_DEFAULT = 100f 47 | 48 | /* 开始位置角度默认值 */ 49 | private val START_ANGLE_DEFAULT = 270f 50 | 51 | /* 刷新滑动速度默认值 */ 52 | private val VELOCITY_DEFAULT = 3.0f 53 | 54 | /* 文字大小默认值,单位为sp */ 55 | private val TEXT_SIZE_DEFAULT = 10.0f 56 | 57 | /* 默认文字颜色 */ 58 | private val TEXT_COLOR_DEFAULT = -0x40adae 59 | 60 | /* 进度条边框宽度默认值,单位为dp */ 61 | private val PROGRESS_WIDTH_DEFAULT = 5.0f 62 | 63 | /* 默认进度颜色 */ 64 | private val PROGRESS_COLOR_DEFAULT = -0xc27a3a 65 | 66 | /* 进度条底色默认值,单位为dp */ 67 | private val S_PROGRESS_WIDTH_DEFAULT = 2.0f 68 | 69 | /* 默认进度颜色 */ 70 | private val S_PROGRESS_COLOR_DEFAULT = -0x222223 71 | 72 | private var mPaint: Paint? = null 73 | private var mTextPaint: Paint? = null 74 | private var mProgressPaint: Paint? = null 75 | private var mSProgressPaint: Paint? = null 76 | 77 | private var mMode = 0 // 进度模式 78 | 79 | private var mMaxProgress = 0f // 最大进度 80 | 81 | private var mShowText = false // 是否显示文字 82 | 83 | private var mStartAngle = 0f // 起始角度 84 | 85 | private var mVelocity = 0f // 速度 86 | 87 | private var mTextSize = 0f // 字体大小 88 | 89 | private var mTextColor = 0 // 字体颜色 90 | 91 | private var mProgressStrokeWidth = 0f // 进度条宽度 92 | 93 | private var mProgressColor = 0 // 进度颜色 94 | 95 | private var mSProgressStrokeWidth = 0f // 二级进度宽度 96 | 97 | private var mSProgressColor = 0 // 二级进度颜色 98 | 99 | private var mFadeEnable = false // 是否开启淡入淡出效果 100 | 101 | private var mStartAlpha = 0 // 开始透明度,0~255 102 | 103 | private var mEndAlpha = 0 // 结束透明度,0~255 104 | 105 | private var mZoomEnable = false // 二级进度缩放 106 | 107 | private var mCapRound = false // 进度条首尾是否圆角 108 | 109 | 110 | private var mProgressRect: RectF? = null 111 | private var mSProgressRect: RectF? = null 112 | private var mTextBounds: Rect? = null 113 | 114 | private var mCurrentAngle = 0f // 当前角度 115 | 116 | private var mUseCenter = false // 是否从中心绘制 117 | 118 | private var mFormat: DecimalFormat? = null // 格式化数值 119 | 120 | 121 | constructor(context: Context) : this(context, null) 122 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 123 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 124 | context, 125 | attrs, 126 | defStyleAttr 127 | ) { 128 | init(attrs) 129 | } 130 | 131 | private fun init(attrs: AttributeSet?) { 132 | if (attrs != null) { 133 | val type = context.obtainStyledAttributes( 134 | attrs, 135 | R.styleable.CircleSeekBar 136 | ) 137 | mMode = type.getInt(R.styleable.CircleSeekBar_mode, MODE_DEFAULT) 138 | mMaxProgress = type.getFloat( 139 | R.styleable.CircleSeekBar_maxProgress, 140 | MAX_PROGRESS_DEFAULT 141 | ) 142 | mShowText = type.getBoolean( 143 | R.styleable.CircleSeekBar_showText, 144 | true 145 | ) 146 | mStartAngle = type.getFloat( 147 | R.styleable.CircleSeekBar_startAngle, 148 | START_ANGLE_DEFAULT 149 | ) 150 | mVelocity = type.getFloat( 151 | R.styleable.CircleSeekBar_velocity, 152 | VELOCITY_DEFAULT 153 | ) 154 | mTextSize = type.getDimension( 155 | R.styleable.CircleSeekBar_textSize, 156 | context.dip2px(TEXT_SIZE_DEFAULT).toFloat() 157 | ) 158 | mTextColor = type.getColor( 159 | R.styleable.CircleSeekBar_textColor, 160 | TEXT_COLOR_DEFAULT 161 | ) 162 | mProgressStrokeWidth = type.getDimension( 163 | R.styleable.CircleSeekBar_progressWidth, 164 | context.dip2px(PROGRESS_WIDTH_DEFAULT).toFloat() 165 | ) 166 | mProgressColor = type.getColor( 167 | R.styleable.CircleSeekBar_progressColor, 168 | PROGRESS_COLOR_DEFAULT 169 | ) 170 | mSProgressStrokeWidth = type.getDimension( 171 | R.styleable.CircleSeekBar_sProgressWidth, 172 | context.dip2px(S_PROGRESS_WIDTH_DEFAULT).toFloat() 173 | ) 174 | mSProgressColor = type.getColor( 175 | R.styleable.CircleSeekBar_sProgressColor, 176 | S_PROGRESS_COLOR_DEFAULT 177 | ) 178 | mFadeEnable = type.getBoolean( 179 | R.styleable.CircleSeekBar_fadeEnable, 180 | false 181 | ) 182 | mStartAlpha = type 183 | .getInt(R.styleable.CircleSeekBar_startAlpha, 255) 184 | mEndAlpha = type.getInt(R.styleable.CircleSeekBar_endAlpha, 255) 185 | mZoomEnable = type.getBoolean( 186 | R.styleable.CircleSeekBar_zoomEnable, 187 | false 188 | ) 189 | mCapRound = type.getBoolean( 190 | R.styleable.CircleSeekBar_capRound, 191 | true 192 | ) 193 | var progress = type.getFloat( 194 | R.styleable.CircleSeekBar_progress, 195 | 0f 196 | ) 197 | progress = if (progress > mMaxProgress || progress < 0f) 0f else progress 198 | mCurrentAngle = progress / mMaxProgress * 360f 199 | type.recycle() 200 | } else { 201 | mMode = MODE_DEFAULT 202 | mMaxProgress = MAX_PROGRESS_DEFAULT 203 | mStartAngle = START_ANGLE_DEFAULT 204 | mVelocity = VELOCITY_DEFAULT 205 | mTextSize = TEXT_SIZE_DEFAULT 206 | mTextColor = TEXT_COLOR_DEFAULT 207 | mProgressStrokeWidth = PROGRESS_WIDTH_DEFAULT 208 | mProgressColor = PROGRESS_COLOR_DEFAULT 209 | mSProgressStrokeWidth = S_PROGRESS_WIDTH_DEFAULT 210 | mSProgressColor = S_PROGRESS_COLOR_DEFAULT 211 | mCurrentAngle = 0f 212 | mStartAlpha = 255 213 | mEndAlpha = 255 214 | mZoomEnable = false 215 | mCapRound = true 216 | } 217 | mPaint = Paint() 218 | mPaint?.isAntiAlias = true 219 | mTextPaint = Paint(mPaint) 220 | mTextPaint?.color = mTextColor 221 | mTextPaint?.textSize = mTextSize 222 | mProgressPaint = Paint(mPaint) 223 | mProgressPaint?.color = mProgressColor 224 | mProgressPaint?.strokeWidth = mProgressStrokeWidth 225 | mSProgressPaint = Paint(mProgressPaint) 226 | mSProgressPaint?.color = mSProgressColor 227 | mSProgressPaint?.strokeWidth = mSProgressStrokeWidth 228 | if (mCapRound) { 229 | mProgressPaint?.strokeCap = Cap.ROUND 230 | } 231 | mUseCenter = when(mMode){ 232 | MODE_FILL_AND_STROKE -> { 233 | mProgressPaint?.style = Paint.Style.FILL 234 | mSProgressPaint?.style = Paint.Style.FILL_AND_STROKE 235 | true 236 | } 237 | MODE_FILL -> { 238 | mProgressPaint?.style = Paint.Style.FILL 239 | mSProgressPaint?.style = Paint.Style.FILL 240 | true 241 | } 242 | else -> { 243 | mProgressPaint?.style = Paint.Style.STROKE 244 | mSProgressPaint?.style = Paint.Style.STROKE 245 | false 246 | } 247 | } 248 | mProgressRect = RectF() 249 | mTextBounds = Rect() 250 | mFormat = DecimalFormat(PROGRESS_FORMAT_DEFAULT) 251 | } 252 | 253 | @SuppressLint("DrawAllocation") 254 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 255 | /* 计算控件宽度与高度 */ 256 | val widthMode = MeasureSpec.getMode(widthMeasureSpec) 257 | val widthSize = MeasureSpec.getSize(widthMeasureSpec) 258 | val heightMode = MeasureSpec.getMode(heightMeasureSpec) 259 | val heightSize = MeasureSpec.getSize(heightMeasureSpec) 260 | val width: Int = if (widthMode == MeasureSpec.EXACTLY) { 261 | widthSize 262 | } else { 263 | (paddingLeft 264 | + context.dip2px(MIN_WIDTH) + paddingRight) 265 | } 266 | val height: Int = if (heightMode == MeasureSpec.EXACTLY) { 267 | heightSize 268 | } else { 269 | (paddingTop 270 | + context.dip2px(MIN_HEIGHT) + paddingBottom) 271 | } 272 | setMeasuredDimension(width, height) 273 | /* 计算进度显示的矩形框 */ 274 | var radius = (if (width > height) height shr 1 else width shr 1).toFloat() 275 | val maxStrokeWidth = mProgressStrokeWidth.coerceAtLeast(mSProgressStrokeWidth) 276 | radius = radius - getMaxPadding() - maxStrokeWidth 277 | val centerX = width shr 1 278 | val centerY = height shr 1 279 | mProgressRect!![centerX - radius, centerY - radius, centerX + radius] = centerY + radius 280 | mSProgressRect = RectF(mProgressRect) 281 | } 282 | 283 | override fun onDraw(canvas: Canvas) { 284 | 285 | val ratio = mCurrentAngle / 360f 286 | // 设置透明度 287 | if (mFadeEnable) { 288 | val alpha = ((mEndAlpha - mStartAlpha) * ratio).toInt() 289 | mProgressPaint!!.alpha = alpha 290 | } 291 | // 设置二级进度缩放效果 292 | if (mZoomEnable) { 293 | zoomSProgressRect(ratio) 294 | } 295 | // 绘制二级进度条 296 | canvas.drawArc(mSProgressRect!!, 0f, 360f, false, mSProgressPaint!!) 297 | // 绘制进度条 298 | canvas.drawArc( 299 | mProgressRect!!, mStartAngle, mCurrentAngle, mUseCenter, 300 | mProgressPaint!! 301 | ) 302 | // 绘制字体 303 | if (mShowText) { 304 | val text = formatProgress(ratio * mMaxProgress) 305 | mTextPaint!!.getTextBounds(text, 0, text.length, mTextBounds) 306 | canvas.drawText( 307 | text, (width - mTextBounds!!.width() shr 1).toFloat(), 308 | ( 309 | (height shr 1) + (mTextBounds!!.height() shr 1)).toFloat(), 310 | mTextPaint!! 311 | ) 312 | } 313 | } 314 | 315 | /** 316 | * 格式化进度 317 | * 318 | * @param progress 319 | * @return 320 | */ 321 | private fun formatProgress(progress: Float): String { 322 | return mFormat!!.format(progress.toDouble()) + "%" 323 | } 324 | 325 | /** 326 | * 获取内边距最大值 327 | * 328 | * @return 329 | */ 330 | private fun getMaxPadding(): Int { 331 | var maxPadding = paddingLeft 332 | val paddingRight = paddingRight 333 | val paddingTop = paddingTop 334 | val paddingBottom = paddingBottom 335 | if (maxPadding < paddingRight) { 336 | maxPadding = paddingRight 337 | } 338 | if (maxPadding < paddingTop) { 339 | maxPadding = paddingTop 340 | } 341 | if (maxPadding < paddingBottom) { 342 | maxPadding = paddingBottom 343 | } 344 | return maxPadding 345 | } 346 | 347 | /** 348 | * 缩放二级进度条 349 | * 350 | * @param ratio 351 | */ 352 | private fun zoomSProgressRect(ratio: Float) { 353 | val width = mProgressRect!!.width() 354 | val height = mProgressRect!!.height() 355 | val centerX = mProgressRect!!.centerX() 356 | val centerY = mProgressRect!!.centerY() 357 | val offsetX = width * 0.5f * ratio 358 | val offsetY = height * 0.5f * ratio 359 | val left = centerX - offsetX 360 | val right = centerX + offsetX 361 | val top = centerY - offsetY 362 | val bottom = centerY + offsetY 363 | mSProgressRect!![left, top, right] = bottom 364 | } 365 | 366 | override fun onDisplayHint(hint: Int) { 367 | if (hint == VISIBLE) { 368 | mCurrentAngle = 0f 369 | invalidate() 370 | } 371 | super.onDisplayHint(hint) 372 | } 373 | 374 | /** 375 | * 设置目标进度 376 | * 377 | * @param progress 378 | */ 379 | fun setProgress(progress: Float) { 380 | setProgress(progress, true) 381 | } 382 | 383 | /** 384 | * 设置目标进度 385 | * 386 | * @param progress 387 | * 进度值 388 | * @param isAnim 389 | * 是否有动画 390 | */ 391 | fun setProgress(progress: Float, isAnim: Boolean) { 392 | var p = progress 393 | p = if (p > mMaxProgress || p < 0f) 0f else p 394 | ValueAnimator.ofFloat(mCurrentAngle, p / mMaxProgress * 360f).apply { 395 | // duration = if (isAnim){ 396 | // (abs(p - (mCurrentAngle * mMaxProgress / 360f )) * 10).toLong() 397 | // }else { 398 | // 0 399 | // } 400 | duration = 1500 401 | interpolator = AccelerateDecelerateInterpolator() 402 | addUpdateListener { 403 | val v = it.animatedValue as Float 404 | mCurrentAngle = v 405 | postInvalidate() 406 | } 407 | }.start() 408 | } 409 | 410 | fun getProgress() = mCurrentAngle * mMaxProgress / 360f 411 | /** 412 | * 设置进度画笔着色方式 413 | * 414 | * @param shader 415 | */ 416 | fun setProgressShader(shader: Shader?) { 417 | mProgressPaint!!.shader = shader 418 | invalidate() 419 | } 420 | 421 | /** 422 | * 设置二级进度画笔着色方式 423 | * 424 | * @param shader 425 | */ 426 | fun setSProgressShader(shader: Shader?) { 427 | mSProgressPaint!!.shader = shader 428 | invalidate() 429 | } 430 | 431 | fun setMaxProgress(max: Float) { 432 | mMaxProgress = max 433 | } 434 | 435 | fun getMaxProgress(): Float { 436 | return mMaxProgress 437 | } 438 | 439 | fun getMode(): Int { 440 | return mMode 441 | } 442 | 443 | fun setMode(mMode: Int) { 444 | this.mMode = mMode 445 | } 446 | 447 | fun getStartAngle(): Float { 448 | return mStartAngle 449 | } 450 | 451 | fun setStartAngle(mStartAngle: Float) { 452 | this.mStartAngle = mStartAngle 453 | } 454 | 455 | fun getVelocity(): Float { 456 | return mVelocity 457 | } 458 | 459 | fun setVelocity(mVelocity: Float) { 460 | this.mVelocity = mVelocity 461 | } 462 | 463 | fun getTextSize(): Float { 464 | return mTextSize 465 | } 466 | 467 | fun setTextSize(mTextSize: Float) { 468 | this.mTextSize = mTextSize 469 | } 470 | 471 | fun getTextColor(): Int { 472 | return mTextColor 473 | } 474 | 475 | fun setTextColor(mTextColor: Int) { 476 | this.mTextColor = mTextColor 477 | postInvalidate() 478 | } 479 | 480 | fun getProgressStrokeWidth(): Float { 481 | return mProgressStrokeWidth 482 | } 483 | 484 | fun setProgressStrokeWidth(mProgressStrokeWidth: Float) { 485 | this.mProgressStrokeWidth = mProgressStrokeWidth 486 | postInvalidate() 487 | } 488 | 489 | fun getProgressColor(): Int { 490 | return mProgressColor 491 | } 492 | 493 | fun setProgressColor(mProgressColor: Int) { 494 | this.mProgressColor = mProgressColor 495 | postInvalidate() 496 | } 497 | 498 | fun getSProgressStrokeWidth(): Float { 499 | return mSProgressStrokeWidth 500 | } 501 | 502 | fun setSProgressStrokeWidth(mSProgressStrokeWidth: Float) { 503 | this.mSProgressStrokeWidth = mSProgressStrokeWidth 504 | } 505 | 506 | fun getSProgressColor(): Int { 507 | return mSProgressColor 508 | } 509 | 510 | fun setSProgressColor(mSProgressColor: Int) { 511 | this.mSProgressColor = mSProgressColor 512 | } 513 | 514 | fun isFadeEnable(): Boolean { 515 | return mFadeEnable 516 | } 517 | 518 | fun setFadeEnable(mFadeEnable: Boolean) { 519 | this.mFadeEnable = mFadeEnable 520 | } 521 | 522 | fun getStartAlpha(): Int { 523 | return mStartAlpha 524 | } 525 | 526 | fun setStartAlpha(mStartAlpha: Int) { 527 | this.mStartAlpha = mStartAlpha 528 | } 529 | 530 | fun getEndAlpha(): Int { 531 | return mEndAlpha 532 | } 533 | 534 | fun setEndAlpha(mEndAlpha: Int) { 535 | this.mEndAlpha = mEndAlpha 536 | } 537 | 538 | fun isZoomEnable(): Boolean { 539 | return mZoomEnable 540 | } 541 | 542 | fun setZoomEnable(mZoomEnable: Boolean) { 543 | this.mZoomEnable = mZoomEnable 544 | } 545 | } -------------------------------------------------------------------------------- /CircleSeekBar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CircleSeekBar/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CircleSeekBar/src/test/java/io/github/lumyuan/ux/circleseekbar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.circleseekbar 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /CleverSeekBar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /CleverSeekBar/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.cleverseekbar' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_11 25 | targetCompatibility JavaVersion.VERSION_11 26 | } 27 | kotlinOptions { 28 | jvmTarget = '11' 29 | } 30 | } 31 | 32 | dependencies { 33 | 34 | implementation 'androidx.core:core-ktx:1.9.0' 35 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') 36 | implementation 'androidx.appcompat:appcompat:1.6.1' 37 | implementation 'com.google.android.material:material:1.8.0' 38 | testImplementation 'junit:junit:4.13.2' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 41 | 42 | implementation project(":Core") 43 | } -------------------------------------------------------------------------------- /CleverSeekBar/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/CleverSeekBar/consumer-rules.pro -------------------------------------------------------------------------------- /CleverSeekBar/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /CleverSeekBar/src/androidTest/java/io/github/lumyuan/ux/cleverseekbar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.cleverseekbar 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.cleverseekbar.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /CleverSeekBar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CleverSeekBar/src/main/java/io/github/lumyuan/ux/cleverseekbar/widget/CleverSeekBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.cleverseekbar.widget 2 | 3 | import android.animation.ValueAnimator 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.graphics.Canvas 7 | import android.graphics.Color 8 | import android.graphics.Paint 9 | import android.graphics.RectF 10 | import android.os.Build 11 | import android.util.AttributeSet 12 | import android.view.HapticFeedbackConstants 13 | import android.view.MotionEvent 14 | import android.view.View 15 | import android.view.animation.AccelerateDecelerateInterpolator 16 | import android.view.animation.Interpolator 17 | import io.github.lumyuan.ux.cleverseekbar.R 18 | import io.github.lumyuan.ux.cleverseekbar.widget.CleverSeekBars.OnSeekBarChangeListener 19 | import io.github.lumyuan.ux.core.common.dip2px 20 | 21 | @SuppressLint("Recycle", "CustomViewStyleable", "ResourceType") 22 | open class CleverSeekBar : View { 23 | 24 | constructor(context: Context?) : this(context, null) 25 | constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) 26 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( 27 | context, 28 | attrs, 29 | defStyleAttr 30 | ) { 31 | initView(attrs) 32 | } 33 | 34 | var barWidth: Float = 0f 35 | set(value) { 36 | field = value 37 | invalidate() 38 | } 39 | 40 | private var barThumbWidth: Float = 0f 41 | private var barThumbMaxWidth: Float = 0f 42 | private var viewHeight: Float = 0f 43 | private var thumbFillWidth: Float = 0f 44 | 45 | var barColor: Int = Color.TRANSPARENT 46 | set(value) { 47 | field = value 48 | invalidate() 49 | } 50 | var barBackgroundColor: Int = Color.TRANSPARENT 51 | set(value) { 52 | field = value 53 | invalidate() 54 | } 55 | 56 | private var progressRadius: Float = 0f 57 | var barProgressRadius: Float = 0f 58 | set(value) { 59 | field = value 60 | progressRadius = value 61 | invalidate() 62 | } 63 | 64 | private var thumbRadius: Float = 0f 65 | private var barThumbMaxRadius: Float = 0f 66 | var barThumbRadius: Float = 0f 67 | set(value) { 68 | field = value 69 | thumbRadius = value 70 | barThumbMaxRadius = value * 3 71 | invalidate() 72 | } 73 | 74 | var barThumbColor: Int = 0 75 | set(value) { 76 | field = value 77 | invalidate() 78 | } 79 | 80 | var progressMin: Float = 0f 81 | set(value) { 82 | field = value 83 | invalidate() 84 | } 85 | 86 | var progressMax: Float = 0f 87 | set(value) { 88 | field = value 89 | invalidate() 90 | } 91 | 92 | var changeAnimationDuration: Long = 400 93 | var changeAnimationInterpolator: Interpolator = AccelerateDecelerateInterpolator() 94 | var progress: Float = 0f 95 | set(value) { 96 | ValueAnimator.ofFloat(field, value).apply { 97 | duration = changeAnimationDuration 98 | interpolator = changeAnimationInterpolator 99 | addUpdateListener { 100 | val v = it.animatedValue as Float 101 | field = v 102 | seekBarChangeListener?.onChanged(this@CleverSeekBar, v) 103 | invalidate() 104 | } 105 | }.start() 106 | } 107 | 108 | private var seekBarChangeListener: OnSeekBarChangeListener? = null 109 | 110 | fun setOnSeekBarChangeListener(onSeekBarChangeListener: OnSeekBarChangeListener) { 111 | this.seekBarChangeListener = onSeekBarChangeListener 112 | } 113 | 114 | fun getOnSeekBarChangeListener(): OnSeekBarChangeListener? = this.seekBarChangeListener 115 | 116 | private fun initView(attrs: AttributeSet?) { 117 | val typedArray = 118 | this.context.obtainStyledAttributes(attrs, R.styleable.material_clever_seekbar) 119 | barWidth = typedArray.getDimension( 120 | R.styleable.material_clever_seekbar_barWidth, context.dip2px( 121 | context.resources.getDimension( 122 | io.github.lumyuan.ux.core.R.dimen.extra_small 123 | ) 124 | ).toFloat() 125 | ) 126 | 127 | barThumbWidth = barWidth * 3f 128 | barThumbMaxWidth = barThumbWidth * 1.5f 129 | thumbFillWidth = barWidth 130 | 131 | viewHeight = barThumbMaxWidth + elevationTarget * 2 132 | 133 | barColor = typedArray.getColor( 134 | R.styleable.material_clever_seekbar_barColor, 135 | Color.parseColor(context.getString(io.github.lumyuan.ux.core.R.color.seed)) 136 | ) 137 | 138 | barBackgroundColor = typedArray.getColor( 139 | R.styleable.material_clever_seekbar_barBackgroundColor, 140 | Color.parseColor(context.getString(io.github.lumyuan.ux.core.R.color.progressBackgroundColor)) 141 | ) 142 | 143 | val radius = barWidth * .5f 144 | val backgroundRadius = typedArray.getDimension( 145 | R.styleable.material_clever_seekbar_barProgressRadius, 146 | radius 147 | ) 148 | 149 | barProgressRadius = if (backgroundRadius > radius) radius else backgroundRadius 150 | 151 | val thumbRadius = typedArray.getDimension( 152 | R.styleable.material_clever_seekbar_barThumbRadius, 153 | radius * 3 154 | ) 155 | 156 | barThumbRadius = if (thumbRadius > radius * 3) radius * 3 else thumbRadius 157 | 158 | barThumbColor = typedArray.getColor( 159 | R.styleable.material_clever_seekbar_barThumbColor, Color.parseColor( 160 | context.getString( 161 | io.github.lumyuan.ux.core.R.color.white 162 | ) 163 | ) 164 | ) 165 | 166 | progressMin = typedArray.getFloat(R.styleable.material_clever_seekbar_minProgress, 0f) 167 | progressMax = typedArray.getFloat(R.styleable.material_clever_seekbar_maxProgress, 100f) 168 | 169 | changeAnimationDuration = 170 | typedArray.getInteger(R.styleable.material_clever_seekbar_duration, 500).toLong() 171 | 172 | val p = typedArray.getFloat(R.styleable.material_clever_seekbar_progress, progressMin) 173 | if (p > progressMax || p < progressMin) { 174 | throw IllegalStateException("进度超过范围。(应在最小进度与最大进度之间)") 175 | } 176 | progress = p 177 | } 178 | 179 | //背景画笔 180 | private val bgPaint by lazy { 181 | Paint().apply { 182 | style = Paint.Style.FILL_AND_STROKE 183 | } 184 | } 185 | 186 | //进度画笔 187 | private val progressPaint by lazy { 188 | Paint().apply { 189 | style = Paint.Style.FILL_AND_STROKE 190 | } 191 | } 192 | 193 | //thumb边框画笔 194 | private val thumbStrokePaint by lazy { 195 | Paint().apply { 196 | style = Paint.Style.FILL_AND_STROKE 197 | } 198 | } 199 | 200 | //thumb画笔 201 | private val thumbFillPaint by lazy { 202 | Paint().apply { 203 | style = Paint.Style.FILL_AND_STROKE 204 | } 205 | } 206 | 207 | //矩阵 208 | private val rectF by lazy { 209 | RectF() 210 | } 211 | 212 | private var elevation = 0f 213 | private val elevationTarget = 8f 214 | 215 | /** 216 | * 获取真实的进度百分比 217 | */ 218 | private fun getRealProgressPercentage(): Float = 219 | (progress - progressMin) / (progressMax - progressMin) 220 | 221 | private fun getRealProgress(percentage: Float): Float = 222 | percentage * (progressMax - progressMin) + progressMin 223 | 224 | private fun initState(canvas: Canvas) { 225 | bgPaint.color = barBackgroundColor 226 | progressPaint.color = barColor 227 | thumbStrokePaint.color = barColor 228 | thumbFillPaint.color = barThumbColor 229 | 230 | val bgPadding = viewHeight * .5f 231 | val barRadius = barWidth * .5f 232 | 233 | val thumbFillRadius = thumbFillWidth * .5f 234 | 235 | val thumbWidthRadius = barThumbWidth * .5f 236 | 237 | val progressWidth = (width - viewHeight) * getRealProgressPercentage() + bgPadding 238 | 239 | //绘制背景 240 | rectF.apply { 241 | this.left = bgPadding 242 | this.top = bgPadding - barRadius 243 | this.right = this.left + (width - viewHeight) 244 | this.bottom = this.top + barWidth 245 | } 246 | canvas.drawRoundRect(rectF, progressRadius, progressRadius, bgPaint) 247 | 248 | //绘制进度条 249 | rectF.apply { 250 | this.left = bgPadding 251 | this.top = bgPadding - barRadius 252 | this.right = progressWidth 253 | this.bottom = this.top + barWidth 254 | } 255 | canvas.drawRoundRect(rectF, progressRadius, progressRadius, progressPaint) 256 | 257 | //绘制ThumbStroke 258 | rectF.apply { 259 | this.left = progressWidth - thumbWidthRadius 260 | this.top = bgPadding - thumbWidthRadius 261 | this.right = this.left + barThumbWidth 262 | this.bottom = this.top + barThumbWidth 263 | } 264 | 265 | //绘制阴影 266 | thumbStrokePaint.setShadowLayer(elevation, 0f, 3f, Color.GRAY) 267 | canvas.drawRoundRect(rectF, thumbRadius, thumbRadius, thumbStrokePaint) 268 | 269 | //绘制Thumb 270 | rectF.apply { 271 | this.left = progressWidth - thumbFillRadius 272 | this.top = bgPadding - thumbFillRadius 273 | this.right = this.left + thumbFillWidth 274 | this.bottom = this.top + thumbFillWidth 275 | } 276 | canvas.drawRoundRect(rectF, progressRadius, progressRadius, thumbFillPaint) 277 | 278 | } 279 | 280 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 281 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 282 | var width = 0 283 | var height = 0 284 | 285 | var specMode = MeasureSpec.getMode(widthMeasureSpec) 286 | var specSize = MeasureSpec.getSize(widthMeasureSpec) 287 | when (specMode) { 288 | MeasureSpec.EXACTLY -> width = specSize 289 | MeasureSpec.AT_MOST -> width = paddingLeft + paddingRight 290 | MeasureSpec.UNSPECIFIED -> { 291 | 292 | } 293 | } 294 | 295 | // 设置高度 296 | specMode = MeasureSpec.getMode(heightMeasureSpec) 297 | specSize = MeasureSpec.getSize(heightMeasureSpec) 298 | when (specMode) { 299 | MeasureSpec.EXACTLY -> height = specSize 300 | MeasureSpec.AT_MOST -> height = width / 10 301 | MeasureSpec.UNSPECIFIED -> { 302 | height = (viewHeight + elevationTarget + .5f).toInt() 303 | } 304 | } 305 | 306 | setMeasuredDimension(width, height) 307 | } 308 | 309 | override fun draw(canvas: Canvas) { 310 | super.draw(canvas) 311 | initState(canvas) 312 | } 313 | 314 | private var dt = 0L 315 | @SuppressLint("ClickableViewAccessibility") 316 | override fun onTouchEvent(event: MotionEvent): Boolean { 317 | val barHeight = barWidth * 3 318 | val progressWidth = (width - barHeight) 319 | 320 | val x = event.x 321 | val rx = if (x < barWidth) { 322 | barWidth 323 | } else if (x > progressWidth + barHeight * .5) { 324 | progressWidth + barHeight * .5f 325 | } else { 326 | x 327 | } 328 | when (event.action) { 329 | MotionEvent.ACTION_DOWN -> { 330 | val realProgress = getRealProgress((rx - barWidth) / progressWidth) 331 | progress = if (realProgress < progressMin) 0f else if (realProgress > progressMax) progressMax else realProgress 332 | dt = changeAnimationDuration 333 | parent?.requestDisallowInterceptTouchEvent(true) 334 | 335 | vibrationDown() 336 | 337 | //elevation 338 | ValueAnimator.ofFloat(elevation, elevationTarget).apply { 339 | duration = 400 340 | interpolator = changeAnimationInterpolator 341 | addUpdateListener { 342 | val v = it.animatedValue as Float 343 | elevation = v 344 | } 345 | }.start() 346 | 347 | //thumb stroke 348 | ValueAnimator.ofFloat(barThumbWidth, barThumbMaxWidth).apply { 349 | duration = 400 350 | interpolator = changeAnimationInterpolator 351 | addUpdateListener { 352 | val v = it.animatedValue as Float 353 | barThumbWidth = v 354 | } 355 | }.start() 356 | 357 | //thumb stroke radius 358 | ValueAnimator.ofFloat(thumbRadius, barThumbMaxRadius).apply { 359 | duration = 400 360 | interpolator = changeAnimationInterpolator 361 | addUpdateListener { 362 | val v = it.animatedValue as Float 363 | thumbRadius = v 364 | } 365 | }.start() 366 | 367 | //thumb fill 368 | ValueAnimator.ofFloat(thumbFillWidth, barWidth * 1.5f).apply { 369 | duration = 400 370 | interpolator = changeAnimationInterpolator 371 | addUpdateListener { 372 | val v = it.animatedValue as Float 373 | thumbFillWidth = v 374 | } 375 | }.start() 376 | 377 | //progress radius 378 | ValueAnimator.ofFloat(progressRadius, barProgressRadius * 1.5f).apply { 379 | duration = 400 380 | interpolator = changeAnimationInterpolator 381 | addUpdateListener { 382 | val v = it.animatedValue as Float 383 | progressRadius = v 384 | invalidate() 385 | } 386 | }.start() 387 | } 388 | 389 | MotionEvent.ACTION_MOVE -> { 390 | changeAnimationDuration = 0 391 | val realProgress = getRealProgress((rx - barWidth) / progressWidth) 392 | progress = if (realProgress < progressMin) 0f else if (realProgress > progressMax) progressMax else realProgress 393 | invalidate() 394 | } 395 | 396 | MotionEvent.ACTION_UP -> { 397 | vibrationUp() 398 | 399 | ValueAnimator.ofFloat(elevation, 0f).apply { 400 | duration = 400 401 | interpolator = changeAnimationInterpolator 402 | addUpdateListener { 403 | val v = it.animatedValue as Float 404 | elevation = v 405 | } 406 | }.start() 407 | ValueAnimator.ofFloat(barThumbWidth, barWidth * 3).apply { 408 | duration = 400 409 | interpolator = changeAnimationInterpolator 410 | addUpdateListener { 411 | val v = it.animatedValue as Float 412 | barThumbWidth = v 413 | } 414 | }.start() 415 | ValueAnimator.ofFloat(thumbRadius, barThumbRadius).apply { 416 | duration = 400 417 | interpolator = changeAnimationInterpolator 418 | addUpdateListener { 419 | val v = it.animatedValue as Float 420 | thumbRadius = v 421 | } 422 | }.start() 423 | ValueAnimator.ofFloat(thumbFillWidth, barWidth).apply { 424 | duration = 400 425 | interpolator = changeAnimationInterpolator 426 | addUpdateListener { 427 | val v = it.animatedValue as Float 428 | thumbFillWidth = v 429 | } 430 | }.start() 431 | ValueAnimator.ofFloat(progressRadius, barProgressRadius).apply { 432 | duration = 400 433 | interpolator = changeAnimationInterpolator 434 | addUpdateListener { 435 | val v = it.animatedValue as Float 436 | progressRadius = v 437 | invalidate() 438 | } 439 | }.start() 440 | changeAnimationDuration = dt 441 | performClick() 442 | parent?.requestDisallowInterceptTouchEvent(false) 443 | } 444 | 445 | MotionEvent.ACTION_CANCEL -> { 446 | changeAnimationDuration = dt 447 | } 448 | } 449 | return true 450 | } 451 | 452 | private fun vibrationDown() { 453 | val flag = 454 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.GESTURE_START else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) HapticFeedbackConstants.KEYBOARD_PRESS else HapticFeedbackConstants.VIRTUAL_KEY 455 | performHapticFeedback(flag) 456 | } 457 | 458 | private fun vibrationUp() { 459 | val flag = 460 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.GESTURE_END else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) HapticFeedbackConstants.KEYBOARD_RELEASE else HapticFeedbackConstants.VIRTUAL_KEY 461 | performHapticFeedback(flag) 462 | } 463 | } -------------------------------------------------------------------------------- /CleverSeekBar/src/main/java/io/github/lumyuan/ux/cleverseekbar/widget/CleverSeekBars.java: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.cleverseekbar.widget; 2 | 3 | import android.view.View; 4 | 5 | public class CleverSeekBars { 6 | 7 | @FunctionalInterface 8 | public interface OnSeekBarChangeListener { 9 | void onChanged(View view, float progress); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /CleverSeekBar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CleverSeekBar/src/test/java/io/github/lumyuan/ux/cleverseekbar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.cleverseekbar 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /Core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /Core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.core' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | targetSdk 33 13 | 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | kotlinOptions { 30 | jvmTarget = '11' 31 | } 32 | buildFeatures { 33 | viewBinding true 34 | } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation 'androidx.core:core-ktx:1.9.0' 40 | implementation 'androidx.appcompat:appcompat:1.6.0' 41 | implementation 'com.google.android.material:material:1.7.0' 42 | testImplementation 'junit:junit:4.13.2' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 45 | 46 | } 47 | 48 | task makeJar(type:Copy){ 49 | delete 'build/libs/demo.jar' 50 | from('build/intermediates/packaged-classes/debug/') 51 | into('build/libs/') 52 | include('classes.jar') 53 | rename('classes.jar','demo.jar') 54 | } 55 | makeJar.dependsOn(build) -------------------------------------------------------------------------------- /Core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/Core/consumer-rules.pro -------------------------------------------------------------------------------- /Core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /Core/src/androidTest/java/io/github/lumyuan/ux/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.core.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /Core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/LiveData.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core 2 | 3 | class LiveData { 4 | 5 | constructor(data: LD? = null){ 6 | this.value = data 7 | } 8 | 9 | var `value`: LD? 10 | @Synchronized 11 | set(value) { 12 | field = value 13 | observerPool.onEach { 14 | it(value) 15 | } 16 | } 17 | @Synchronized 18 | get 19 | 20 | private val observerPool by lazy { 21 | ArrayList<(LD?) -> Unit>() 22 | } 23 | 24 | fun observe(observer: (field: LD?) -> Unit){ 25 | this.observerPool.add(observer) 26 | } 27 | 28 | fun removeObserver(index: Int){ 29 | this.observerPool.removeAt(index) 30 | } 31 | 32 | fun removeObserver(observer: (field: LD?) -> Unit){ 33 | this.observerPool.remove(observer) 34 | } 35 | 36 | fun clearObservers(){ 37 | this.observerPool.clear() 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/animation/Views.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.animation 2 | 3 | import android.animation.ObjectAnimator 4 | import android.content.Context 5 | import android.os.Build 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.view.HapticFeedbackConstants 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import android.view.View.OnTouchListener 12 | import android.view.animation.BounceInterpolator 13 | import android.view.animation.DecelerateInterpolator 14 | 15 | const val duration = 150L 16 | const val onLongTime = 750L 17 | const val onDownScale = 0.9f 18 | const val onUpScale = 1f 19 | private val interpolator = DecelerateInterpolator() 20 | 21 | /** 22 | * @author https://github.com/lumyuan 23 | * @license Apache-2.0 license 24 | * @copyright 2023 lumyuan 25 | * View触感反馈扩展函数 26 | */ 27 | 28 | private var isGlobalVibrate = true 29 | 30 | fun Context.setVibration(isVibration: Boolean) { 31 | isGlobalVibrate = isVibration 32 | } 33 | 34 | fun View.setOnFeedbackListener( 35 | clickable: Boolean = false/*是否开启点击波纹*/, 36 | callOnLongClick: Boolean = false/*是否响应长按事件*/, 37 | isVibration: Boolean = true, 38 | onLongClick: (View) -> Unit = {}, 39 | click: (View) -> Unit = {} 40 | ) { 41 | val myHandler = Handler(Looper.getMainLooper()) 42 | var cancel = true 43 | var isLong = false 44 | val longTouchRunnable = Runnable { 45 | isLong = true 46 | vibrationLong(this, isVibration && isGlobalVibrate) 47 | onLongClick(this) 48 | cancel = false 49 | onUp(this) 50 | } 51 | if (clickable) { 52 | isClickable = true 53 | } 54 | setOnTouchListener(object : OnTouchListener { 55 | override fun onTouch(v: View, event: MotionEvent): Boolean { 56 | when (event.action) { 57 | MotionEvent.ACTION_UP -> { 58 | if (isLong) { 59 | //onLongClick(this@setOnFeedbackListener) 60 | myHandler.removeCallbacks(longTouchRunnable) 61 | return if (!clickable) { 62 | true 63 | } else { 64 | onTouchEvent(event) 65 | } 66 | } else { 67 | if (cancel) { 68 | performClick() 69 | click(v) 70 | } 71 | } 72 | vibrationUp(v, isVibration && isGlobalVibrate) 73 | onUp(v) 74 | myHandler.removeCallbacks(longTouchRunnable) 75 | return if (!clickable) { 76 | true 77 | } else { 78 | onTouchEvent(event) 79 | } 80 | } 81 | 82 | MotionEvent.ACTION_MOVE -> { 83 | val x = event.x 84 | val y = event.y 85 | if (x < 0 || y < 0 || x > v.measuredWidth || y > v.measuredHeight) { 86 | onUp(v) 87 | cancel = false 88 | myHandler.removeCallbacks(longTouchRunnable) 89 | return if (!clickable) { 90 | true 91 | } else { 92 | onTouchEvent(event) 93 | } 94 | } 95 | } 96 | 97 | MotionEvent.ACTION_CANCEL -> { 98 | onUp(v) 99 | cancel = false 100 | isLong = false 101 | myHandler.removeCallbacks(longTouchRunnable) 102 | return if (!clickable) { 103 | true 104 | } else { 105 | onTouchEvent(event) 106 | } 107 | } 108 | 109 | MotionEvent.ACTION_DOWN -> { 110 | cancel = true 111 | isLong = false 112 | onDown(v) 113 | vibrationDown(v, isVibration && isGlobalVibrate) 114 | if (callOnLongClick) { 115 | myHandler.postDelayed( 116 | longTouchRunnable, onLongTime 117 | ) 118 | } 119 | } 120 | } 121 | return if (!clickable) { 122 | true 123 | } else { 124 | onTouchEvent(event) 125 | } 126 | } 127 | }) 128 | } 129 | 130 | private fun onDown(view: View) { 131 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", onDownScale) 132 | scaleX.duration = duration 133 | scaleX.interpolator = interpolator 134 | scaleX.start() 135 | 136 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", onDownScale) 137 | scaleY.duration = duration 138 | scaleY.interpolator = interpolator 139 | scaleY.start() 140 | } 141 | 142 | private fun onUp(view: View) { 143 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", onUpScale) 144 | scaleX.duration = duration 145 | scaleX.interpolator = interpolator 146 | scaleX.start() 147 | 148 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", onUpScale) 149 | scaleY.duration = duration 150 | scaleY.interpolator = interpolator 151 | scaleY.start() 152 | } 153 | 154 | private fun vibrationDown(view: View, isVibration: Boolean) { 155 | if (!isVibration){ 156 | return 157 | } 158 | val flag = 159 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.GESTURE_START else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) HapticFeedbackConstants.KEYBOARD_PRESS else HapticFeedbackConstants.VIRTUAL_KEY 160 | view.performHapticFeedback(flag) 161 | } 162 | 163 | private fun vibrationUp(view: View, isVibration: Boolean) { 164 | if (!isVibration){ 165 | return 166 | } 167 | val flag = 168 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.GESTURE_END else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) HapticFeedbackConstants.KEYBOARD_RELEASE else HapticFeedbackConstants.VIRTUAL_KEY 169 | view.performHapticFeedback(flag) 170 | } 171 | 172 | private fun vibrationLong(view: View, isVibration: Boolean) { 173 | if (!isVibration){ 174 | return 175 | } 176 | view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) 177 | } 178 | 179 | private var animatorX: ObjectAnimator? = null 180 | private var animatorY: ObjectAnimator? = null 181 | 182 | /** 183 | * 3D触摸动画 184 | * @param maxAngle 最大偏移角度 185 | */ 186 | fun View.setOnTouchAnimationToRotation( 187 | maxAngle: Float = 20F, 188 | isVibration: Boolean = isGlobalVibrate 189 | ) { 190 | var mRotationX: Float 191 | var mRotationY: Float 192 | 193 | this.setOnTouchListener { v, event -> 194 | var x = event.x 195 | var y = event.y 196 | when (event.action) { 197 | MotionEvent.ACTION_DOWN -> { 198 | mRotationX = getSpecificValueX(v, y) * maxAngle 199 | mRotationY = getSpecificValueY(v, x) * maxAngle 200 | startRotationX(v, mRotationX, 200) 201 | startRotationY(v, mRotationY, 200) 202 | } 203 | 204 | MotionEvent.ACTION_MOVE -> { 205 | if (x < 0){ 206 | x = 0f 207 | }else if (x > v.width){ 208 | x = v.width.toFloat() 209 | } 210 | if (y < 0){ 211 | y = 0f 212 | } else if (y > v.height){ 213 | y = v.height.toFloat() 214 | } 215 | mRotationX = getSpecificValueX(v, y) * maxAngle 216 | mRotationY = getSpecificValueY(v, x) * maxAngle 217 | startRotationX(v, mRotationX, 200) 218 | startRotationY(v, mRotationY, 200) 219 | } 220 | 221 | MotionEvent.ACTION_UP -> { 222 | resetState(v, isVibration) 223 | mRotationX = 0f 224 | mRotationY = 0f 225 | performClick() 226 | } 227 | 228 | MotionEvent.ACTION_CANCEL -> { 229 | resetState(v, isVibration) 230 | mRotationX = 0f 231 | mRotationY = 0f 232 | } 233 | } 234 | true 235 | } 236 | } 237 | 238 | private fun getSpecificValueY(targetView: View, x: Float): Float { 239 | val halfHeight = targetView.width / 2f 240 | return -if (x <= halfHeight) { 241 | (1 - (x / halfHeight)) 242 | } else { 243 | -((x - halfHeight) / halfHeight) 244 | } 245 | } 246 | 247 | private fun getSpecificValueX(targetView: View, x: Float): Float { 248 | val halfHeight = targetView.height / 2f 249 | return -if (x <= halfHeight) { 250 | -(1 - (x / halfHeight)) 251 | } else { 252 | ((x - halfHeight) / halfHeight) 253 | } 254 | } 255 | 256 | private fun startRotationX(view: View, `value`: Float, duration: Long = 50) { 257 | animatorX?.end() 258 | animatorX = ObjectAnimator.ofFloat(view, "rotationX", view.rotationX, value).apply { 259 | this.duration = duration 260 | } 261 | animatorX?.start() 262 | } 263 | 264 | private fun startRotationY(view: View, `value`: Float, duration: Long = 50) { 265 | animatorY?.end() 266 | animatorY = ObjectAnimator.ofFloat(view, "rotationY", view.rotationY, value).apply { 267 | this.duration = duration 268 | } 269 | animatorY?.start() 270 | } 271 | 272 | private fun resetState(view: View, isVibration: Boolean) { 273 | var vX = -1 274 | var lastVX = -1 275 | var lastX = view.rotationX 276 | 277 | var vY = -1 278 | var lastVY = -1 279 | var lastY = view.rotationY 280 | animatorX?.end() 281 | animatorY?.end() 282 | animatorX = ObjectAnimator.ofFloat(view, "rotationX", 0f).apply { 283 | duration = 750 284 | interpolator = BounceInterpolator() 285 | addUpdateListener { 286 | val f = it.animatedValue as Float 287 | if (lastX > f){ 288 | vX = 0 289 | } 290 | if (lastX < f){ 291 | vX = 1 292 | } 293 | if (lastVX != vX){ 294 | simulatedOverBounce(view, isVibration) 295 | } 296 | lastVX = vX 297 | lastX = f 298 | } 299 | } 300 | animatorX?.start() 301 | 302 | animatorY = ObjectAnimator.ofFloat(view, "rotationY", 0f).apply { 303 | duration = 750 304 | interpolator = BounceInterpolator() 305 | addUpdateListener { 306 | val f = it.animatedValue as Float 307 | if (lastY > f){ 308 | vY = 0 309 | } 310 | if (lastY < f){ 311 | vY = 1 312 | } 313 | if (lastVY != vY){ 314 | simulatedOverBounce(view, isVibration) 315 | } 316 | lastVY = vY 317 | lastY = f 318 | } 319 | } 320 | animatorY?.start() 321 | } 322 | 323 | private fun simulatedOverBounce(view: View, isVibration: Boolean) { 324 | if (isVibration) { 325 | view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) 326 | } 327 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/common/Contexts.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.common 2 | 3 | import android.content.Context 4 | import android.graphics.Point 5 | import android.view.WindowManager 6 | 7 | fun Context.width(): Int { 8 | val windowManager = this.getSystemService(Context.WINDOW_SERVICE) as WindowManager 9 | val display = windowManager.defaultDisplay 10 | val outPoint = Point() 11 | display.getRealSize(outPoint) 12 | return outPoint.x 13 | } 14 | 15 | fun Context.height(): Int { 16 | val windowManager = this.getSystemService(Context.WINDOW_SERVICE) as WindowManager 17 | val display = windowManager.defaultDisplay 18 | val outPoint = Point() 19 | display.getRealSize(outPoint) 20 | return outPoint.y 21 | } 22 | 23 | fun Context.px2dip(pxValue: Float): Int { 24 | val scale = this.resources.displayMetrics.density 25 | return (pxValue / scale + 0.5f).toInt() 26 | } 27 | 28 | fun Context.dip2px(dipValue: Float): Int { 29 | val scale = this.resources.displayMetrics.density 30 | return (dipValue * scale + 0.5f).toInt() 31 | } 32 | 33 | fun Context.px2sp(pxValue: Float): Int { 34 | val fontScale = this.resources.displayMetrics.scaledDensity 35 | return (pxValue / fontScale + 0.5f).toInt() 36 | } 37 | 38 | fun Context.sp2px(spValue: Float): Int { 39 | val fontScale = this.resources.displayMetrics.scaledDensity 40 | return (spValue * fontScale + 0.5f).toInt() 41 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/common/ViewBindings.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.common 2 | 3 | import android.app.Activity 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.fragment.app.Fragment 7 | import androidx.viewbinding.ViewBinding 8 | 9 | inline fun AppCompatActivity.bind( 10 | crossinline inflater: (LayoutInflater) -> VB, 11 | crossinline onStart: (Activity) -> Unit = {} 12 | ) = lazy { 13 | onStart(this) 14 | inflater(layoutInflater).apply { 15 | setContentView(this.root) 16 | } 17 | } 18 | 19 | inline fun Fragment.bind( 20 | crossinline inflater: (LayoutInflater) -> VB, 21 | crossinline onStart: (Activity) -> Unit = {} 22 | ) = lazy { 23 | activity?.apply { 24 | onStart(this) 25 | } 26 | inflater(layoutInflater) 27 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/ui/adapter/FastRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.ui.adapter 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import androidx.annotation.LayoutRes 6 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 7 | import io.github.lumyuan.ux.core.ui.base.BaseRecyclerViewAdapter 8 | 9 | class FastRecyclerViewAdapter( 10 | @LayoutRes private val layoutId: Int, 11 | private val onBindViewHolderListener: ViewAdapters.OnBindViewHolderListener 12 | ) : BaseRecyclerViewAdapter() { 13 | 14 | class FastRecyclerViewAdapterViewHolder(val rootView: View) : ViewHolder(rootView) 15 | 16 | override fun onCreateViewHolder( 17 | parent: ViewGroup, 18 | viewType: Int 19 | ): FastRecyclerViewAdapterViewHolder = 20 | FastRecyclerViewAdapterViewHolder( 21 | View.inflate(parent.context, layoutId, null) 22 | ) 23 | 24 | override fun getItemCount(): Int = this.list.size 25 | 26 | override fun onBindViewHolder(holder: FastRecyclerViewAdapterViewHolder, position: Int) { 27 | onBindViewHolderListener.onBindViewHolder(this, holder.rootView, this.list[position], position) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/ui/adapter/FastViewBindingRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.ui.adapter 2 | 3 | import android.app.Activity 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 7 | import androidx.viewbinding.ViewBinding 8 | import io.github.lumyuan.ux.core.ui.base.BaseRecyclerViewAdapter 9 | 10 | class FastViewBindingRecyclerViewAdapter( 11 | private val inflate: (LayoutInflater) -> VB, 12 | private val onBindViewHolderListener: ViewBindingAdapters.OnBindViewHolderListener 13 | ) : BaseRecyclerViewAdapter>() { 14 | class FastViewBindingRecyclerViewAdapterViewHolder(val binding: VB) : 15 | ViewHolder(binding.root) 16 | 17 | override fun onCreateViewHolder( 18 | parent: ViewGroup, 19 | viewType: Int 20 | ): FastViewBindingRecyclerViewAdapterViewHolder = 21 | FastViewBindingRecyclerViewAdapterViewHolder( 22 | inflate((parent.context as Activity).layoutInflater) 23 | ) 24 | 25 | override fun getItemCount(): Int = this.list.size 26 | 27 | override fun onBindViewHolder( 28 | holder: FastViewBindingRecyclerViewAdapterViewHolder, 29 | position: Int 30 | ) { 31 | this.onBindViewHolderListener.onBindViewHolder( 32 | this, 33 | holder.binding, 34 | this.list[position], 35 | position 36 | ) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/ui/adapter/ViewAdapters.java: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.ui.adapter; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | public class ViewAdapters { 8 | @FunctionalInterface 9 | public interface OnBindViewHolderListener { 10 | void onBindViewHolder(FastRecyclerViewAdapter adapter, @NonNull View rootView, T data, int position); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/ui/adapter/ViewBindingAdapters.java: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.ui.adapter; 2 | 3 | import androidx.viewbinding.ViewBinding; 4 | 5 | public class ViewBindingAdapters { 6 | @FunctionalInterface 7 | public interface OnBindViewHolderListener { 8 | void onBindViewHolder(FastViewBindingRecyclerViewAdapter adapter, VB binding, T data, int position); 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Core/src/main/java/io/github/lumyuan/ux/core/ui/base/BaseRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core.ui.base 2 | 3 | import androidx.recyclerview.widget.RecyclerView.Adapter 4 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 5 | 6 | abstract class BaseRecyclerViewAdapter : Adapter() { 7 | 8 | val list = ArrayList() 9 | 10 | fun addItem(position: Int, element: T) { 11 | if (position >= 0) { 12 | list.add(position, element) 13 | notifyItemInserted(position) 14 | notifyItemRangeChanged(position, list.size - position, "addItem") 15 | } 16 | } 17 | 18 | fun addItems(position: Int, list: List) { 19 | if (position >= 0) { 20 | this.list.addAll(position, list) 21 | notifyItemRangeInserted(position, list.size) 22 | notifyItemRangeChanged(position, this.list.size - position, "addItems") 23 | } 24 | } 25 | 26 | fun removeItem(position: Int) { 27 | if (position >= 0 && position < list.size) { 28 | list.removeAt(position) 29 | notifyItemRemoved(position) 30 | notifyItemRangeChanged(position, list.size - position, "removeItem") 31 | } 32 | } 33 | 34 | fun removeItems(list: List, positionStart: Int) { 35 | val result: Boolean = this.list.removeAll(list.toSet()) 36 | if (result) { 37 | notifyItemRangeRemoved(positionStart, list.size) 38 | notifyItemRangeChanged(positionStart, list.size - positionStart, "removeItems") 39 | } 40 | } 41 | 42 | fun clearItems() { 43 | val size = list.size 44 | this.list.clear() 45 | notifyItemRangeRemoved(0, size) 46 | notifyItemRangeChanged(0, size, "clearItems") 47 | } 48 | } -------------------------------------------------------------------------------- /Core/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF232323 4 | #FF000000 5 | -------------------------------------------------------------------------------- /Core/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF0099FF 4 | #FFEEEEEE 5 | #FFFFFFFF 6 | -------------------------------------------------------------------------------- /Core/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Core/src/test/java/io/github/lumyuan/ux/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.core 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /GroundGlassView/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /GroundGlassView/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.groundglass' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | targetSdk 33 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_11 26 | targetCompatibility JavaVersion.VERSION_11 27 | } 28 | kotlinOptions { 29 | jvmTarget = '11' 30 | } 31 | } 32 | 33 | dependencies { 34 | 35 | implementation 'androidx.core:core-ktx:1.9.0' 36 | implementation 'androidx.appcompat:appcompat:1.6.0' 37 | implementation 'com.google.android.material:material:1.7.0' 38 | testImplementation 'junit:junit:4.13.2' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 41 | 42 | implementation project(':Core') 43 | } -------------------------------------------------------------------------------- /GroundGlassView/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/GroundGlassView/consumer-rules.pro -------------------------------------------------------------------------------- /GroundGlassView/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /GroundGlassView/src/androidTest/java/io/github/lumyuan/ux/groundglass/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.groundglass.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/dao/AndroidStockBlurImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.dao 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.graphics.Bitmap 6 | import android.renderscript.Allocation 7 | import android.renderscript.Element 8 | import android.renderscript.RSRuntimeException 9 | import android.renderscript.RenderScript 10 | import android.renderscript.ScriptIntrinsicBlur 11 | 12 | class AndroidStockBlurImpl : BlurImpl { 13 | private var mRenderScript: RenderScript? = null 14 | private var mBlurScript: ScriptIntrinsicBlur? = null 15 | private var mBlurInput: Allocation? = null 16 | private var mBlurOutput:Allocation? = null 17 | 18 | override fun prepare(context: Context?, buffer: Bitmap?, radius: Float): Boolean { 19 | if (mRenderScript == null) { 20 | try { 21 | mRenderScript = RenderScript.create(context) 22 | mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)) 23 | } catch (e: RSRuntimeException) { 24 | return if (isDebug(context)) { 25 | throw e 26 | } else { 27 | // In release mode, just ignore 28 | release() 29 | false 30 | } 31 | } 32 | } 33 | mBlurScript!!.setRadius(radius) 34 | mBlurInput = Allocation.createFromBitmap( 35 | mRenderScript, buffer, 36 | Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT 37 | ) 38 | mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput?.type) 39 | return true 40 | } 41 | 42 | override fun release() { 43 | if (mBlurInput != null) { 44 | mBlurInput!!.destroy() 45 | mBlurInput = null 46 | } 47 | if (mBlurOutput != null) { 48 | mBlurOutput?.destroy() 49 | mBlurOutput = null 50 | } 51 | if (mBlurScript != null) { 52 | mBlurScript!!.destroy() 53 | mBlurScript = null 54 | } 55 | if (mRenderScript != null) { 56 | mRenderScript!!.destroy() 57 | mRenderScript = null 58 | } 59 | } 60 | 61 | override fun blur(input: Bitmap?, output: Bitmap?) { 62 | mBlurInput!!.copyFrom(input) 63 | mBlurScript!!.setInput(mBlurInput) 64 | mBlurScript!!.forEach(mBlurOutput) 65 | mBlurOutput?.copyTo(output) 66 | } 67 | 68 | private var DEBUG: Boolean? = null 69 | 70 | fun isDebug(ctx: Context?): Boolean { 71 | if (DEBUG == null && ctx != null) { 72 | DEBUG = ctx.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 73 | } 74 | return DEBUG === java.lang.Boolean.TRUE 75 | } 76 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/dao/AndroidXBlurImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.dao 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.renderscript.Allocation 6 | import android.renderscript.Element 7 | import android.renderscript.RSRuntimeException 8 | import android.renderscript.RenderScript 9 | import android.renderscript.ScriptIntrinsicBlur 10 | 11 | class AndroidXBlurImpl : BlurImpl { 12 | 13 | private var mRenderScript: RenderScript? = null 14 | private var mBlurScript: ScriptIntrinsicBlur? = null 15 | private var mBlurInput: Allocation? = null 16 | private var mBlurOutput: Allocation? = null 17 | var DEBUG: Boolean? = null 18 | 19 | override fun prepare(context: Context?, buffer: Bitmap?, radius: Float): Boolean { 20 | if (mRenderScript == null) { 21 | try { 22 | mRenderScript = RenderScript.create(context) 23 | mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)) 24 | } catch (var5: RSRuntimeException) { 25 | if (isDebug(context)) { 26 | throw var5 27 | } 28 | release() 29 | return false 30 | } 31 | } 32 | mBlurScript?.setRadius(radius) 33 | mBlurInput = 34 | Allocation.createFromBitmap(mRenderScript, buffer, Allocation.MipmapControl.MIPMAP_NONE, 1) 35 | mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput?.type) 36 | return true 37 | } 38 | 39 | override fun release() { 40 | if (mBlurInput != null) { 41 | mBlurInput?.destroy() 42 | mBlurInput = null 43 | } 44 | if (mBlurOutput != null) { 45 | mBlurOutput?.destroy() 46 | mBlurOutput = null 47 | } 48 | if (mBlurScript != null) { 49 | mBlurScript?.destroy() 50 | mBlurScript = null 51 | } 52 | if (mRenderScript != null) { 53 | mRenderScript?.destroy() 54 | mRenderScript = null 55 | } 56 | } 57 | 58 | override fun blur(input: Bitmap?, output: Bitmap?) { 59 | mBlurInput?.copyFrom(input) 60 | mBlurScript?.setInput(mBlurInput) 61 | mBlurScript?.forEach(mBlurOutput) 62 | mBlurOutput?.copyTo(output) 63 | } 64 | 65 | fun isDebug(ctx: Context?): Boolean { 66 | if (DEBUG == null && ctx != null) { 67 | DEBUG = ctx.applicationInfo.flags and 2 != 0 68 | } 69 | return DEBUG === java.lang.Boolean.TRUE 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/dao/BlurImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.dao 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | 6 | interface BlurImpl { 7 | fun prepare(context: Context?, buffer: Bitmap?, radius: Float): Boolean 8 | fun release() 9 | fun blur(input: Bitmap?, output: Bitmap?) 10 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/dao/EmptyBlurImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.dao 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | 6 | class EmptyBlurImpl: BlurImpl { 7 | override fun prepare(context: Context?, buffer: Bitmap?, radius: Float): Boolean { 8 | return false 9 | } 10 | 11 | override fun release() {} 12 | 13 | override fun blur(input: Bitmap?, output: Bitmap?) {} 14 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/dao/SupportLibraryBlurImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.dao 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.renderscript.Allocation 6 | import android.renderscript.Element 7 | import android.renderscript.RSRuntimeException 8 | import android.renderscript.RenderScript 9 | import android.renderscript.ScriptIntrinsicBlur 10 | 11 | class SupportLibraryBlurImpl : BlurImpl { 12 | private var mRenderScript: RenderScript? = null 13 | private var mBlurScript: ScriptIntrinsicBlur? = null 14 | private var mBlurInput: Allocation? = null 15 | private var mBlurOutput: Allocation? = null 16 | var DEBUG: Boolean? = null 17 | 18 | override fun prepare(context: Context?, buffer: Bitmap?, radius: Float): Boolean { 19 | if (mRenderScript == null) { 20 | try { 21 | mRenderScript = RenderScript.create(context) 22 | mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)) 23 | } catch (var5: RSRuntimeException) { 24 | if (isDebug(context)) { 25 | throw var5 26 | } 27 | release() 28 | return false 29 | } 30 | } 31 | mBlurScript?.setRadius(radius) 32 | mBlurInput = 33 | Allocation.createFromBitmap(mRenderScript, buffer, Allocation.MipmapControl.MIPMAP_NONE, 1) 34 | mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput?.type) 35 | return true 36 | } 37 | 38 | override fun release() { 39 | if (mBlurInput != null) { 40 | mBlurInput?.destroy() 41 | mBlurInput = null 42 | } 43 | if (mBlurOutput != null) { 44 | mBlurOutput?.destroy() 45 | mBlurOutput = null 46 | } 47 | if (mBlurScript != null) { 48 | mBlurScript?.destroy() 49 | mBlurScript = null 50 | } 51 | if (mRenderScript != null) { 52 | mRenderScript?.destroy() 53 | mRenderScript = null 54 | } 55 | } 56 | 57 | override fun blur(input: Bitmap?, output: Bitmap?) { 58 | mBlurInput?.copyFrom(input) 59 | mBlurScript?.setInput(mBlurInput) 60 | mBlurScript?.forEach(mBlurOutput) 61 | mBlurOutput?.copyTo(output) 62 | } 63 | 64 | fun isDebug(ctx: Context?): Boolean { 65 | if (DEBUG == null && ctx != null) { 66 | DEBUG = ctx.applicationInfo.flags and 2 != 0 67 | } 68 | return DEBUG === java.lang.Boolean.TRUE 69 | } 70 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/java/io/github/lumyuan/ux/groundglass/widget/GroundGlassView.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass.widget 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.graphics.Bitmap 7 | import android.graphics.Canvas 8 | import android.graphics.Paint 9 | import android.graphics.Rect 10 | import android.os.Build 11 | import android.util.AttributeSet 12 | import android.util.TypedValue 13 | import android.view.View 14 | import android.view.ViewTreeObserver 15 | import android.widget.FrameLayout 16 | import io.github.lumyuan.ux.groundglass.R 17 | import io.github.lumyuan.ux.groundglass.dao.AndroidStockBlurImpl 18 | import io.github.lumyuan.ux.groundglass.dao.AndroidXBlurImpl 19 | import io.github.lumyuan.ux.groundglass.dao.BlurImpl 20 | import io.github.lumyuan.ux.groundglass.dao.EmptyBlurImpl 21 | import io.github.lumyuan.ux.groundglass.dao.SupportLibraryBlurImpl 22 | 23 | open class GroundGlassView : View { 24 | constructor(context: Context) : this(context, null) 25 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 26 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 27 | context, 28 | attrs, 29 | defStyleAttr 30 | ) { 31 | iniView(attrs) 32 | } 33 | 34 | private var mDownSampleFactor = 0f // default 4 35 | private var mOverlayColor = 0 // default #aaffffff 36 | private var mBlurRadius = 0f // default 10dp (0 < r <= 25) 37 | 38 | private var mBlurImpl: BlurImpl? = null 39 | private var mDirty = false 40 | private var mBitmapToBlur: Bitmap? = null 41 | private var mBlurredBitmap: Bitmap? = null 42 | private var mBlurringCanvas: Canvas? = null 43 | private var mIsRendering = false 44 | private var mPaint: Paint? = null 45 | private var mRectSrc = Rect() 46 | private var mRectDst = Rect() 47 | 48 | // mDecorView should be the root view of the activity (even if you are on a different window like a dialog) 49 | private var mDecorView: View? = null 50 | 51 | // If the view is on different root view (usually means we are on a PopupWindow), 52 | // we need to manually call invalidate() in onPreDraw(), otherwise we will not be able to see the changes 53 | private var mDifferentRoot = false 54 | private var RENDERING_COUNT = 0 55 | private var BLUR_IMPL = 0 56 | 57 | private fun iniView(attrs: AttributeSet?) { 58 | mBlurImpl = getBlurImpl() // provide your own by override getBlurImpl() 59 | 60 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.GroundGlassView) 61 | mBlurRadius = typedArray.getDimension( 62 | R.styleable.GroundGlassView_blurRadius, 63 | TypedValue.applyDimension( 64 | TypedValue.COMPLEX_UNIT_DIP, 65 | 10f, 66 | context.resources.displayMetrics 67 | ) 68 | ) 69 | mDownSampleFactor = typedArray.getFloat(R.styleable.GroundGlassView_downSampleFactor, 4f) 70 | mOverlayColor = typedArray.getColor(R.styleable.GroundGlassView_overlayColor, -0x55000001) 71 | 72 | typedArray.recycle() 73 | 74 | mPaint = Paint() 75 | } 76 | 77 | private fun getBlurImpl(): BlurImpl { 78 | if (BLUR_IMPL == 0) { 79 | try { 80 | val impl = AndroidStockBlurImpl() 81 | val bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888) 82 | impl.prepare(context, bmp, 4f) 83 | impl.release() 84 | bmp.recycle() 85 | BLUR_IMPL = 3 86 | } catch (e: Throwable) { 87 | e.printStackTrace() 88 | } 89 | } 90 | if (BLUR_IMPL == 0) { 91 | try { 92 | javaClass.classLoader?.loadClass("androidx.renderscript.RenderScript") 93 | // initialize RenderScript to load jni impl 94 | // may throw unsatisfied link error 95 | val impl = AndroidXBlurImpl() 96 | val bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888) 97 | impl.prepare(context, bmp, 4f) 98 | impl.release() 99 | bmp.recycle() 100 | BLUR_IMPL = 1 101 | } catch (e: Throwable) { 102 | e.printStackTrace() 103 | } 104 | } 105 | if (BLUR_IMPL == 0) { 106 | try { 107 | javaClass.classLoader?.loadClass("android.support.v8.renderscript.RenderScript") 108 | // initialize RenderScript to load jni impl 109 | // may throw unsatisfied link error 110 | val impl = SupportLibraryBlurImpl() 111 | val bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888) 112 | impl.prepare(context, bmp, 4f) 113 | impl.release() 114 | bmp.recycle() 115 | BLUR_IMPL = 2 116 | } catch (e: Throwable) { 117 | e.printStackTrace() 118 | } 119 | } 120 | if (BLUR_IMPL == 0) { 121 | BLUR_IMPL = -1 122 | } 123 | return when (BLUR_IMPL) { 124 | 1 -> AndroidXBlurImpl() 125 | 2 -> SupportLibraryBlurImpl() 126 | 3 -> AndroidStockBlurImpl() 127 | else -> EmptyBlurImpl() 128 | } 129 | } 130 | 131 | open fun setBlurRadius(radius: Float) { 132 | if (mBlurRadius != radius) { 133 | mBlurRadius = radius 134 | mDirty = true 135 | invalidate() 136 | } 137 | } 138 | 139 | open fun setDownSampleFactor(factor: Float) { 140 | require(factor > 0) { "Down sample factor must be greater than 0." } 141 | if (mDownSampleFactor != factor) { 142 | mDownSampleFactor = factor 143 | mDirty = true // may also change blur radius 144 | releaseBitmap() 145 | invalidate() 146 | } 147 | } 148 | 149 | open fun setOverlayColor(color: Int) { 150 | if (mOverlayColor != color) { 151 | mOverlayColor = color 152 | invalidate() 153 | } 154 | } 155 | 156 | private fun releaseBitmap() { 157 | if (mBitmapToBlur != null) { 158 | mBitmapToBlur!!.recycle() 159 | mBitmapToBlur = null 160 | } 161 | if (mBlurredBitmap != null) { 162 | mBlurredBitmap!!.recycle() 163 | mBlurredBitmap = null 164 | } 165 | } 166 | 167 | protected open fun release() { 168 | releaseBitmap() 169 | mBlurImpl!!.release() 170 | } 171 | 172 | protected open fun prepare(): Boolean { 173 | if (mBlurRadius == 0f) { 174 | release() 175 | return false 176 | } 177 | var downsampleFactor: Float = mDownSampleFactor 178 | var radius = mBlurRadius / downsampleFactor 179 | if (radius > 25) { 180 | downsampleFactor = downsampleFactor * radius / 25 181 | radius = 25f 182 | } 183 | val width = width 184 | val height = height 185 | val scaledWidth = Math.max(1, (width / downsampleFactor).toInt()) 186 | val scaledHeight = Math.max(1, (height / downsampleFactor).toInt()) 187 | var dirty = mDirty 188 | if (mBlurringCanvas == null || mBlurredBitmap == null || mBlurredBitmap!!.width != scaledWidth || mBlurredBitmap!!.height != scaledHeight) { 189 | dirty = true 190 | releaseBitmap() 191 | var r = false 192 | try { 193 | mBitmapToBlur = 194 | Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888) 195 | if (mBitmapToBlur == null) { 196 | return false 197 | } 198 | mBlurringCanvas = Canvas(mBitmapToBlur!!) 199 | mBlurredBitmap = 200 | Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888) 201 | if (mBlurredBitmap == null) { 202 | return false 203 | } 204 | r = true 205 | } catch (e: OutOfMemoryError) { 206 | // Bitmap.createBitmap() may cause OOM error 207 | // Simply ignore and fallback 208 | } finally { 209 | if (!r) { 210 | release() 211 | return false 212 | } 213 | } 214 | } 215 | if (dirty) { 216 | mDirty = if (mBlurImpl!!.prepare(context, mBitmapToBlur, radius)) { 217 | false 218 | } else { 219 | return false 220 | } 221 | } 222 | return true 223 | } 224 | 225 | protected open fun blur(bitmapToBlur: Bitmap?, blurredBitmap: Bitmap?) { 226 | mBlurImpl!!.blur(bitmapToBlur, blurredBitmap) 227 | } 228 | 229 | private val preDrawListener = ViewTreeObserver.OnPreDrawListener { 230 | val locations = IntArray(2) 231 | var oldBmp = mBlurredBitmap 232 | val decor = mDecorView 233 | if (decor != null && isShown && prepare()) { 234 | val redrawBitmap = mBlurredBitmap != oldBmp 235 | oldBmp = null 236 | decor.getLocationOnScreen(locations) 237 | var x = -locations[0] 238 | var y = -locations[1] 239 | getLocationOnScreen(locations) 240 | x += locations[0] 241 | y += locations[1] 242 | 243 | // just erase transparent 244 | mBitmapToBlur!!.eraseColor(mOverlayColor and 0xffffff) 245 | val rc = mBlurringCanvas!!.save() 246 | mIsRendering = true 247 | RENDERING_COUNT++ 248 | try { 249 | mBlurringCanvas!!.scale( 250 | 1f * mBitmapToBlur!!.width / width, 251 | 1f * mBitmapToBlur!!.height / height 252 | ) 253 | mBlurringCanvas!!.translate(-x.toFloat(), -y.toFloat()) 254 | if (decor.background != null) { 255 | decor.background.draw(mBlurringCanvas!!) 256 | } 257 | decor.draw(mBlurringCanvas) 258 | } catch (e: StopException) { 259 | } finally { 260 | mIsRendering = false 261 | RENDERING_COUNT-- 262 | mBlurringCanvas!!.restoreToCount(rc) 263 | } 264 | blur(mBitmapToBlur, mBlurredBitmap) 265 | if (redrawBitmap || mDifferentRoot) { 266 | invalidate() 267 | } 268 | } 269 | true 270 | } 271 | 272 | protected open fun getActivityDecorView(): View? { 273 | var ctx = context 274 | var i = 0 275 | while (i < 4 && ctx != null && ctx !is Activity && ctx is ContextWrapper) { 276 | ctx = ctx.baseContext 277 | i++ 278 | } 279 | return if (ctx is Activity) { 280 | ctx.window.decorView 281 | } else { 282 | null 283 | } 284 | } 285 | 286 | override fun onAttachedToWindow() { 287 | super.onAttachedToWindow() 288 | mDecorView = getActivityDecorView() 289 | if (mDecorView != null) { 290 | mDecorView?.viewTreeObserver?.addOnPreDrawListener(preDrawListener) 291 | mDifferentRoot = mDecorView?.rootView !== rootView 292 | if (mDifferentRoot) { 293 | mDecorView?.postInvalidate() 294 | } 295 | } else { 296 | mDifferentRoot = false 297 | } 298 | } 299 | 300 | override fun onDetachedFromWindow() { 301 | if (mDecorView != null) { 302 | mDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener) 303 | } 304 | release() 305 | super.onDetachedFromWindow() 306 | } 307 | 308 | override fun draw(canvas: Canvas?) { 309 | if (mIsRendering) { 310 | // Quit here, don't draw views above me 311 | throw STOP_EXCEPTION 312 | } else if (RENDERING_COUNT > 0) { 313 | // Doesn't support blurview overlap on another blurview 314 | } else { 315 | super.draw(canvas) 316 | } 317 | } 318 | 319 | override fun onDraw(canvas: Canvas) { 320 | super.onDraw(canvas) 321 | drawBlurredBitmap(canvas, mBlurredBitmap, mOverlayColor) 322 | } 323 | 324 | /** 325 | * Custom draw the blurred bitmap and color to define your own shape 326 | * 327 | * @param canvas 328 | * @param blurredBitmap 329 | * @param overlayColor 330 | */ 331 | protected open fun drawBlurredBitmap( 332 | canvas: Canvas, 333 | blurredBitmap: Bitmap?, 334 | overlayColor: Int 335 | ) { 336 | if (blurredBitmap != null) { 337 | mRectSrc.right = blurredBitmap.width 338 | mRectSrc.bottom = blurredBitmap.height 339 | mRectDst.right = width 340 | mRectDst.bottom = height 341 | canvas.drawBitmap(blurredBitmap, mRectSrc, mRectDst, null) 342 | } 343 | mPaint!!.color = overlayColor 344 | canvas.drawRect(mRectDst, mPaint!!) 345 | } 346 | 347 | private class StopException : RuntimeException() 348 | 349 | private val STOP_EXCEPTION = StopException() 350 | } -------------------------------------------------------------------------------- /GroundGlassView/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GroundGlassView/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GroundGlassView/src/test/java/io/github/lumyuan/ux/groundglass/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.groundglass 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OverScrollView/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /OverScrollView/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.overscroll' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | targetSdk 33 13 | 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | kotlinOptions { 30 | jvmTarget = '11' 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation 'androidx.core:core-ktx:1.9.0' 37 | implementation 'androidx.appcompat:appcompat:1.6.0' 38 | implementation 'com.google.android.material:material:1.7.0' 39 | testImplementation 'junit:junit:4.13.2' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 42 | 43 | implementation project(':Core') 44 | 45 | } -------------------------------------------------------------------------------- /OverScrollView/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/OverScrollView/consumer-rules.pro -------------------------------------------------------------------------------- /OverScrollView/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /OverScrollView/src/androidTest/java/io/github/lumyuan/ux/overscroll/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.overscroll 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.overscroll.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /OverScrollView/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /OverScrollView/src/test/java/io/github/lumyuan/ux/overscroll/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.overscroll 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaterialUX 2 | 一个基于androidx的质感UI、UX组件库(基于Kotlin),下载预览APK 3 | 4 | * 推荐基于Kotlin编程语言开发的Android项目使用本组件库 5 | 6 | # 使用 7 | 1. 在你的项目下的build.gradle文件或项目下的settings.gradle文件中: 8 | ```gradle 9 | allprojects { 10 | repositories { 11 | ... 12 | maven { url 'https://jitpack.io' } 13 | } 14 | } 15 | ``` 16 | 2. 导入依赖 17 | * 模块:Core(必须)、BottomNavigationView、TopBar、GroundGlassView、CircleSeekBar、OverScrollView,更多组件开发中... 18 | * 版本:[![](https://jitpack.io/v/lumyuan/MaterialUX.svg)](https://jitpack.io/#lumyuan/MaterialUX)(版本名前面记得加v,如:v1.0.1) 19 | ```gradle 20 | implementation 'androidx.core:core-ktx:1.9.0' // Java 项目必须 21 | implementation 'com.github.lumyuan.MaterialUX:Core:{version-name}' //组件库必须 22 | 23 | //可选模块 24 | implementation 'com.github.lumyuan.MaterialUX:BottomNavigationView:{version-name}' 25 | implementation 'com.github.lumyuan.MaterialUX:TopBar:{version-name}' 26 | implementation 'com.github.lumyuan.MaterialUX:GroundGlassView:{version-name}' 27 | implementation 'com.github.lumyuan.MaterialUX:CircleSeekBar:{version-name}' 28 | implementation 'com.github.lumyuan.MaterialUX:OverScrollView:{version-name}' 29 | implementation 'com.github.lumyuan.MaterialUX:CleverSeekBar:{version-name}' 30 | ``` 31 | 32 | ## 代码:查阅app/src/main/java/io/github/lumyuan/ux/MainActivity.java 33 | 34 | # License 35 | ``` 36 | https://github.com/lumyuan/MaterialUX 37 | Copyright 2023 lumyuan 38 | 39 | Licensed under the Apache License, Version 2.0 (the "License"); 40 | you may not use this file except in compliance with the License. 41 | You may obtain a copy of the License at 42 | 43 | http://www.apache.org/licenses/LICENSE-2.0 44 | 45 | Unless required by applicable law or agreed to in writing, software 46 | distributed under the License is distributed on an "AS IS" BASIS, 47 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 48 | See the License for the specific language governing permissions and 49 | limitations under the License. 50 | 51 | Please contact LumYuan by email 2205903933@qq.com if you need 52 | additional information or have any questions 53 | ``` 54 | -------------------------------------------------------------------------------- /TopBar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /TopBar/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux.topbar' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 19 12 | targetSdk 33 13 | 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_11 27 | targetCompatibility JavaVersion.VERSION_11 28 | } 29 | kotlinOptions { 30 | jvmTarget = '11' 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation 'androidx.core:core-ktx:1.9.0' 37 | implementation 'androidx.appcompat:appcompat:1.6.0' 38 | implementation 'com.google.android.material:material:1.7.0' 39 | testImplementation 'junit:junit:4.13.2' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 42 | 43 | implementation project(':Core') 44 | 45 | } 46 | 47 | task makeJar(type:Copy){ 48 | delete 'build/libs/demo.jar' 49 | from('build/intermediates/packaged-classes/debug/') 50 | into('build/libs/') 51 | include('classes.jar') 52 | rename('classes.jar','demo.jar') 53 | } 54 | makeJar.dependsOn(build) -------------------------------------------------------------------------------- /TopBar/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumyuan/MaterialUX/5e3c8188276aa7f52e8a9a7288e473e5e6b09a55/TopBar/consumer-rules.pro -------------------------------------------------------------------------------- /TopBar/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /TopBar/src/androidTest/java/io/github/lumyuan/ux/topbar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.topbar 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux.topbar.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /TopBar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /TopBar/src/main/java/io/github/lumyuan/ux/topbar/widget/TopBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.topbar.widget 2 | 3 | import android.animation.ObjectAnimator 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.graphics.Color 7 | import android.text.TextUtils 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import android.widget.FrameLayout 11 | import android.widget.ImageView 12 | import android.widget.LinearLayout 13 | import android.widget.TextView 14 | import androidx.annotation.DrawableRes 15 | import androidx.viewpager.widget.ViewPager 16 | import androidx.viewpager.widget.ViewPager.OnPageChangeListener 17 | import io.github.lumyuan.ux.core.LiveData 18 | import io.github.lumyuan.ux.core.common.dip2px 19 | import io.github.lumyuan.ux.topbar.R 20 | import kotlin.properties.Delegates 21 | 22 | @SuppressLint("ResourceType") 23 | class TopBar : FrameLayout { 24 | 25 | constructor(context: Context) : this(context, null) 26 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 27 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 28 | context, 29 | attrs, 30 | defStyleAttr 31 | ) { 32 | initView(attrs) 33 | } 34 | 35 | private val topView by lazy { 36 | View.inflate(context, R.layout.top_bar, null) 37 | } 38 | 39 | private lateinit var titleView: TextView 40 | private lateinit var subtitleView: TextView 41 | private lateinit var firstMenu: ImageView 42 | private lateinit var secondsMenu: ImageView 43 | private lateinit var menuLayout: LinearLayout 44 | 45 | private var titleColor by Delegates.notNull() 46 | private var subtitleColor by Delegates.notNull() 47 | private var menuColor by Delegates.notNull() 48 | 49 | private val itemsPool by lazy { 50 | ArrayList() 51 | } 52 | 53 | private var oldOffset = 0f 54 | private val pageChangeListener = object : OnPageChangeListener { 55 | override fun onPageScrolled( 56 | position: Int, 57 | positionOffset: Float, 58 | positionOffsetPixels: Int 59 | ) { 60 | if (positionOffset == 0f){ 61 | titleView.apply { 62 | translationX = 0f 63 | alpha = 1f 64 | } 65 | 66 | subtitleView.apply { 67 | translationX = 0f 68 | alpha = 1f 69 | } 70 | 71 | firstMenu.apply { 72 | translationX = 0f 73 | alpha = 1f 74 | } 75 | 76 | secondsMenu.apply { 77 | translationX = 0f 78 | alpha = 1f 79 | } 80 | }else if (positionOffset < .5f){ 81 | setCurrentItem(position) 82 | 83 | val set = oldOffset * 2f 84 | val tOffset = translationOffset * set 85 | val aOffset = 1 - set 86 | 87 | titleView.apply { 88 | translationX = tOffset 89 | alpha = aOffset 90 | } 91 | 92 | subtitleView.apply { 93 | val t = (translationOffset + context.dip2px(5f)) * set 94 | translationX = t 95 | alpha = aOffset 96 | } 97 | 98 | firstMenu.apply { 99 | translationX = tOffset 100 | alpha = aOffset 101 | } 102 | 103 | secondsMenu.apply { 104 | translationX = tOffset 105 | alpha = aOffset 106 | } 107 | 108 | }else { 109 | setCurrentItem(position + 1) 110 | val set = (positionOffset - .5f) * 2f 111 | val tOffset = (translationOffset * set) - translationOffset 112 | 113 | titleView.apply { 114 | translationX = tOffset 115 | alpha = set 116 | } 117 | 118 | subtitleView.apply { 119 | val t = (translationOffset + context.dip2px(5f)) * set - (translationOffset + context.dip2px(5f)) 120 | translationX = t 121 | alpha = set 122 | } 123 | 124 | firstMenu.apply { 125 | translationX = tOffset 126 | alpha = set 127 | } 128 | 129 | secondsMenu.apply { 130 | translationX = tOffset 131 | alpha = set 132 | } 133 | } 134 | oldOffset = positionOffset 135 | } 136 | 137 | override fun onPageSelected(position: Int) { 138 | } 139 | 140 | override fun onPageScrollStateChanged(state: Int) { 141 | } 142 | 143 | } 144 | 145 | private val positionLiveData = LiveData(0) 146 | 147 | @SuppressLint("Recycle", "ResourceType") 148 | private fun initView(attrs: AttributeSet?) { 149 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.TopBar) 150 | titleColor = typedArray.getColor( 151 | R.styleable.TopBar_titleColor, 152 | Color.parseColor(context.getString(R.color.titleColor)) 153 | ) 154 | subtitleColor = typedArray.getColor( 155 | R.styleable.TopBar_subtitleColor, 156 | Color.parseColor(context.getString(R.color.subtitleColor)) 157 | ) 158 | menuColor = typedArray.getColor( 159 | R.styleable.TopBar_menuColorFilter, 160 | Color.TRANSPARENT 161 | ) 162 | 163 | addView( 164 | topView, 165 | LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) 166 | ) 167 | 168 | titleView = topView.findViewById(R.id.titleView) 169 | subtitleView = topView.findViewById(R.id.subtitleView) 170 | firstMenu = topView.findViewById(R.id.firstMenu) 171 | secondsMenu = topView.findViewById(R.id.secondsMenu) 172 | menuLayout = topView.findViewById(R.id.menuLayout) 173 | 174 | titleView.setTextColor(titleColor) 175 | subtitleView.setTextColor(subtitleColor) 176 | firstMenu.setColorFilter(menuColor) 177 | secondsMenu.setColorFilter(menuColor) 178 | 179 | setBackgroundColor(Color.parseColor(context.getString(R.color.backgroundColor))) 180 | 181 | firstMenu.setOnClickListener { 182 | firstMenuClickListener(it, positionLiveData.value ?: 0) 183 | } 184 | 185 | secondsMenu.setOnClickListener { 186 | secondsMenuClickListener(it, positionLiveData.value ?: 0) 187 | } 188 | } 189 | 190 | fun setupViewpager(viewpager: ViewPager) { 191 | viewpager.addOnPageChangeListener(this.pageChangeListener) 192 | } 193 | 194 | fun setupData(items: ArrayList){ 195 | this.itemsPool.clear() 196 | this.itemsPool.addAll(items) 197 | setCurrentItem(0) 198 | } 199 | 200 | fun setCurrentItem(position: Int){ 201 | this.positionLiveData.value = position 202 | val item = itemsPool[position] 203 | titleView.visibility = if (TextUtils.isEmpty(item.titleText)){ 204 | GONE 205 | }else { 206 | VISIBLE 207 | } 208 | titleView.text = item.titleText 209 | 210 | subtitleView.apply { 211 | visibility = if (TextUtils.isEmpty(item.subtitleText)){ 212 | GONE 213 | }else { 214 | VISIBLE 215 | } 216 | } 217 | subtitleView.text = item.subtitleText 218 | 219 | firstMenu.visibility = if (item.firstMenuIconResource == null){ 220 | GONE 221 | }else { 222 | firstMenu.setImageResource(item.firstMenuIconResource) 223 | VISIBLE 224 | } 225 | 226 | secondsMenu.visibility = if (item.secondsMenuIconResource == null){ 227 | GONE 228 | }else { 229 | secondsMenu.setImageResource(item.secondsMenuIconResource) 230 | VISIBLE 231 | } 232 | } 233 | 234 | private var firstMenuClickListener: (view: View, position: Int) -> Unit = {_, _ ->} 235 | private var secondsMenuClickListener: (view: View, position: Int) -> Unit = {_, _ ->} 236 | fun setFirstMenuOnClickListener(firstMenuClickListener: (view: View, position: Int) -> Unit){ 237 | this.firstMenuClickListener = firstMenuClickListener 238 | } 239 | 240 | fun setSecondsMenuOnClickListener(secondsMenuClickListener: (view: View, position: Int) -> Unit){ 241 | this.secondsMenuClickListener = secondsMenuClickListener 242 | } 243 | 244 | private val translationOffset by lazy { 245 | context.dip2px(8f) 246 | } 247 | private fun setOffsetAnimation(view: View, offset: Float){ 248 | ObjectAnimator.ofFloat(view, "translationX", offset) 249 | } 250 | 251 | data class Item( 252 | var titleText: String?, 253 | var subtitleText: String?, 254 | @DrawableRes val firstMenuIconResource: Int?, 255 | @DrawableRes val secondsMenuIconResource: Int? 256 | ) 257 | 258 | } -------------------------------------------------------------------------------- /TopBar/src/main/res/layout/top_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 20 | 21 | 32 | 33 | 44 | 45 | 46 | 47 | 52 | 53 | 63 | 64 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /TopBar/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FFE6E1E5 5 | #FF938F99 6 | #FFE6E1E5 7 | #FF1C1B1F 8 | 9 | -------------------------------------------------------------------------------- /TopBar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TopBar/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FF1C1B1F 5 | #FF616161 6 | #FF1C1B1F 7 | #FFFFFFFF 8 | 9 | -------------------------------------------------------------------------------- /TopBar/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /TopBar/src/test/java/io/github/lumyuan/ux/topbar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.topbar 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.github.lumyuan.ux' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "io.github.lumyuan.ux" 12 | minSdk 19 13 | targetSdk 33 14 | versionCode libsVersionCode 15 | versionName libsVersionName 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | shrinkResources true 23 | zipAlignEnabled true 24 | minifyEnabled true 25 | debuggable false 26 | jniDebuggable = false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_11 32 | targetCompatibility JavaVersion.VERSION_11 33 | } 34 | kotlinOptions { 35 | jvmTarget = '11' 36 | } 37 | buildFeatures { 38 | viewBinding true 39 | } 40 | 41 | } 42 | 43 | dependencies { 44 | 45 | implementation fileTree(dir: 'libs', includes: ['*.aar', '*.jar']) 46 | 47 | implementation 'androidx.core:core-ktx:1.9.0' 48 | implementation 'androidx.appcompat:appcompat:1.6.0' 49 | implementation 'com.google.android.material:material:1.7.0' 50 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 51 | testImplementation 'junit:junit:4.13.2' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 54 | 55 | implementation project(':Core') 56 | implementation project(':BottomNavigationView') 57 | implementation project(':TopBar') 58 | implementation project(':GroundGlassView') 59 | implementation project(':CircleSeekBar') 60 | implementation project(':OverScrollView') 61 | implementation project(':CleverSeekBar') 62 | 63 | implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2' 64 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "io.github.lumyuan.ux", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 13, 15 | "versionName": "1.0.13", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/lumyuan/ux/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.lumyuan.ux", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/lumyuan/ux/KTBasicActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 7 | import com.gyf.immersionbar.ImmersionBar 8 | import io.github.lumyuan.ux.core.animation.setOnFeedbackListener 9 | import io.github.lumyuan.ux.core.common.bind 10 | import io.github.lumyuan.ux.core.ui.adapter.FastViewBindingRecyclerViewAdapter 11 | import io.github.lumyuan.ux.databinding.ActivityKtBasicBinding 12 | import io.github.lumyuan.ux.databinding.ItemBasicBinding 13 | 14 | class KTBasicActivity : AppCompatActivity() { 15 | 16 | private val binding by bind(ActivityKtBasicBinding::inflate) { 17 | ImmersionBar.with(it) 18 | .transparentStatusBar() 19 | .transparentNavigationBar() 20 | .statusBarDarkFont(true) 21 | .navigationBarDarkIcon(true) 22 | .keyboardEnable(true) 23 | .init() 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | 29 | val recyclerViewAdapter = 30 | FastViewBindingRecyclerViewAdapter(ItemBasicBinding::inflate) {adapter, binding, data, position -> 31 | binding.text1.text = data 32 | binding.root.setOnFeedbackListener { 33 | Toast.makeText(this@KTBasicActivity, data, Toast.LENGTH_SHORT).show() 34 | } 35 | binding.delete.setOnFeedbackListener { 36 | //删除项目 37 | adapter.removeItem(position) 38 | Toast.makeText(this, "已删除$data", Toast.LENGTH_SHORT).show() 39 | } 40 | binding.insert.setOnFeedbackListener { 41 | //插入数据(向下) 42 | adapter.addItem(position + 1, "插入数据测试:${position + 1}") 43 | } 44 | } 45 | 46 | binding.listView.run { 47 | layoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL) 48 | adapter = recyclerViewAdapter 49 | } 50 | 51 | val arrayList = ArrayList() 52 | for (i in 0 until 50) { 53 | arrayList.add("条目测试:$i") 54 | } 55 | //添加数据 56 | recyclerViewAdapter.addItems(0, arrayList) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/lumyuan/ux/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.view.animation.DecelerateInterpolator; 7 | import android.widget.TextView; 8 | import android.widget.Toast; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 15 | import com.gyf.immersionbar.ImmersionBar; 16 | 17 | import java.util.ArrayList; 18 | 19 | import io.github.lumyuan.ux.bottomnavigationview.widget.BottomNavigationView; 20 | import io.github.lumyuan.ux.cleverseekbar.widget.CleverSeekBar; 21 | import io.github.lumyuan.ux.cleverseekbar.widget.CleverSeekBars; 22 | import io.github.lumyuan.ux.core.ui.adapter.FastRecyclerViewAdapter; 23 | import io.github.lumyuan.ux.core.ui.adapter.ViewAdapters; 24 | import io.github.lumyuan.ux.databinding.ActivityMainBinding; 25 | import io.github.lumyuan.ux.topbar.widget.TopBar; 26 | import io.github.lumyuan.ux.ui.PagerAdapterForFragment; 27 | import io.github.lumyuan.ux.ui.fragments.BlankFragment; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | 31 | private ActivityMainBinding binding; 32 | 33 | private ArrayList topBarItems = new ArrayList<>(); 34 | private ArrayList pages = new ArrayList<>(); 35 | @Override 36 | protected void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | binding = ActivityMainBinding.inflate(getLayoutInflater()); 39 | setContentView(binding.getRoot()); 40 | 41 | ImmersionBar.with(this) 42 | .transparentStatusBar() 43 | .transparentNavigationBar() 44 | .statusBarDarkFont(true) 45 | .navigationBarDarkIcon(true) 46 | .keyboardEnable(true) 47 | .init(); 48 | 49 | pages.add( 50 | new PagerAdapterForFragment.Page( 51 | BlankFragment.newInstance("", ""), null 52 | ) 53 | ); 54 | 55 | pages.add( 56 | new PagerAdapterForFragment.Page( 57 | BlankFragment.newInstance("", ""), null 58 | ) 59 | ); 60 | 61 | pages.add( 62 | new PagerAdapterForFragment.Page( 63 | BlankFragment.newInstance("", ""), null 64 | ) 65 | ); 66 | 67 | binding.viewpager.setAdapter( 68 | new PagerAdapterForFragment( 69 | pages.toArray(new PagerAdapterForFragment.Page[3]), 70 | getSupportFragmentManager() 71 | ) 72 | ); 73 | 74 | binding.viewpager.setOffscreenPageLimit(pages.size()); 75 | 76 | topBarItems.add( 77 | new TopBar.Item( 78 | "首页", 79 | "欢迎使用", 80 | null, 81 | null 82 | ) 83 | ); 84 | topBarItems.add( 85 | new TopBar.Item( 86 | "模块", 87 | null, 88 | R.drawable.ic_module, 89 | null 90 | ) 91 | ); 92 | topBarItems.add( 93 | new TopBar.Item( 94 | "我的", 95 | "个人中心", 96 | R.mipmap.ic_launcher, 97 | R.drawable.ic_mine 98 | ) 99 | ); 100 | 101 | //设置标题栏数据 102 | binding.topBar.setupData(topBarItems); 103 | //绑定ViewPager 104 | binding.topBar.setupViewpager(binding.viewpager); 105 | 106 | 107 | //创建导航按钮 108 | //推荐数量:3~5,太多会挤压内部view,太少有点空 109 | BottomNavigationView.ItemView v1 = binding.navigationView.newItemView(); 110 | v1.setText("首页"); //导航条设置标题 111 | v1.setImageResource(R.drawable.ic_home); //设置导航条图标 112 | binding.navigationView.addItemView(v1); //将导航条添加到导航栏 113 | 114 | BottomNavigationView.ItemView v2 = binding.navigationView.newItemView(); 115 | v2.setText("模块"); 116 | v2.setImageResource(R.drawable.ic_module); 117 | binding.navigationView.addItemView(v2); 118 | 119 | BottomNavigationView.ItemView v3 = binding.navigationView.newItemView(); 120 | v3.setText("我的"); 121 | v3.setImageResource(R.drawable.ic_mine); 122 | binding.navigationView.addItemView(v3); 123 | 124 | //导航栏绑定ViewPager 125 | binding.navigationView.setupViewpager(binding.viewpager); 126 | 127 | //TopBar两个按钮的点击事件 128 | binding.topBar.setFirstMenuOnClickListener(((view, position) -> { 129 | Toast.makeText(this, "当前位置:" + (position + 1), Toast.LENGTH_SHORT).show(); 130 | return null; 131 | })); 132 | 133 | binding.topBar.setSecondsMenuOnClickListener(((view, position) -> { 134 | Toast.makeText(this, "当前位置:" + (position + 1), Toast.LENGTH_SHORT).show(); 135 | return null; 136 | })); 137 | 138 | RecyclerView recyclerView = new RecyclerView(this); 139 | 140 | 141 | //View 版本 142 | FastRecyclerViewAdapter adapter = new FastRecyclerViewAdapter<>(R.layout.item_basic, new ViewAdapters.OnBindViewHolderListener() { 143 | @Override 144 | public void onBindViewHolder(FastRecyclerViewAdapter adapter, @NonNull View rootView, String data, int position) { 145 | //当列表项显示时调用,一般是用来绑定数据的 146 | TextView textView = rootView.findViewById(R.id.text1); 147 | textView.setText(data); 148 | } 149 | }); 150 | 151 | recyclerView.setAdapter(adapter); 152 | } 153 | 154 | @Override 155 | protected void onDestroy() { 156 | super.onDestroy(); 157 | binding = null; 158 | } 159 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/lumyuan/ux/ui/PagerAdapterForFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.FragmentStatePagerAdapter 6 | 7 | class PagerAdapterForFragment(private val list: Array, fragmentManager: FragmentManager) 8 | : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 9 | 10 | open class Page(val fragment: Fragment, val title: CharSequence? = null) 11 | 12 | override fun getCount(): Int { 13 | return list.size 14 | } 15 | 16 | override fun getItem(position: Int): Fragment { 17 | return list[position].fragment 18 | } 19 | 20 | override fun getPageTitle(position: Int): CharSequence? { 21 | return list[position].title 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/lumyuan/ux/ui/XViewPager.java: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.ui; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.util.Log; 6 | import android.view.MotionEvent; 7 | 8 | import androidx.viewpager.widget.ViewPager; 9 | 10 | public class XViewPager extends ViewPager { 11 | 12 | private boolean noScroll = false; 13 | private boolean noScrollAnim = false; 14 | private boolean left = false; 15 | private boolean right = false; 16 | private boolean isScrolling = false; 17 | private int lastValue = -1; 18 | private ChangeViewCallback changeViewCallback = null; 19 | 20 | public XViewPager(Context context) { 21 | super(context); 22 | init(); 23 | } 24 | 25 | public XViewPager(Context context, AttributeSet attrs) { 26 | super(context, attrs); 27 | init(); 28 | } 29 | 30 | public void setSlide(boolean slide) { 31 | this.noScroll = slide; 32 | } 33 | 34 | /** 35 | * 设置是否能左右滑动 36 | * @param noScroll true 不能滑动 37 | */ 38 | public void setScroll(boolean noScroll) { 39 | this.noScroll = noScroll; 40 | } 41 | 42 | /** 43 | * 设置没有滑动动画 44 | * @param noAnim false 无动画 45 | */ 46 | public void setScrollAnim(boolean noAnim){ 47 | this.noScrollAnim = noAnim; 48 | } 49 | 50 | @Override 51 | public boolean onTouchEvent(MotionEvent arg0) { 52 | return !noScroll && super.onTouchEvent(arg0); 53 | } 54 | 55 | @Override 56 | public boolean onInterceptTouchEvent(MotionEvent arg0) { 57 | return !noScroll && super.onInterceptTouchEvent(arg0); 58 | } 59 | 60 | @Override 61 | public void setCurrentItem(int item, boolean smoothScroll) { 62 | super.setCurrentItem(item, smoothScroll); 63 | } 64 | 65 | @Override 66 | public void setCurrentItem(int item) { 67 | super.setCurrentItem(item,noScrollAnim); 68 | } 69 | 70 | private void init() { 71 | setOnPageChangeListener(listener); 72 | } 73 | 74 | /** 75 | * listener ,to get move direction . 76 | */ 77 | public OnPageChangeListener listener = new OnPageChangeListener() { 78 | @Override 79 | public void onPageScrollStateChanged(int arg0) { 80 | if (arg0 == 1) { 81 | isScrolling = true; 82 | } else { 83 | isScrolling = false; 84 | } 85 | 86 | } 87 | 88 | @Override 89 | public void onPageScrolled(int arg0, float arg1, int arg2) { 90 | if (isScrolling) { 91 | if (lastValue > arg2) { 92 | // 递减,向右侧滑动 93 | right = true; 94 | left = false; 95 | } else if (lastValue < arg2) { 96 | // 递减,向右侧滑动 97 | right = false; 98 | left = true; 99 | } else if (lastValue == arg2) { 100 | right = left = false; 101 | } 102 | } 103 | Log.i("meityitianViewPager", 104 | "meityitianViewPager onPageScrolled last :arg2 ," 105 | + lastValue + ":" + arg2); 106 | lastValue = arg2; 107 | } 108 | 109 | @Override 110 | public void onPageSelected(int arg0) { 111 | if(changeViewCallback!=null){ 112 | changeViewCallback.getCurrentPageIndex(arg0); 113 | } 114 | } 115 | }; 116 | 117 | /** 118 | * 得到是否向右侧滑动 119 | * @return true 为右滑动 120 | */ 121 | public boolean getMoveRight(){ 122 | return right; 123 | } 124 | 125 | /** 126 | * 得到是否向左侧滑动 127 | * @return true 为左做滑动 128 | */ 129 | public boolean getMoveLeft(){ 130 | return left; 131 | } 132 | 133 | /** 134 | * 滑动状态改变回调 135 | * @author zxy 136 | * 137 | */ 138 | public interface ChangeViewCallback{ 139 | /** 140 | * 切换视图 ?决定于left和right 。 141 | * @param left 142 | * @param right 143 | */ 144 | public void changeView(boolean left,boolean right); 145 | public void getCurrentPageIndex(int index); 146 | } 147 | 148 | /** 149 | * set ... 150 | * @param callback 151 | */ 152 | public void setChangeViewCallback(ChangeViewCallback callback){ 153 | changeViewCallback = callback; 154 | } 155 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/lumyuan/ux/ui/fragments/BlankFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumyuan.ux.ui.fragments 2 | 3 | import android.animation.ObjectAnimator 4 | import android.annotation.SuppressLint 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import androidx.fragment.app.Fragment 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.view.animation.AccelerateDecelerateInterpolator 14 | import android.widget.Toast 15 | import androidx.cardview.widget.CardView 16 | import io.github.lumyuan.ux.KTBasicActivity 17 | import io.github.lumyuan.ux.R 18 | import io.github.lumyuan.ux.core.animation.setOnFeedbackListener 19 | import io.github.lumyuan.ux.core.animation.setOnTouchAnimationToRotation 20 | import io.github.lumyuan.ux.core.common.dip2px 21 | import io.github.lumyuan.ux.databinding.FragmentBlankBinding 22 | import java.util.Random 23 | 24 | // TODO: Rename parameter arguments, choose names that match 25 | // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER 26 | private const val ARG_PARAM1 = "param1" 27 | private const val ARG_PARAM2 = "param2" 28 | 29 | /** 30 | * A simple [Fragment] subclass. 31 | * Use the [BlankFragment.newInstance] factory method to 32 | * create an instance of this fragment. 33 | */ 34 | class BlankFragment : Fragment() { 35 | // TODO: Rename and change types of parameters 36 | private var param1: String? = null 37 | private var param2: String? = null 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | arguments?.let { 42 | param1 = it.getString(ARG_PARAM1) 43 | param2 = it.getString(ARG_PARAM2) 44 | } 45 | } 46 | 47 | private lateinit var binding: FragmentBlankBinding 48 | @SuppressLint("SetTextI18n") 49 | override fun onCreateView( 50 | inflater: LayoutInflater, container: ViewGroup?, 51 | savedInstanceState: Bundle? 52 | ): View? { 53 | binding = FragmentBlankBinding.inflate(layoutInflater) 54 | //触摸动画 55 | binding.blurCard.setOnTouchAnimationToRotation(20f) 56 | //点击反馈事件 57 | binding.circularFlow.setOnFeedbackListener( 58 | callOnLongClick = true, 59 | onLongClick = { 60 | Toast.makeText(it.context, "onLongClick: ${binding.circularFlow.getProgress()}", Toast.LENGTH_SHORT).show() 61 | } 62 | ) { 63 | Toast.makeText(it.context, "${binding.circularFlow.getProgress()}", Toast.LENGTH_SHORT).show() 64 | } 65 | 66 | val rt = object : Runnable { 67 | override fun run() { 68 | val random = Random() 69 | ObjectAnimator.ofFloat(binding.moveCard, "translationX", random.nextInt(400).toFloat() * (random.nextInt(3) - 1)).apply{ 70 | this.duration = d 71 | interpolator = AccelerateDecelerateInterpolator() 72 | }.start() 73 | ObjectAnimator.ofFloat(binding.moveCard, "translationY", random.nextInt(400).toFloat() * (random.nextInt(3) - 1)).apply { 74 | duration = d 75 | interpolator = AccelerateDecelerateInterpolator() 76 | }.start() 77 | val progress = random.nextInt(101).toFloat() 78 | binding.circularFlow.setProgress(progress) 79 | binding.seekbar.progress = progress 80 | handler.postDelayed(this, d) 81 | } 82 | 83 | } 84 | handler.removeCallbacks(rt) 85 | handler.post(rt) 86 | binding.startButton.setOnClickListener { 87 | activity?.apply { 88 | startActivity(Intent(this, KTBasicActivity::class.java)) 89 | } 90 | } 91 | 92 | //设置拖动监听 93 | binding.seekbar.setOnSeekBarChangeListener { _, progress -> 94 | binding.seekbarProgressText.text = "拖动条进度:${String.format("%.2f", progress)}" 95 | } 96 | return binding.root 97 | } 98 | 99 | companion object { 100 | /** 101 | * Use this factory method to create a new instance of 102 | * this fragment using the provided parameters. 103 | * 104 | * @param param1 Parameter 1. 105 | * @param param2 Parameter 2. 106 | * @return A new instance of fragment BlankFragment. 107 | */ 108 | // TODO: Rename and change types and number of parameters 109 | @JvmStatic 110 | fun newInstance(param1: String, param2: String) = 111 | BlankFragment().apply { 112 | arguments = Bundle().apply { 113 | putString(ARG_PARAM1, param1) 114 | putString(ARG_PARAM2, param2) 115 | } 116 | } 117 | } 118 | 119 | private val d = 4000L 120 | 121 | private val handler = Handler(Looper.getMainLooper()) 122 | 123 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mine.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_module.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_kt_basic.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_blank.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 17 | 20 | 21 | 28 | 29 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 61 | 62 | 75 | 76 | 77 | 78 |