├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── freddy │ │ │ └── silhouette │ │ │ └── example │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── freddy │ │ │ └── silhouette │ │ │ └── example │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── freddy │ │ └── silhouette │ │ └── example │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── silhouette ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── freddy │ │ │ │ └── silhouette │ │ │ │ ├── ext │ │ │ │ ├── ViewExt.kt │ │ │ │ └── DensityExt.kt │ │ │ │ ├── utils │ │ │ │ ├── ViewUtil.kt │ │ │ │ └── DensityUtil.kt │ │ │ │ ├── config │ │ │ │ └── ViewConfig.kt │ │ │ │ └── widget │ │ │ │ ├── button │ │ │ │ ├── SleImageButton.kt │ │ │ │ └── SleTextButton.kt │ │ │ │ └── layout │ │ │ │ ├── SleFrameLayout.kt │ │ │ │ ├── SleLinearLayout.kt │ │ │ │ ├── SleRelativeLayout.kt │ │ │ │ └── SleConstraintLayout.kt │ │ └── res │ │ │ └── values │ │ │ └── sle-attrs.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── freddy │ │ │ └── silhouette │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── freddy │ │ └── silhouette │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml └── gradle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── gradlew ├── LICENSE ├── Silhouette——更方便的ShapeSelector实现方案.md └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /silhouette/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /silhouette/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Silhouette 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/Silhouette/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /silhouette/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 07 05:47:04 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "Silhouette" 10 | include ':app' 11 | include ':silhouette' 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /silhouette/src/test/java/com/freddy/silhouette/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette 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/src/test/java/com/freddy/silhouette/example/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.example 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 | } -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/ext/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.ext 2 | 3 | import android.view.View 4 | import com.freddy.silhouette.utils.ViewUtil 5 | 6 | /** 7 | * 8 | * @author: FreddyChen 9 | * @date : 2022/02/07 06:02 10 | * @email : freddychencsc@gmail.com 11 | */ 12 | fun View.expandViewTouchArea(size: Int = 10.0f.dp) { 13 | ViewUtil.expandViewTouchArea(this, size = size) 14 | } 15 | 16 | fun View.expandViewTouchArea(left: Int, top: Int, right: Int, bottom: Int) { 17 | ViewUtil.expandViewTouchArea(this, left = left, top = top, right = right, bottom = bottom) 18 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /silhouette/src/androidTest/java/com/freddy/silhouette/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette 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.freddy.silhouette.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/freddy/silhouette/example/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.example 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.freddy.silhouette.example", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /silhouette/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/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/ext/DensityExt.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.ext 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import androidx.fragment.app.Fragment 5 | import com.freddy.silhouette.utils.DensityUtil 6 | 7 | /** 8 | * 9 | * @author: FreddyChen 10 | * @date : 2022/02/07 05:57 11 | * @email : freddychencsc@gmail.com 12 | */ 13 | val Float.dp 14 | get() = DensityUtil.dp2px(this) 15 | 16 | val Float.sp 17 | get() = DensityUtil.sp2px(this) 18 | 19 | val Int.dp 20 | get() = DensityUtil.dp2px(toFloat()) 21 | 22 | val Int.sp 23 | get() = DensityUtil.sp2px(toFloat()) 24 | 25 | fun AppCompatActivity.getScreenWidth(): Int = DensityUtil.getScreenWidth(this.applicationContext) 26 | fun AppCompatActivity.getScreenHeight(): Int = DensityUtil.getScreenWidth(this.applicationContext) 27 | 28 | fun Fragment.getScreenWidth(): Int = DensityUtil.getScreenWidth(this.requireContext()) 29 | fun Fragment.getScreenHeight(): Int = DensityUtil.getScreenHeight(this.requireContext()) -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/utils/ViewUtil.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.utils 2 | 3 | import android.graphics.Rect 4 | import android.view.TouchDelegate 5 | import android.view.View 6 | import com.freddy.silhouette.ext.dp 7 | 8 | /** 9 | * 10 | * @author: FreddyChen 11 | * @date : 2022/02/07 06:02 12 | * @email : freddychencsc@gmail.com 13 | */ 14 | object ViewUtil { 15 | 16 | fun expandViewTouchArea(v: View, size: Int = 10.0f.dp) { 17 | this.expandViewTouchArea(v, size, size, size, size) 18 | } 19 | 20 | fun expandViewTouchArea(v: View, left: Int, top: Int, right: Int, bottom: Int) { 21 | if (v.parent == null) { 22 | return 23 | } 24 | 25 | (v.parent as View).post { 26 | val bounds = Rect() 27 | v.isEnabled = true 28 | v.getHitRect(bounds) 29 | bounds.top -= top 30 | bounds.bottom += bottom 31 | bounds.left -= left 32 | bounds.right += right 33 | val touchDelegate = TouchDelegate(bounds, v) 34 | if (View::class.java.isInstance(v.parent)) { 35 | (v.parent as View).touchDelegate = touchDelegate 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/src/main/java/com/freddy/silhouette/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.example 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.freddy.silhouette.widget.button.SleImageButton 6 | import com.freddy.silhouette.widget.button.SleTextButton 7 | import com.freddy.silhouette.widget.layout.SleConstraintLayout 8 | import com.freddy.silhouette.widget.layout.SleLinearLayout 9 | 10 | class MainActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_main) 15 | 16 | findViewById(R.id.stb_1).setOnClickListener { } 17 | findViewById(R.id.stb_2).setOnClickListener { } 18 | findViewById(R.id.stb_3).setOnClickListener { } 19 | findViewById(R.id.sib_1).setOnClickListener { } 20 | findViewById(R.id.sib_2).setOnClickListener { } 21 | findViewById(R.id.sib_3).setOnClickListener { } 22 | findViewById(R.id.scl_1).setOnClickListener { } 23 | findViewById(R.id.sll_1).setOnClickListener { } 24 | 25 | findViewById(R.id.stb_1).postDelayed(Runnable { 26 | findViewById(R.id.stb_1).isSelected = true 27 | }, 3000) 28 | } 29 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.freddy.silhouette.example" 11 | minSdk 19 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation 'androidx.core:core-ktx:1.7.0' 36 | implementation 'androidx.appcompat:appcompat:1.3.1' 37 | implementation 'com.google.android.material:material:1.4.0' 38 | implementation 'androidx.constraintlayout:constraintlayout:2.1.1' 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(':silhouette') 44 | // implementation 'io.github.freddychen:silhouette:0.0.2' 45 | } -------------------------------------------------------------------------------- /silhouette/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | minSdk 19 11 | targetSdk 31 12 | versionCode 1 13 | versionName "1.0" 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_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation 'androidx.core:core-ktx:1.7.0' 36 | implementation 'androidx.appcompat:appcompat:1.3.1' 37 | implementation 'com.google.android.material:material:1.4.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 | 43 | ext { 44 | PUBLISH_GROUP_ID = "io.github.freddychen" // 项目包名 45 | PUBLISH_ARTIFACT_ID = 'silhouette' // 项目名 46 | PUBLISH_VERSION = '0.0.3' // 版本号 47 | } 48 | apply from: './scripts/publish-mavencentral.gradle' -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/caches 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | /.idea/navEditor.xml 8 | /.idea/assetWizardSettings.xml 9 | .DS_Store 10 | /build 11 | /captures 12 | .cxx 13 | # Built application files 14 | *.apk 15 | *.aar 16 | *.ap_ 17 | *.aab 18 | 19 | # Files for the ART/Dalvik VM 20 | *.dex 21 | 22 | # Java class files 23 | *.class 24 | 25 | # Generated files 26 | bin/ 27 | gen/ 28 | out/ 29 | # Uncomment the following line in case you need and you don't have the release build type files in your app 30 | # release/ 31 | 32 | # Gradle files 33 | .gradle/ 34 | build/ 35 | 36 | # Local configuration file (sdk path, etc) 37 | local.properties 38 | 39 | # Proguard folder generated by Eclipse 40 | proguard/ 41 | 42 | # Log Files 43 | *.log 44 | 45 | # Android Studio Navigation editor temp files 46 | .navigation/ 47 | 48 | # Android Studio captures folder 49 | captures/ 50 | 51 | # IntelliJ 52 | *.iml 53 | .idea/workspace.xml 54 | .idea/tasks.xml 55 | .idea/gradle.xml 56 | .idea/assetWizardSettings.xml 57 | .idea/dictionaries 58 | .idea/libraries 59 | # Android Studio 3 in .gitignore file. 60 | .idea/caches 61 | .idea/modules.xml 62 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 63 | .idea/navEditor.xml 64 | .idea/misc.xml 65 | 66 | # Keystore files 67 | # Uncomment the following lines if you do not want to check your keystore files in. 68 | #*.jks 69 | #*.keystore 70 | 71 | # External native build folder generated in Android Studio 2.2 and later 72 | .externalNativeBuild 73 | .cxx/ 74 | 75 | # Google Services (e.g. APIs or Firebase) 76 | # google-services.json 77 | 78 | # Freeline 79 | freeline.py 80 | freeline/ 81 | freeline_project_description.json 82 | 83 | # fastlane 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots 87 | fastlane/test_output 88 | fastlane/readme.md 89 | 90 | # Version control 91 | vcs.xml 92 | 93 | # lint 94 | lint/intermediates/ 95 | lint/generated/ 96 | lint/outputs/ 97 | lint/tmp/ 98 | # lint/reports/ 99 | 100 | silhouette/scripts -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/utils/DensityUtil.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.utils 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Insets 6 | import android.os.Build 7 | import android.util.DisplayMetrics 8 | import android.util.TypedValue 9 | import android.view.WindowInsets 10 | import android.view.WindowManager 11 | import android.view.WindowMetrics 12 | 13 | /** 14 | * 15 | * @author: FreddyChen 16 | * @date : 2022/02/07 05:57 17 | * @email : freddychencsc@gmail.com 18 | */ 19 | object DensityUtil { 20 | 21 | /** 22 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 23 | * 24 | * @param dp 25 | * @return 26 | */ 27 | fun dp2px(dp: Float): Int { 28 | val scale = Resources.getSystem().displayMetrics.density 29 | return (dp * scale + 0.5f).toInt() 30 | } 31 | 32 | /** 33 | * sp转px 34 | * 35 | * @param sp 36 | * @return 37 | */ 38 | fun sp2px(sp: Float): Int { 39 | return TypedValue.applyDimension( 40 | TypedValue.COMPLEX_UNIT_SP, 41 | sp, Resources.getSystem().displayMetrics 42 | ).toInt() 43 | } 44 | 45 | /** 46 | * 获取屏幕宽度 47 | */ 48 | fun getScreenWidth(context: Context): Int { 49 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 50 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 51 | val windowMetrics: WindowMetrics = wm.currentWindowMetrics 52 | val insets: Insets = windowMetrics.windowInsets 53 | .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) 54 | windowMetrics.bounds.width() - insets.left - insets.right 55 | } else { 56 | val displayMetrics = DisplayMetrics() 57 | wm.defaultDisplay.getMetrics(displayMetrics) 58 | displayMetrics.widthPixels 59 | } 60 | } 61 | 62 | /** 63 | * 获取屏幕高度 64 | */ 65 | fun getScreenHeight(context: Context): Int { 66 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 67 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 68 | val windowMetrics: WindowMetrics = wm.currentWindowMetrics 69 | val insets: Insets = windowMetrics.windowInsets 70 | .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) 71 | windowMetrics.bounds.height() - insets.top - insets.bottom 72 | } else { 73 | val displayMetrics = DisplayMetrics() 74 | wm.defaultDisplay.getMetrics(displayMetrics) 75 | displayMetrics.heightPixels 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/config/ViewConfig.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.config 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.GradientDrawable 5 | import androidx.annotation.IntDef 6 | import com.freddy.silhouette.ext.dp 7 | 8 | /** 9 | * 10 | * @author: FreddyChen 11 | * @date : 2022/02/07 05:56 12 | * @email : freddychencsc@gmail.com 13 | */ 14 | const val TYPE_NONE = -1 15 | const val TYPE_MASK = 0 16 | const val TYPE_SELECTOR = 1 17 | @Target( 18 | AnnotationTarget.FIELD, 19 | AnnotationTarget.FUNCTION, 20 | AnnotationTarget.PROPERTY, 21 | AnnotationTarget.VALUE_PARAMETER 22 | ) 23 | @Retention(AnnotationRetention.SOURCE) 24 | @IntDef( 25 | TYPE_NONE, TYPE_MASK, TYPE_SELECTOR 26 | ) 27 | annotation class Type 28 | 29 | @Target( 30 | AnnotationTarget.FIELD, 31 | AnnotationTarget.FUNCTION, 32 | AnnotationTarget.PROPERTY, 33 | AnnotationTarget.VALUE_PARAMETER 34 | ) 35 | @Retention(AnnotationRetention.SOURCE) 36 | @IntDef( 37 | GradientDrawable.RECTANGLE, 38 | GradientDrawable.OVAL, 39 | GradientDrawable.LINE, 40 | GradientDrawable.RING 41 | ) 42 | annotation class Shape 43 | 44 | @Target( 45 | AnnotationTarget.FIELD, 46 | AnnotationTarget.FUNCTION, 47 | AnnotationTarget.PROPERTY, 48 | AnnotationTarget.VALUE_PARAMETER) 49 | @Retention(AnnotationRetention.SOURCE) 50 | @IntDef( 51 | GradientDrawable.LINEAR_GRADIENT, 52 | GradientDrawable.RADIAL_GRADIENT, 53 | GradientDrawable.SWEEP_GRADIENT 54 | ) 55 | annotation class GradientType 56 | 57 | const val INTERCEPT_TYPE_SUPER = 0 58 | const val INTERCEPT_TYPE_TRUE = 1 59 | const val INTERCEPT_TYPE_FALSE = 2 60 | @Target( 61 | AnnotationTarget.FIELD, 62 | AnnotationTarget.FUNCTION, 63 | AnnotationTarget.PROPERTY, 64 | AnnotationTarget.VALUE_PARAMETER 65 | ) 66 | @Retention(AnnotationRetention.SOURCE) 67 | @IntDef( 68 | INTERCEPT_TYPE_SUPER, 69 | INTERCEPT_TYPE_TRUE, 70 | INTERCEPT_TYPE_FALSE 71 | ) 72 | annotation class InterceptType 73 | 74 | const val GRADIENT_ORIENTATION_TOP_BOTTOM = 0 75 | const val GRADIENT_ORIENTATION_TR_BL = 1 76 | const val GRADIENT_ORIENTATION_RIGHT_LEFT = 2 77 | const val GRADIENT_ORIENTATION_BR_TL = 3 78 | const val GRADIENT_ORIENTATION_BOTTOM_TOP = 4 79 | const val GRADIENT_ORIENTATION_BL_TR = 5 80 | const val GRADIENT_ORIENTATION_LEFT_RIGHT = 6 81 | const val GRADIENT_ORIENTATION_TL_BR = 7 82 | 83 | const val ALPHA_NORMAL = 1.0f 84 | const val DEFAULT_ALPHA_PRESSED = ALPHA_NORMAL * 0.7f 85 | const val DEFAULT_ALPHA_DISABLED = ALPHA_NORMAL * 0.3f 86 | 87 | val DEFAULT_MASK_BACKGROUND_COLOR: Int by lazy { 88 | Color.parseColor("#1a000000") 89 | } 90 | val DEFAULT_DISABLE_BACKGROUND_COLOR: Int by lazy { 91 | Color.parseColor("#cccccc") 92 | } 93 | val DEFAULT_CANCEL_OFFSET by lazy { 94 | 8.dp 95 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 25 | 26 | 41 | 42 | 59 | 60 | 67 | 68 | 75 | 76 | 84 | 85 | 96 | 97 | 102 | 103 | 113 | 114 | 115 | 130 | 131 | 136 | 137 | 148 | 149 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/button/SleImageButton.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.button 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.StateListDrawable 5 | import android.util.AttributeSet 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import androidx.annotation.IntDef 9 | import androidx.annotation.IntRange 10 | import androidx.core.content.ContextCompat 11 | import com.freddy.silhouette.R 12 | import com.freddy.silhouette.config.* 13 | import com.google.android.material.imageview.ShapeableImageView 14 | import com.google.android.material.shape.CornerFamily 15 | import com.google.android.material.shape.RelativeCornerSize 16 | import com.google.android.material.shape.ShapeAppearanceModel 17 | import kotlin.properties.Delegates 18 | 19 | /** 20 | * 21 | * @author: FreddyChen 22 | * @date : 2022/02/07 06:05 23 | * @email : freddychencsc@gmail.com 24 | */ 25 | class SleImageButton : ShapeableImageView, View.OnClickListener, View.OnTouchListener { 26 | 27 | companion object { 28 | const val TYPE_MASK = 0 29 | const val TYPE_ALPHA = 1 30 | const val TYPE_SELECTOR = 2 31 | const val TYPE_CHECKBOX = 3 32 | 33 | const val STYLE_NORMAL = 0 34 | const val STYLE_ROUNDED = 1 35 | const val STYLE_OVAL = 2 36 | } 37 | 38 | @Target( 39 | AnnotationTarget.FIELD, 40 | AnnotationTarget.FUNCTION, 41 | AnnotationTarget.PROPERTY, 42 | AnnotationTarget.VALUE_PARAMETER 43 | ) 44 | @Retention(AnnotationRetention.SOURCE) 45 | @IntDef( 46 | STYLE_NORMAL, STYLE_ROUNDED, STYLE_OVAL 47 | ) 48 | annotation class Style 49 | 50 | @Target( 51 | AnnotationTarget.FIELD, 52 | AnnotationTarget.FUNCTION, 53 | AnnotationTarget.PROPERTY, 54 | AnnotationTarget.VALUE_PARAMETER 55 | ) 56 | @Retention(AnnotationRetention.SOURCE) 57 | @IntDef( 58 | TYPE_MASK, TYPE_ALPHA, TYPE_SELECTOR, TYPE_CHECKBOX 59 | ) 60 | annotation class Type 61 | 62 | @Type 63 | private var type: Int = TYPE_MASK 64 | var normalResId: Int = 0 65 | set(value) { 66 | if (value != 0) { 67 | setImageResource(value) 68 | } 69 | field = value 70 | } 71 | 72 | @Style 73 | private var style: Int = STYLE_NORMAL 74 | private var pressedResId: Int by Delegates.notNull() 75 | private var disabledResId: Int by Delegates.notNull() 76 | private var checkedResId: Int by Delegates.notNull() 77 | private var uncheckedResId: Int by Delegates.notNull() 78 | var isChecked = false 79 | set(value) { 80 | field = value 81 | if (type != TYPE_CHECKBOX) return 82 | isSelected = isChecked 83 | } 84 | private var pressedAlpha: Float by Delegates.notNull() 85 | private var disabledAlpha: Float by Delegates.notNull() 86 | private var maskBackgroundColor: Int by Delegates.notNull() 87 | private var cancelOffset: Int by Delegates.notNull() 88 | private var cornersRadius: Float = 0f 89 | private var cornersTopLeftRadius: Float = 0f 90 | private var cornersTopRightRadius: Float = 0f 91 | private var cornersBottomLeftRadius: Float = 0f 92 | private var cornersBottomRightRadius: Float = 0f 93 | 94 | constructor(context: Context) : this(context, null) 95 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 96 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 97 | context, 98 | attrs, 99 | defStyleAttr 100 | ) { 101 | context.obtainStyledAttributes(attrs, R.styleable.SleImageButton, defStyleAttr, 0).apply { 102 | type = getInt(R.styleable.SleImageButton_sle_ib_type, TYPE_MASK) 103 | style = getInt(R.styleable.SleImageButton_sle_ib_style, STYLE_NORMAL) 104 | normalResId = getResourceId(R.styleable.SleImageButton_sle_normalResId, 0) 105 | pressedResId = getResourceId(R.styleable.SleImageButton_sle_pressedResId, 0) 106 | disabledResId = getResourceId(R.styleable.SleImageButton_sle_disabledResId, 0) 107 | checkedResId = getResourceId(R.styleable.SleImageButton_sle_checkedResId, 0) 108 | uncheckedResId = getResourceId(R.styleable.SleImageButton_sle_uncheckedResId, 0) 109 | isChecked = getBoolean(R.styleable.SleImageButton_sle_isChecked, false) 110 | pressedAlpha = 111 | getFloat(R.styleable.SleImageButton_sle_pressedAlpha, DEFAULT_ALPHA_PRESSED) 112 | disabledAlpha = 113 | getFloat(R.styleable.SleImageButton_sle_disabledAlpha, DEFAULT_ALPHA_DISABLED) 114 | maskBackgroundColor = getColor( 115 | R.styleable.SleImageButton_sle_maskBackgroundColor, 116 | DEFAULT_MASK_BACKGROUND_COLOR 117 | ) 118 | cancelOffset = getDimensionPixelSize( 119 | R.styleable.SleImageButton_sle_cancelOffset, 120 | DEFAULT_CANCEL_OFFSET 121 | ) 122 | cornersRadius = getDimension(R.styleable.SleImageButton_sle_cornersRadius, 0f) 123 | cornersTopLeftRadius = 124 | getDimension(R.styleable.SleImageButton_sle_cornersTopLeftRadius, 0f) 125 | cornersTopRightRadius = 126 | getDimension(R.styleable.SleImageButton_sle_cornersTopRightRadius, 0f) 127 | cornersBottomLeftRadius = 128 | getDimension(R.styleable.SleImageButton_sle_cornersBottomLeftRadius, 0f) 129 | cornersBottomRightRadius = 130 | getDimension(R.styleable.SleImageButton_sle_cornersBottomRightRadius, 0f) 131 | recycle() 132 | } 133 | init() 134 | } 135 | 136 | private fun init() { 137 | initType() 138 | setStyle(style) 139 | setOnTouchListener(this) 140 | if (type == TYPE_CHECKBOX) { 141 | setOnClickListener(this) 142 | } 143 | } 144 | 145 | private fun initType() { 146 | when (type) { 147 | TYPE_MASK, TYPE_ALPHA -> { 148 | if (!isEnabled) { 149 | alpha = disabledAlpha 150 | } 151 | } 152 | TYPE_SELECTOR -> { 153 | if (!isEnabled) { 154 | setImageResource(disabledResId) 155 | } 156 | } 157 | TYPE_CHECKBOX -> { 158 | val sld = StateListDrawable() 159 | sld.addState( 160 | intArrayOf(android.R.attr.state_selected), 161 | ContextCompat.getDrawable(context, checkedResId) 162 | ) 163 | sld.addState( 164 | intArrayOf(-android.R.attr.state_selected), 165 | ContextCompat.getDrawable(context, uncheckedResId) 166 | ) 167 | background = sld 168 | isSelected = isChecked 169 | } 170 | } 171 | } 172 | 173 | fun setStyle(@Style style: Int) { 174 | this.style = style 175 | when (style) { 176 | STYLE_NORMAL -> { 177 | shapeAppearanceModel = ShapeAppearanceModel.Builder() 178 | .setAllCorners(CornerFamily.ROUNDED, 0f) 179 | .build() 180 | } 181 | STYLE_ROUNDED -> { 182 | shapeAppearanceModel = if (cornersRadius > 0f) { 183 | ShapeAppearanceModel.Builder() 184 | .setAllCorners(CornerFamily.ROUNDED, cornersRadius) 185 | .build() 186 | } else { 187 | ShapeAppearanceModel.Builder() 188 | .setTopLeftCorner(CornerFamily.ROUNDED, cornersTopLeftRadius) 189 | .setTopRightCorner(CornerFamily.ROUNDED, cornersTopRightRadius) 190 | .setBottomLeftCorner(CornerFamily.ROUNDED, cornersBottomLeftRadius) 191 | .setBottomRightCorner(CornerFamily.ROUNDED, cornersBottomRightRadius) 192 | .build() 193 | } 194 | } 195 | STYLE_OVAL -> { 196 | shapeAppearanceModel = ShapeAppearanceModel.builder() 197 | .setAllCornerSizes(RelativeCornerSize(0.5f)) 198 | .build() 199 | } 200 | } 201 | } 202 | 203 | override fun onClick(v: View?) { 204 | when (type) { 205 | TYPE_CHECKBOX -> { 206 | isChecked = !isChecked 207 | isSelected = isChecked 208 | onCheckedChangedListener?.invoke(isChecked) 209 | } 210 | } 211 | } 212 | 213 | override fun onTouch(v: View, event: MotionEvent): Boolean { 214 | if (!isEnabled || !isClickable || type == TYPE_NONE) { 215 | return false 216 | } 217 | when (event.action) { 218 | MotionEvent.ACTION_DOWN -> { 219 | when (type) { 220 | TYPE_MASK -> { 221 | setColorFilter(maskBackgroundColor) 222 | } 223 | TYPE_ALPHA -> { 224 | alpha = pressedAlpha 225 | } 226 | TYPE_SELECTOR -> { 227 | if (pressedResId != 0) { 228 | setImageResource(pressedResId) 229 | } 230 | } 231 | } 232 | } 233 | 234 | MotionEvent.ACTION_MOVE -> { 235 | val currentX = event.x 236 | val currentY = event.y 237 | if (currentX < (0 - cancelOffset) || currentX > (width + cancelOffset) || currentY < (0 - cancelOffset) || currentY > (height + cancelOffset)) { 238 | when (type) { 239 | TYPE_MASK -> { 240 | clearColorFilter() 241 | } 242 | TYPE_ALPHA -> { 243 | alpha = ALPHA_NORMAL 244 | } 245 | TYPE_SELECTOR -> { 246 | if (normalResId != 0) { 247 | setImageResource(normalResId) 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 255 | when (type) { 256 | TYPE_MASK -> { 257 | clearColorFilter() 258 | } 259 | TYPE_ALPHA -> { 260 | alpha = ALPHA_NORMAL 261 | } 262 | TYPE_SELECTOR -> { 263 | if (normalResId != 0) { 264 | setImageResource(normalResId) 265 | } 266 | } 267 | } 268 | } 269 | } 270 | return false 271 | } 272 | 273 | fun setType(@IntRange(from = TYPE_MASK.toLong(), to = TYPE_CHECKBOX.toLong()) type: Int) { 274 | this.type = type 275 | initType() 276 | } 277 | 278 | override fun setEnabled(enabled: Boolean) { 279 | when (type) { 280 | TYPE_ALPHA -> { 281 | alpha = if (enabled) ALPHA_NORMAL else disabledAlpha 282 | } 283 | TYPE_SELECTOR -> { 284 | if (enabled) { 285 | if (normalResId != 0) setImageResource(normalResId) 286 | } else { 287 | if (disabledResId != 0) setImageResource(disabledResId) 288 | } 289 | } 290 | } 291 | super.setEnabled(enabled) 292 | } 293 | 294 | override fun onWindowFocusChanged(hasWindowFocus: Boolean) { 295 | super.onWindowFocusChanged(hasWindowFocus) 296 | // 页面跳转后再回来,需要重设按钮透明度 297 | if (hasWindowFocus) isEnabled = isEnabled 298 | } 299 | 300 | override fun onAttachedToWindow() { 301 | super.onAttachedToWindow() 302 | isEnabled = isEnabled 303 | } 304 | 305 | private var onCheckedChangedListener: ((isChecked: Boolean) -> Unit)? = null 306 | fun setOnCheckedChangedListener(listener: (isChecked: Boolean) -> Unit) { 307 | this.onCheckedChangedListener = listener 308 | } 309 | } -------------------------------------------------------------------------------- /silhouette/src/main/res/values/sle-attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/layout/SleFrameLayout.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.layout 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.StateListDrawable 8 | import android.os.Build 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import android.widget.FrameLayout 12 | import com.freddy.silhouette.R 13 | import com.freddy.silhouette.config.* 14 | 15 | /** 16 | * 17 | * @author: FreddyChen 18 | * @date : 2022/02/07 06:16 19 | * @email : freddychencsc@gmail.com 20 | */ 21 | class SleFrameLayout : FrameLayout { 22 | 23 | @Type 24 | private var type: Int = TYPE_MASK 25 | 26 | @Shape 27 | private var shape: Int = GradientDrawable.RECTANGLE 28 | private var innerRadius: Int = 0 29 | private var innerRadiusRatio: Float = 0f 30 | private var thickness: Int = 0 31 | private var thicknessRatio: Float = 0f 32 | private var normalBackgroundColor: Int = 0 33 | private var pressedBackgroundColor: Int = 0 34 | private var disabledBackgroundColor: Int = 0 35 | private var selectedBackgroundColor: Int = 0 36 | private var strokeWidth: Int = 0 37 | private var dashWidth: Float = 0f 38 | private var dashGap: Float = 0f 39 | private var normalStrokeColor: Int = 0 40 | private var pressedStrokeColor: Int = 0 41 | private var disabledStrokeColor: Int = 0 42 | private var selectedStrokeColor: Int = 0 43 | private var cornersRadius: Float = 0f 44 | private var cornersTopLeftRadius: Float = 0f 45 | private var cornersTopRightRadius: Float = 0f 46 | private var cornersBottomLeftRadius: Float = 0f 47 | private var cornersBottomRightRadius: Float = 0f 48 | private var normalGradientColors: IntArray? = null 49 | private var pressedGradientColors: IntArray? = null 50 | private var disabledGradientColors: IntArray? = null 51 | private var selectedGradientColors: IntArray? = null 52 | private var gradientOrientation: Int = GRADIENT_ORIENTATION_TOP_BOTTOM 53 | 54 | @GradientType 55 | private var gradientType: Int = GradientDrawable.LINEAR_GRADIENT 56 | private var gradientCenterX: Float = 0f 57 | private var gradientCenterY: Float = 0f 58 | private var gradientRadius: Float = 0f 59 | 60 | private var maskBackgroundColor: Int = DEFAULT_MASK_BACKGROUND_COLOR 61 | 62 | @InterceptType 63 | private var interceptType: Int = INTERCEPT_TYPE_SUPER 64 | 65 | constructor(context: Context) : this(context, null) 66 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 67 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 68 | context, 69 | attrs, 70 | defStyleAttr 71 | ) { 72 | context.obtainStyledAttributes(attrs, R.styleable.SleFrameLayout, defStyleAttr, 0) 73 | .apply { 74 | type = getInt(R.styleable.SleFrameLayout_sle_type, TYPE_MASK) 75 | shape = 76 | getInt(R.styleable.SleFrameLayout_sle_shape, GradientDrawable.RECTANGLE) 77 | innerRadius = 78 | getDimensionPixelSize(R.styleable.SleFrameLayout_sle_innerRadius, 0) 79 | innerRadiusRatio = 80 | getFloat(R.styleable.SleFrameLayout_sle_innerRadiusRatio, 0f) 81 | thickness = 82 | getDimensionPixelSize(R.styleable.SleFrameLayout_sle_thickness, 0) 83 | thicknessRatio = getFloat(R.styleable.SleFrameLayout_sle_thicknessRatio, 0f) 84 | normalBackgroundColor = 85 | getColor(R.styleable.SleFrameLayout_sle_normalBackgroundColor, 0) 86 | pressedBackgroundColor = 87 | getColor(R.styleable.SleFrameLayout_sle_pressedBackgroundColor, 0) 88 | disabledBackgroundColor = 89 | getColor( 90 | R.styleable.SleFrameLayout_sle_disabledBackgroundColor, 91 | DEFAULT_DISABLE_BACKGROUND_COLOR 92 | ) 93 | selectedBackgroundColor = 94 | getColor(R.styleable.SleFrameLayout_sle_selectedBackgroundColor, 0) 95 | strokeWidth = 96 | getDimensionPixelSize(R.styleable.SleFrameLayout_sle_strokeWidth, 0) 97 | dashWidth = getDimension(R.styleable.SleFrameLayout_sle_dashWidth, 0f) 98 | dashGap = getDimension(R.styleable.SleFrameLayout_sle_dashGap, 0f) 99 | normalStrokeColor = 100 | getColor(R.styleable.SleFrameLayout_sle_normalStrokeColor, 0) 101 | pressedStrokeColor = 102 | getColor( 103 | R.styleable.SleFrameLayout_sle_pressedStrokeColor, 104 | normalStrokeColor 105 | ) 106 | disabledStrokeColor = 107 | getColor( 108 | R.styleable.SleFrameLayout_sle_disabledStrokeColor, 109 | normalStrokeColor 110 | ) 111 | selectedStrokeColor = 112 | getColor( 113 | R.styleable.SleFrameLayout_sle_selectedStrokeColor, 114 | normalStrokeColor 115 | ) 116 | cornersRadius = 117 | getDimension(R.styleable.SleFrameLayout_sle_cornersRadius, 0f) 118 | cornersTopLeftRadius = 119 | getDimension(R.styleable.SleFrameLayout_sle_cornersTopLeftRadius, 0f) 120 | cornersTopRightRadius = 121 | getDimension(R.styleable.SleFrameLayout_sle_cornersTopRightRadius, 0f) 122 | cornersBottomLeftRadius = 123 | getDimension(R.styleable.SleFrameLayout_sle_cornersBottomLeftRadius, 0f) 124 | cornersBottomRightRadius = 125 | getDimension(R.styleable.SleFrameLayout_sle_cornersBottomRightRadius, 0f) 126 | val normalGradientColorsResourceId = 127 | getResourceId(R.styleable.SleFrameLayout_sle_normalGradientColors, 0) 128 | if (normalGradientColorsResourceId != 0) { 129 | normalGradientColors = resources.getIntArray(normalGradientColorsResourceId) 130 | } 131 | val pressedGradientColorsResourceId = 132 | getResourceId(R.styleable.SleFrameLayout_sle_pressedGradientColors, 0) 133 | if (pressedGradientColorsResourceId != 0) { 134 | pressedGradientColors = resources.getIntArray(pressedGradientColorsResourceId) 135 | } 136 | val disabledGradientColorsResourceId = 137 | getResourceId(R.styleable.SleFrameLayout_sle_disabledGradientColors, 0) 138 | if (disabledGradientColorsResourceId != 0) { 139 | disabledGradientColors = resources.getIntArray(disabledGradientColorsResourceId) 140 | } 141 | val selectedGradientColorsResourceId = 142 | getResourceId(R.styleable.SleFrameLayout_sle_selectedGradientColors, 0) 143 | if (selectedGradientColorsResourceId != 0) { 144 | selectedGradientColors = resources.getIntArray(selectedGradientColorsResourceId) 145 | } 146 | gradientOrientation = getInt( 147 | R.styleable.SleFrameLayout_sle_gradientOrientation, 148 | GRADIENT_ORIENTATION_TOP_BOTTOM 149 | ) 150 | gradientType = 151 | getInt( 152 | R.styleable.SleFrameLayout_sle_gradientType, 153 | GradientDrawable.LINEAR_GRADIENT 154 | ) 155 | gradientCenterX = 156 | getDimension(R.styleable.SleFrameLayout_sle_gradientCenterX, 0f) 157 | gradientCenterY = 158 | getDimension(R.styleable.SleFrameLayout_sle_gradientCenterY, 0f) 159 | gradientRadius = 160 | getDimension(R.styleable.SleFrameLayout_sle_gradientRadius, 0f) 161 | maskBackgroundColor = getColor( 162 | R.styleable.SleFrameLayout_sle_maskBackgroundColor, 163 | DEFAULT_MASK_BACKGROUND_COLOR 164 | ) 165 | interceptType = 166 | getInt( 167 | R.styleable.SleFrameLayout_sle_interceptType, 168 | INTERCEPT_TYPE_SUPER 169 | ) 170 | recycle() 171 | } 172 | init() 173 | } 174 | 175 | private fun init() { 176 | val normalDrawable = 177 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 178 | var pressedDrawable: GradientDrawable? = null 179 | var disabledDrawable: GradientDrawable? = null 180 | val selectedDrawable: GradientDrawable? 181 | when (type) { 182 | TYPE_MASK -> { 183 | pressedDrawable = getDrawable( 184 | normalBackgroundColor, 185 | normalStrokeColor, 186 | normalGradientColors 187 | ).apply { 188 | colorFilter = 189 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 190 | } 191 | disabledDrawable = 192 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 193 | } 194 | TYPE_SELECTOR -> { 195 | pressedDrawable = 196 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 197 | disabledDrawable = getDrawable( 198 | disabledBackgroundColor, 199 | disabledStrokeColor, 200 | disabledGradientColors 201 | ) 202 | } 203 | } 204 | selectedDrawable = getDrawable( 205 | selectedBackgroundColor, 206 | selectedStrokeColor, 207 | selectedGradientColors 208 | ) 209 | background = StateListDrawable().apply { 210 | if(type != TYPE_NONE) { 211 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 212 | } 213 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 214 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 215 | addState(intArrayOf(), normalDrawable) 216 | } 217 | } 218 | 219 | private fun getDrawable( 220 | backgroundColor: Int, 221 | strokeColor: Int, 222 | gradientColors: IntArray? = null 223 | ): GradientDrawable { 224 | // 背景色相关 225 | val drawable = GradientDrawable() 226 | setupColor(drawable, backgroundColor) 227 | 228 | // 形状相关 229 | (drawable.mutate() as GradientDrawable).shape = shape 230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 231 | drawable.innerRadius = innerRadius 232 | if (innerRadiusRatio > 0f) { 233 | drawable.innerRadiusRatio = innerRadiusRatio 234 | } 235 | drawable.thickness = thickness 236 | if (thicknessRatio > 0f) { 237 | drawable.thicknessRatio = thicknessRatio 238 | } 239 | } 240 | 241 | // 描边相关 242 | if (strokeColor != 0) { 243 | (drawable.mutate() as GradientDrawable).setStroke( 244 | strokeWidth, 245 | strokeColor, 246 | dashWidth, 247 | dashGap 248 | ) 249 | } 250 | 251 | // 圆角相关 252 | if (cornersRadius != 0.0f) { 253 | (drawable.mutate() as GradientDrawable).cornerRadius = cornersRadius 254 | } else { 255 | // 指定4个角点中每个角点的半径。对于每个角点,数组 256 | // 包含两个值,X半径,Y半径 257 | // 顺序为左上角、右上角、右下角、左下角 258 | (drawable.mutate() as GradientDrawable).cornerRadii = floatArrayOf( 259 | cornersTopLeftRadius, 260 | cornersTopLeftRadius, 261 | 262 | cornersTopRightRadius, 263 | cornersTopRightRadius, 264 | 265 | cornersBottomRightRadius, 266 | cornersBottomRightRadius, 267 | 268 | cornersBottomLeftRadius, 269 | cornersBottomLeftRadius, 270 | ) 271 | } 272 | 273 | // 渐变相关 274 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 275 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 276 | (drawable.mutate() as GradientDrawable).setGradientCenter( 277 | gradientCenterX, 278 | gradientCenterY 279 | ) 280 | } 281 | gradientColors?.let { colors -> 282 | (drawable.mutate() as GradientDrawable).colors = colors 283 | } 284 | var orientation: GradientDrawable.Orientation? = null 285 | when (gradientOrientation) { 286 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 287 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 288 | } 289 | GRADIENT_ORIENTATION_TR_BL -> { 290 | orientation = GradientDrawable.Orientation.TR_BL 291 | } 292 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 293 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 294 | } 295 | GRADIENT_ORIENTATION_BR_TL -> { 296 | orientation = GradientDrawable.Orientation.BR_TL 297 | } 298 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 299 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 300 | } 301 | GRADIENT_ORIENTATION_BL_TR -> { 302 | orientation = GradientDrawable.Orientation.BL_TR 303 | } 304 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 305 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 306 | } 307 | GRADIENT_ORIENTATION_TL_BR -> { 308 | orientation = GradientDrawable.Orientation.TL_BR 309 | } 310 | } 311 | orientation?.apply { 312 | (drawable.mutate() as GradientDrawable).orientation = this 313 | } 314 | return drawable 315 | } 316 | 317 | private fun setupColor(drawable: GradientDrawable, backgroundColor: Int) { 318 | if (backgroundColor != 0) { 319 | (drawable.mutate() as GradientDrawable).setColor(backgroundColor) 320 | } 321 | } 322 | 323 | fun setInterceptType(@InterceptType interceptType: Int) { 324 | this.interceptType = interceptType 325 | } 326 | 327 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 328 | return when (interceptType) { 329 | INTERCEPT_TYPE_TRUE -> { 330 | true 331 | } 332 | INTERCEPT_TYPE_FALSE -> { 333 | false 334 | } 335 | else -> { 336 | return super.onInterceptTouchEvent(ev) 337 | } 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/layout/SleLinearLayout.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.layout 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.StateListDrawable 8 | import android.os.Build 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import android.widget.LinearLayout 12 | import com.freddy.silhouette.R 13 | import com.freddy.silhouette.config.* 14 | 15 | /** 16 | * 17 | * @author: FreddyChen 18 | * @date : 2022/02/07 06:16 19 | * @email : freddychencsc@gmail.com 20 | */ 21 | class SleLinearLayout : LinearLayout { 22 | 23 | @Type 24 | private var type: Int = TYPE_MASK 25 | 26 | @Shape 27 | private var shape: Int = GradientDrawable.RECTANGLE 28 | private var innerRadius: Int = 0 29 | private var innerRadiusRatio: Float = 0f 30 | private var thickness: Int = 0 31 | private var thicknessRatio: Float = 0f 32 | private var normalBackgroundColor: Int = 0 33 | private var pressedBackgroundColor: Int = 0 34 | private var disabledBackgroundColor: Int = 0 35 | private var selectedBackgroundColor: Int = 0 36 | private var strokeWidth: Int = 0 37 | private var dashWidth: Float = 0f 38 | private var dashGap: Float = 0f 39 | private var normalStrokeColor: Int = 0 40 | private var pressedStrokeColor: Int = 0 41 | private var disabledStrokeColor: Int = 0 42 | private var selectedStrokeColor: Int = 0 43 | private var cornersRadius: Float = 0f 44 | private var cornersTopLeftRadius: Float = 0f 45 | private var cornersTopRightRadius: Float = 0f 46 | private var cornersBottomLeftRadius: Float = 0f 47 | private var cornersBottomRightRadius: Float = 0f 48 | private var normalGradientColors: IntArray? = null 49 | private var pressedGradientColors: IntArray? = null 50 | private var disabledGradientColors: IntArray? = null 51 | private var selectedGradientColors: IntArray? = null 52 | private var gradientOrientation: Int = GRADIENT_ORIENTATION_TOP_BOTTOM 53 | 54 | @GradientType 55 | private var gradientType: Int = GradientDrawable.LINEAR_GRADIENT 56 | private var gradientCenterX: Float = 0f 57 | private var gradientCenterY: Float = 0f 58 | private var gradientRadius: Float = 0f 59 | 60 | private var maskBackgroundColor: Int = DEFAULT_MASK_BACKGROUND_COLOR 61 | 62 | @InterceptType 63 | private var interceptType: Int = INTERCEPT_TYPE_SUPER 64 | 65 | constructor(context: Context) : this(context, null) 66 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 67 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 68 | context, 69 | attrs, 70 | defStyleAttr 71 | ) { 72 | context.obtainStyledAttributes(attrs, R.styleable.SleLinearLayout, defStyleAttr, 0) 73 | .apply { 74 | type = getInt(R.styleable.SleLinearLayout_sle_type, TYPE_MASK) 75 | shape = 76 | getInt(R.styleable.SleLinearLayout_sle_shape, GradientDrawable.RECTANGLE) 77 | innerRadius = 78 | getDimensionPixelSize(R.styleable.SleLinearLayout_sle_innerRadius, 0) 79 | innerRadiusRatio = 80 | getFloat(R.styleable.SleLinearLayout_sle_innerRadiusRatio, 0f) 81 | thickness = 82 | getDimensionPixelSize(R.styleable.SleLinearLayout_sle_thickness, 0) 83 | thicknessRatio = getFloat(R.styleable.SleLinearLayout_sle_thicknessRatio, 0f) 84 | normalBackgroundColor = 85 | getColor(R.styleable.SleLinearLayout_sle_normalBackgroundColor, 0) 86 | pressedBackgroundColor = 87 | getColor(R.styleable.SleLinearLayout_sle_pressedBackgroundColor, 0) 88 | disabledBackgroundColor = 89 | getColor( 90 | R.styleable.SleLinearLayout_sle_disabledBackgroundColor, 91 | DEFAULT_DISABLE_BACKGROUND_COLOR 92 | ) 93 | selectedBackgroundColor = 94 | getColor(R.styleable.SleLinearLayout_sle_selectedBackgroundColor, 0) 95 | strokeWidth = 96 | getDimensionPixelSize(R.styleable.SleLinearLayout_sle_strokeWidth, 0) 97 | dashWidth = getDimension(R.styleable.SleLinearLayout_sle_dashWidth, 0f) 98 | dashGap = getDimension(R.styleable.SleLinearLayout_sle_dashGap, 0f) 99 | normalStrokeColor = 100 | getColor(R.styleable.SleLinearLayout_sle_normalStrokeColor, 0) 101 | pressedStrokeColor = 102 | getColor( 103 | R.styleable.SleLinearLayout_sle_pressedStrokeColor, 104 | normalStrokeColor 105 | ) 106 | disabledStrokeColor = 107 | getColor( 108 | R.styleable.SleLinearLayout_sle_disabledStrokeColor, 109 | normalStrokeColor 110 | ) 111 | selectedStrokeColor = 112 | getColor( 113 | R.styleable.SleLinearLayout_sle_selectedStrokeColor, 114 | normalStrokeColor 115 | ) 116 | cornersRadius = 117 | getDimension(R.styleable.SleLinearLayout_sle_cornersRadius, 0f) 118 | cornersTopLeftRadius = 119 | getDimension(R.styleable.SleLinearLayout_sle_cornersTopLeftRadius, 0f) 120 | cornersTopRightRadius = 121 | getDimension(R.styleable.SleLinearLayout_sle_cornersTopRightRadius, 0f) 122 | cornersBottomLeftRadius = 123 | getDimension(R.styleable.SleLinearLayout_sle_cornersBottomLeftRadius, 0f) 124 | cornersBottomRightRadius = 125 | getDimension(R.styleable.SleLinearLayout_sle_cornersBottomRightRadius, 0f) 126 | val normalGradientColorsResourceId = 127 | getResourceId(R.styleable.SleLinearLayout_sle_normalGradientColors, 0) 128 | if (normalGradientColorsResourceId != 0) { 129 | normalGradientColors = resources.getIntArray(normalGradientColorsResourceId) 130 | } 131 | val pressedGradientColorsResourceId = 132 | getResourceId(R.styleable.SleLinearLayout_sle_pressedGradientColors, 0) 133 | if (pressedGradientColorsResourceId != 0) { 134 | pressedGradientColors = resources.getIntArray(pressedGradientColorsResourceId) 135 | } 136 | val disabledGradientColorsResourceId = 137 | getResourceId(R.styleable.SleLinearLayout_sle_disabledGradientColors, 0) 138 | if (disabledGradientColorsResourceId != 0) { 139 | disabledGradientColors = resources.getIntArray(disabledGradientColorsResourceId) 140 | } 141 | val selectedGradientColorsResourceId = 142 | getResourceId(R.styleable.SleLinearLayout_sle_selectedGradientColors, 0) 143 | if (selectedGradientColorsResourceId != 0) { 144 | selectedGradientColors = resources.getIntArray(selectedGradientColorsResourceId) 145 | } 146 | gradientOrientation = getInt( 147 | R.styleable.SleLinearLayout_sle_gradientOrientation, 148 | GRADIENT_ORIENTATION_TOP_BOTTOM 149 | ) 150 | gradientType = 151 | getInt( 152 | R.styleable.SleLinearLayout_sle_gradientType, 153 | GradientDrawable.LINEAR_GRADIENT 154 | ) 155 | gradientCenterX = 156 | getDimension(R.styleable.SleLinearLayout_sle_gradientCenterX, 0f) 157 | gradientCenterY = 158 | getDimension(R.styleable.SleLinearLayout_sle_gradientCenterY, 0f) 159 | gradientRadius = 160 | getDimension(R.styleable.SleLinearLayout_sle_gradientRadius, 0f) 161 | maskBackgroundColor = getColor( 162 | R.styleable.SleLinearLayout_sle_maskBackgroundColor, 163 | DEFAULT_MASK_BACKGROUND_COLOR 164 | ) 165 | interceptType = 166 | getInt( 167 | R.styleable.SleLinearLayout_sle_interceptType, 168 | INTERCEPT_TYPE_SUPER 169 | ) 170 | recycle() 171 | } 172 | init() 173 | } 174 | 175 | private fun init() { 176 | val normalDrawable = 177 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 178 | var pressedDrawable: GradientDrawable? = null 179 | var disabledDrawable: GradientDrawable? = null 180 | val selectedDrawable: GradientDrawable? 181 | when (type) { 182 | TYPE_MASK -> { 183 | pressedDrawable = getDrawable( 184 | normalBackgroundColor, 185 | normalStrokeColor, 186 | normalGradientColors 187 | ).apply { 188 | colorFilter = 189 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 190 | } 191 | disabledDrawable = 192 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 193 | } 194 | TYPE_SELECTOR -> { 195 | pressedDrawable = 196 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 197 | disabledDrawable = getDrawable( 198 | disabledBackgroundColor, 199 | disabledStrokeColor, 200 | disabledGradientColors 201 | ) 202 | } 203 | } 204 | selectedDrawable = getDrawable( 205 | selectedBackgroundColor, 206 | selectedStrokeColor, 207 | selectedGradientColors 208 | ) 209 | background = StateListDrawable().apply { 210 | if (type != TYPE_NONE) { 211 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 212 | } 213 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 214 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 215 | addState(intArrayOf(), normalDrawable) 216 | } 217 | } 218 | 219 | private fun getDrawable( 220 | backgroundColor: Int, 221 | strokeColor: Int, 222 | gradientColors: IntArray? = null 223 | ): GradientDrawable { 224 | // 背景色相关 225 | val drawable = GradientDrawable() 226 | setupColor(drawable, backgroundColor) 227 | 228 | // 形状相关 229 | (drawable.mutate() as GradientDrawable).shape = shape 230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 231 | drawable.innerRadius = innerRadius 232 | if (innerRadiusRatio > 0f) { 233 | drawable.innerRadiusRatio = innerRadiusRatio 234 | } 235 | drawable.thickness = thickness 236 | if (thicknessRatio > 0f) { 237 | drawable.thicknessRatio = thicknessRatio 238 | } 239 | } 240 | 241 | // 描边相关 242 | if (strokeColor != 0) { 243 | (drawable.mutate() as GradientDrawable).setStroke( 244 | strokeWidth, 245 | strokeColor, 246 | dashWidth, 247 | dashGap 248 | ) 249 | } 250 | 251 | // 圆角相关 252 | if (cornersRadius != 0.0f) { 253 | (drawable.mutate() as GradientDrawable).cornerRadius = cornersRadius 254 | } else { 255 | // 指定4个角点中每个角点的半径。对于每个角点,数组 256 | // 包含两个值,X半径,Y半径 257 | // 顺序为左上角、右上角、右下角、左下角 258 | (drawable.mutate() as GradientDrawable).cornerRadii = floatArrayOf( 259 | cornersTopLeftRadius, 260 | cornersTopLeftRadius, 261 | 262 | cornersTopRightRadius, 263 | cornersTopRightRadius, 264 | 265 | cornersBottomRightRadius, 266 | cornersBottomRightRadius, 267 | 268 | cornersBottomLeftRadius, 269 | cornersBottomLeftRadius, 270 | ) 271 | } 272 | 273 | // 渐变相关 274 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 275 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 276 | (drawable.mutate() as GradientDrawable).setGradientCenter( 277 | gradientCenterX, 278 | gradientCenterY 279 | ) 280 | } 281 | gradientColors?.let { colors -> 282 | (drawable.mutate() as GradientDrawable).colors = colors 283 | } 284 | var orientation: GradientDrawable.Orientation? = null 285 | when (gradientOrientation) { 286 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 287 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 288 | } 289 | GRADIENT_ORIENTATION_TR_BL -> { 290 | orientation = GradientDrawable.Orientation.TR_BL 291 | } 292 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 293 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 294 | } 295 | GRADIENT_ORIENTATION_BR_TL -> { 296 | orientation = GradientDrawable.Orientation.BR_TL 297 | } 298 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 299 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 300 | } 301 | GRADIENT_ORIENTATION_BL_TR -> { 302 | orientation = GradientDrawable.Orientation.BL_TR 303 | } 304 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 305 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 306 | } 307 | GRADIENT_ORIENTATION_TL_BR -> { 308 | orientation = GradientDrawable.Orientation.TL_BR 309 | } 310 | } 311 | orientation?.apply { 312 | (drawable.mutate() as GradientDrawable).orientation = this 313 | } 314 | return drawable 315 | } 316 | 317 | private fun setupColor(drawable: GradientDrawable, backgroundColor: Int) { 318 | if (backgroundColor != 0) { 319 | (drawable.mutate() as GradientDrawable).setColor(backgroundColor) 320 | } 321 | } 322 | 323 | fun setInterceptType(@InterceptType interceptType: Int) { 324 | this.interceptType = interceptType 325 | } 326 | 327 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 328 | return when (interceptType) { 329 | INTERCEPT_TYPE_TRUE -> { 330 | true 331 | } 332 | INTERCEPT_TYPE_FALSE -> { 333 | false 334 | } 335 | else -> { 336 | return super.onInterceptTouchEvent(ev) 337 | } 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/layout/SleRelativeLayout.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.layout 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.StateListDrawable 8 | import android.os.Build 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import android.widget.RelativeLayout 12 | import com.freddy.silhouette.R 13 | import com.freddy.silhouette.config.* 14 | 15 | /** 16 | * 17 | * @author: FreddyChen 18 | * @date : 2022/02/07 06:16 19 | * @email : freddychencsc@gmail.com 20 | */ 21 | class SleRelativeLayout : RelativeLayout { 22 | 23 | @Type 24 | private var type: Int = TYPE_MASK 25 | 26 | @Shape 27 | private var shape: Int = GradientDrawable.RECTANGLE 28 | private var innerRadius: Int = 0 29 | private var innerRadiusRatio: Float = 0f 30 | private var thickness: Int = 0 31 | private var thicknessRatio: Float = 0f 32 | private var normalBackgroundColor: Int = 0 33 | private var pressedBackgroundColor: Int = 0 34 | private var disabledBackgroundColor: Int = 0 35 | private var selectedBackgroundColor: Int = 0 36 | private var strokeWidth: Int = 0 37 | private var dashWidth: Float = 0f 38 | private var dashGap: Float = 0f 39 | private var normalStrokeColor: Int = 0 40 | private var pressedStrokeColor: Int = 0 41 | private var disabledStrokeColor: Int = 0 42 | private var selectedStrokeColor: Int = 0 43 | private var cornersRadius: Float = 0f 44 | private var cornersTopLeftRadius: Float = 0f 45 | private var cornersTopRightRadius: Float = 0f 46 | private var cornersBottomLeftRadius: Float = 0f 47 | private var cornersBottomRightRadius: Float = 0f 48 | private var normalGradientColors: IntArray? = null 49 | private var pressedGradientColors: IntArray? = null 50 | private var disabledGradientColors: IntArray? = null 51 | private var selectedGradientColors: IntArray? = null 52 | private var gradientOrientation: Int = GRADIENT_ORIENTATION_TOP_BOTTOM 53 | 54 | @GradientType 55 | private var gradientType: Int = GradientDrawable.LINEAR_GRADIENT 56 | private var gradientCenterX: Float = 0f 57 | private var gradientCenterY: Float = 0f 58 | private var gradientRadius: Float = 0f 59 | 60 | private var maskBackgroundColor: Int = DEFAULT_MASK_BACKGROUND_COLOR 61 | 62 | @InterceptType 63 | private var interceptType: Int = INTERCEPT_TYPE_SUPER 64 | 65 | constructor(context: Context) : this(context, null) 66 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 67 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 68 | context, 69 | attrs, 70 | defStyleAttr 71 | ) { 72 | context.obtainStyledAttributes(attrs, R.styleable.SleRelativeLayout, defStyleAttr, 0) 73 | .apply { 74 | type = getInt(R.styleable.SleRelativeLayout_sle_type, TYPE_MASK) 75 | shape = 76 | getInt(R.styleable.SleRelativeLayout_sle_shape, GradientDrawable.RECTANGLE) 77 | innerRadius = 78 | getDimensionPixelSize(R.styleable.SleRelativeLayout_sle_innerRadius, 0) 79 | innerRadiusRatio = 80 | getFloat(R.styleable.SleRelativeLayout_sle_innerRadiusRatio, 0f) 81 | thickness = 82 | getDimensionPixelSize(R.styleable.SleRelativeLayout_sle_thickness, 0) 83 | thicknessRatio = getFloat(R.styleable.SleRelativeLayout_sle_thicknessRatio, 0f) 84 | normalBackgroundColor = 85 | getColor(R.styleable.SleRelativeLayout_sle_normalBackgroundColor, 0) 86 | pressedBackgroundColor = 87 | getColor(R.styleable.SleRelativeLayout_sle_pressedBackgroundColor, 0) 88 | disabledBackgroundColor = 89 | getColor( 90 | R.styleable.SleRelativeLayout_sle_disabledBackgroundColor, 91 | DEFAULT_DISABLE_BACKGROUND_COLOR 92 | ) 93 | selectedBackgroundColor = 94 | getColor(R.styleable.SleRelativeLayout_sle_selectedBackgroundColor, 0) 95 | strokeWidth = 96 | getDimensionPixelSize(R.styleable.SleRelativeLayout_sle_strokeWidth, 0) 97 | dashWidth = getDimension(R.styleable.SleRelativeLayout_sle_dashWidth, 0f) 98 | dashGap = getDimension(R.styleable.SleRelativeLayout_sle_dashGap, 0f) 99 | normalStrokeColor = 100 | getColor(R.styleable.SleRelativeLayout_sle_normalStrokeColor, 0) 101 | pressedStrokeColor = 102 | getColor( 103 | R.styleable.SleRelativeLayout_sle_pressedStrokeColor, 104 | normalStrokeColor 105 | ) 106 | disabledStrokeColor = 107 | getColor( 108 | R.styleable.SleRelativeLayout_sle_disabledStrokeColor, 109 | normalStrokeColor 110 | ) 111 | selectedStrokeColor = 112 | getColor( 113 | R.styleable.SleRelativeLayout_sle_selectedStrokeColor, 114 | normalStrokeColor 115 | ) 116 | cornersRadius = 117 | getDimension(R.styleable.SleRelativeLayout_sle_cornersRadius, 0f) 118 | cornersTopLeftRadius = 119 | getDimension(R.styleable.SleRelativeLayout_sle_cornersTopLeftRadius, 0f) 120 | cornersTopRightRadius = 121 | getDimension(R.styleable.SleRelativeLayout_sle_cornersTopRightRadius, 0f) 122 | cornersBottomLeftRadius = 123 | getDimension(R.styleable.SleRelativeLayout_sle_cornersBottomLeftRadius, 0f) 124 | cornersBottomRightRadius = 125 | getDimension(R.styleable.SleRelativeLayout_sle_cornersBottomRightRadius, 0f) 126 | val normalGradientColorsResourceId = 127 | getResourceId(R.styleable.SleRelativeLayout_sle_normalGradientColors, 0) 128 | if (normalGradientColorsResourceId != 0) { 129 | normalGradientColors = resources.getIntArray(normalGradientColorsResourceId) 130 | } 131 | val pressedGradientColorsResourceId = 132 | getResourceId(R.styleable.SleRelativeLayout_sle_pressedGradientColors, 0) 133 | if (pressedGradientColorsResourceId != 0) { 134 | pressedGradientColors = resources.getIntArray(pressedGradientColorsResourceId) 135 | } 136 | val disabledGradientColorsResourceId = 137 | getResourceId(R.styleable.SleRelativeLayout_sle_disabledGradientColors, 0) 138 | if (disabledGradientColorsResourceId != 0) { 139 | disabledGradientColors = resources.getIntArray(disabledGradientColorsResourceId) 140 | } 141 | val selectedGradientColorsResourceId = 142 | getResourceId(R.styleable.SleRelativeLayout_sle_selectedGradientColors, 0) 143 | if (selectedGradientColorsResourceId != 0) { 144 | selectedGradientColors = resources.getIntArray(selectedGradientColorsResourceId) 145 | } 146 | gradientOrientation = getInt( 147 | R.styleable.SleRelativeLayout_sle_gradientOrientation, 148 | GRADIENT_ORIENTATION_TOP_BOTTOM 149 | ) 150 | gradientType = 151 | getInt( 152 | R.styleable.SleRelativeLayout_sle_gradientType, 153 | GradientDrawable.LINEAR_GRADIENT 154 | ) 155 | gradientCenterX = 156 | getDimension(R.styleable.SleRelativeLayout_sle_gradientCenterX, 0f) 157 | gradientCenterY = 158 | getDimension(R.styleable.SleRelativeLayout_sle_gradientCenterY, 0f) 159 | gradientRadius = 160 | getDimension(R.styleable.SleRelativeLayout_sle_gradientRadius, 0f) 161 | maskBackgroundColor = getColor( 162 | R.styleable.SleRelativeLayout_sle_maskBackgroundColor, 163 | DEFAULT_MASK_BACKGROUND_COLOR 164 | ) 165 | interceptType = 166 | getInt( 167 | R.styleable.SleRelativeLayout_sle_interceptType, 168 | INTERCEPT_TYPE_SUPER 169 | ) 170 | recycle() 171 | } 172 | init() 173 | } 174 | 175 | private fun init() { 176 | val normalDrawable = 177 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 178 | var pressedDrawable: GradientDrawable? = null 179 | var disabledDrawable: GradientDrawable? = null 180 | val selectedDrawable: GradientDrawable? 181 | when (type) { 182 | TYPE_MASK -> { 183 | pressedDrawable = getDrawable( 184 | normalBackgroundColor, 185 | normalStrokeColor, 186 | normalGradientColors 187 | ).apply { 188 | colorFilter = 189 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 190 | } 191 | disabledDrawable = 192 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 193 | } 194 | TYPE_SELECTOR -> { 195 | pressedDrawable = 196 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 197 | disabledDrawable = getDrawable( 198 | disabledBackgroundColor, 199 | disabledStrokeColor, 200 | disabledGradientColors 201 | ) 202 | } 203 | } 204 | selectedDrawable = getDrawable( 205 | selectedBackgroundColor, 206 | selectedStrokeColor, 207 | selectedGradientColors 208 | ) 209 | background = StateListDrawable().apply { 210 | if (type != TYPE_NONE) { 211 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 212 | } 213 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 214 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 215 | addState(intArrayOf(), normalDrawable) 216 | } 217 | } 218 | 219 | private fun getDrawable( 220 | backgroundColor: Int, 221 | strokeColor: Int, 222 | gradientColors: IntArray? = null 223 | ): GradientDrawable { 224 | // 背景色相关 225 | val drawable = GradientDrawable() 226 | setupColor(drawable, backgroundColor) 227 | 228 | // 形状相关 229 | (drawable.mutate() as GradientDrawable).shape = shape 230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 231 | drawable.innerRadius = innerRadius 232 | if (innerRadiusRatio > 0f) { 233 | drawable.innerRadiusRatio = innerRadiusRatio 234 | } 235 | drawable.thickness = thickness 236 | if (thicknessRatio > 0f) { 237 | drawable.thicknessRatio = thicknessRatio 238 | } 239 | } 240 | 241 | // 描边相关 242 | if (strokeColor != 0) { 243 | (drawable.mutate() as GradientDrawable).setStroke( 244 | strokeWidth, 245 | strokeColor, 246 | dashWidth, 247 | dashGap 248 | ) 249 | } 250 | 251 | // 圆角相关 252 | if (cornersRadius != 0.0f) { 253 | (drawable.mutate() as GradientDrawable).cornerRadius = cornersRadius 254 | } else { 255 | // 指定4个角点中每个角点的半径。对于每个角点,数组 256 | // 包含两个值,X半径,Y半径 257 | // 顺序为左上角、右上角、右下角、左下角 258 | (drawable.mutate() as GradientDrawable).cornerRadii = floatArrayOf( 259 | cornersTopLeftRadius, 260 | cornersTopLeftRadius, 261 | 262 | cornersTopRightRadius, 263 | cornersTopRightRadius, 264 | 265 | cornersBottomRightRadius, 266 | cornersBottomRightRadius, 267 | 268 | cornersBottomLeftRadius, 269 | cornersBottomLeftRadius, 270 | ) 271 | } 272 | 273 | // 渐变相关 274 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 275 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 276 | (drawable.mutate() as GradientDrawable).setGradientCenter( 277 | gradientCenterX, 278 | gradientCenterY 279 | ) 280 | } 281 | gradientColors?.let { colors -> 282 | (drawable.mutate() as GradientDrawable).colors = colors 283 | } 284 | var orientation: GradientDrawable.Orientation? = null 285 | when (gradientOrientation) { 286 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 287 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 288 | } 289 | GRADIENT_ORIENTATION_TR_BL -> { 290 | orientation = GradientDrawable.Orientation.TR_BL 291 | } 292 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 293 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 294 | } 295 | GRADIENT_ORIENTATION_BR_TL -> { 296 | orientation = GradientDrawable.Orientation.BR_TL 297 | } 298 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 299 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 300 | } 301 | GRADIENT_ORIENTATION_BL_TR -> { 302 | orientation = GradientDrawable.Orientation.BL_TR 303 | } 304 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 305 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 306 | } 307 | GRADIENT_ORIENTATION_TL_BR -> { 308 | orientation = GradientDrawable.Orientation.TL_BR 309 | } 310 | } 311 | orientation?.apply { 312 | (drawable.mutate() as GradientDrawable).orientation = this 313 | } 314 | return drawable 315 | } 316 | 317 | private fun setupColor(drawable: GradientDrawable, backgroundColor: Int) { 318 | if (backgroundColor != 0) { 319 | (drawable.mutate() as GradientDrawable).setColor(backgroundColor) 320 | } 321 | } 322 | 323 | fun setInterceptType(@InterceptType interceptType: Int) { 324 | this.interceptType = interceptType 325 | } 326 | 327 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 328 | return when (interceptType) { 329 | INTERCEPT_TYPE_TRUE -> { 330 | true 331 | } 332 | INTERCEPT_TYPE_FALSE -> { 333 | false 334 | } 335 | else -> { 336 | return super.onInterceptTouchEvent(ev) 337 | } 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/layout/SleConstraintLayout.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.layout 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import android.graphics.drawable.GradientDrawable 7 | import android.graphics.drawable.StateListDrawable 8 | import android.os.Build 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import com.freddy.silhouette.R 13 | import com.freddy.silhouette.config.* 14 | 15 | /** 16 | * 17 | * @author: FreddyChen 18 | * @date : 2022/02/07 06:16 19 | * @email : freddychencsc@gmail.com 20 | */ 21 | class SleConstraintLayout : ConstraintLayout { 22 | 23 | @Type 24 | private var type: Int = TYPE_MASK 25 | 26 | @Shape 27 | private var shape: Int = GradientDrawable.RECTANGLE 28 | private var innerRadius: Int = 0 29 | private var innerRadiusRatio: Float = 0f 30 | private var thickness: Int = 0 31 | private var thicknessRatio: Float = 0f 32 | private var normalBackgroundColor: Int = 0 33 | private var pressedBackgroundColor: Int = 0 34 | private var disabledBackgroundColor: Int = 0 35 | private var selectedBackgroundColor: Int = 0 36 | private var strokeWidth: Int = 0 37 | private var dashWidth: Float = 0f 38 | private var dashGap: Float = 0f 39 | private var normalStrokeColor: Int = 0 40 | private var pressedStrokeColor: Int = 0 41 | private var disabledStrokeColor: Int = 0 42 | private var selectedStrokeColor: Int = 0 43 | private var cornersRadius: Float = 0f 44 | private var cornersTopLeftRadius: Float = 0f 45 | private var cornersTopRightRadius: Float = 0f 46 | private var cornersBottomLeftRadius: Float = 0f 47 | private var cornersBottomRightRadius: Float = 0f 48 | private var normalGradientColors: IntArray? = null 49 | private var pressedGradientColors: IntArray? = null 50 | private var disabledGradientColors: IntArray? = null 51 | private var selectedGradientColors: IntArray? = null 52 | private var gradientOrientation: Int = GRADIENT_ORIENTATION_TOP_BOTTOM 53 | 54 | @GradientType 55 | private var gradientType: Int = GradientDrawable.LINEAR_GRADIENT 56 | private var gradientCenterX: Float = 0f 57 | private var gradientCenterY: Float = 0f 58 | private var gradientRadius: Float = 0f 59 | 60 | private var maskBackgroundColor: Int = DEFAULT_MASK_BACKGROUND_COLOR 61 | 62 | @InterceptType 63 | private var interceptType: Int = INTERCEPT_TYPE_SUPER 64 | 65 | constructor(context: Context) : this(context, null) 66 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 67 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 68 | context, 69 | attrs, 70 | defStyleAttr 71 | ) { 72 | context.obtainStyledAttributes(attrs, R.styleable.SleConstraintLayout, defStyleAttr, 0) 73 | .apply { 74 | type = getInt(R.styleable.SleConstraintLayout_sle_type, TYPE_MASK) 75 | shape = 76 | getInt(R.styleable.SleConstraintLayout_sle_shape, GradientDrawable.RECTANGLE) 77 | innerRadius = 78 | getDimensionPixelSize(R.styleable.SleConstraintLayout_sle_innerRadius, 0) 79 | innerRadiusRatio = 80 | getFloat(R.styleable.SleConstraintLayout_sle_innerRadiusRatio, 0f) 81 | thickness = 82 | getDimensionPixelSize(R.styleable.SleConstraintLayout_sle_thickness, 0) 83 | thicknessRatio = getFloat(R.styleable.SleConstraintLayout_sle_thicknessRatio, 0f) 84 | normalBackgroundColor = 85 | getColor(R.styleable.SleConstraintLayout_sle_normalBackgroundColor, 0) 86 | pressedBackgroundColor = 87 | getColor(R.styleable.SleConstraintLayout_sle_pressedBackgroundColor, 0) 88 | disabledBackgroundColor = 89 | getColor( 90 | R.styleable.SleConstraintLayout_sle_disabledBackgroundColor, 91 | DEFAULT_DISABLE_BACKGROUND_COLOR 92 | ) 93 | selectedBackgroundColor = 94 | getColor(R.styleable.SleConstraintLayout_sle_selectedBackgroundColor, 0) 95 | strokeWidth = 96 | getDimensionPixelSize(R.styleable.SleConstraintLayout_sle_strokeWidth, 0) 97 | dashWidth = getDimension(R.styleable.SleConstraintLayout_sle_dashWidth, 0f) 98 | dashGap = getDimension(R.styleable.SleConstraintLayout_sle_dashGap, 0f) 99 | normalStrokeColor = 100 | getColor(R.styleable.SleConstraintLayout_sle_normalStrokeColor, 0) 101 | pressedStrokeColor = 102 | getColor( 103 | R.styleable.SleConstraintLayout_sle_pressedStrokeColor, 104 | normalStrokeColor 105 | ) 106 | disabledStrokeColor = 107 | getColor( 108 | R.styleable.SleConstraintLayout_sle_disabledStrokeColor, 109 | normalStrokeColor 110 | ) 111 | selectedStrokeColor = 112 | getColor( 113 | R.styleable.SleConstraintLayout_sle_selectedStrokeColor, 114 | normalStrokeColor 115 | ) 116 | cornersRadius = 117 | getDimension(R.styleable.SleConstraintLayout_sle_cornersRadius, 0f) 118 | cornersTopLeftRadius = 119 | getDimension(R.styleable.SleConstraintLayout_sle_cornersTopLeftRadius, 0f) 120 | cornersTopRightRadius = 121 | getDimension(R.styleable.SleConstraintLayout_sle_cornersTopRightRadius, 0f) 122 | cornersBottomLeftRadius = 123 | getDimension(R.styleable.SleConstraintLayout_sle_cornersBottomLeftRadius, 0f) 124 | cornersBottomRightRadius = 125 | getDimension(R.styleable.SleConstraintLayout_sle_cornersBottomRightRadius, 0f) 126 | val normalGradientColorsResourceId = 127 | getResourceId(R.styleable.SleConstraintLayout_sle_normalGradientColors, 0) 128 | if (normalGradientColorsResourceId != 0) { 129 | normalGradientColors = resources.getIntArray(normalGradientColorsResourceId) 130 | } 131 | val pressedGradientColorsResourceId = 132 | getResourceId(R.styleable.SleConstraintLayout_sle_pressedGradientColors, 0) 133 | if (pressedGradientColorsResourceId != 0) { 134 | pressedGradientColors = resources.getIntArray(pressedGradientColorsResourceId) 135 | } 136 | val disabledGradientColorsResourceId = 137 | getResourceId(R.styleable.SleConstraintLayout_sle_disabledGradientColors, 0) 138 | if (disabledGradientColorsResourceId != 0) { 139 | disabledGradientColors = resources.getIntArray(disabledGradientColorsResourceId) 140 | } 141 | val selectedGradientColorsResourceId = 142 | getResourceId(R.styleable.SleConstraintLayout_sle_selectedGradientColors, 0) 143 | if (selectedGradientColorsResourceId != 0) { 144 | selectedGradientColors = resources.getIntArray(selectedGradientColorsResourceId) 145 | } 146 | gradientOrientation = getInt( 147 | R.styleable.SleConstraintLayout_sle_gradientOrientation, 148 | GRADIENT_ORIENTATION_TOP_BOTTOM 149 | ) 150 | gradientType = 151 | getInt( 152 | R.styleable.SleConstraintLayout_sle_gradientType, 153 | GradientDrawable.LINEAR_GRADIENT 154 | ) 155 | gradientCenterX = 156 | getDimension(R.styleable.SleConstraintLayout_sle_gradientCenterX, 0f) 157 | gradientCenterY = 158 | getDimension(R.styleable.SleConstraintLayout_sle_gradientCenterY, 0f) 159 | gradientRadius = 160 | getDimension(R.styleable.SleConstraintLayout_sle_gradientRadius, 0f) 161 | maskBackgroundColor = getColor( 162 | R.styleable.SleConstraintLayout_sle_maskBackgroundColor, 163 | DEFAULT_MASK_BACKGROUND_COLOR 164 | ) 165 | interceptType = 166 | getInt( 167 | R.styleable.SleConstraintLayout_sle_interceptType, 168 | INTERCEPT_TYPE_SUPER 169 | ) 170 | recycle() 171 | } 172 | init() 173 | } 174 | 175 | private fun init() { 176 | val normalDrawable = 177 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 178 | var pressedDrawable: GradientDrawable? = null 179 | var disabledDrawable: GradientDrawable? = null 180 | val selectedDrawable: GradientDrawable? 181 | when (type) { 182 | TYPE_MASK -> { 183 | pressedDrawable = getDrawable( 184 | normalBackgroundColor, 185 | normalStrokeColor, 186 | normalGradientColors 187 | ).apply { 188 | colorFilter = 189 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 190 | } 191 | disabledDrawable = 192 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 193 | } 194 | TYPE_SELECTOR -> { 195 | pressedDrawable = 196 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 197 | disabledDrawable = getDrawable( 198 | disabledBackgroundColor, 199 | disabledStrokeColor, 200 | disabledGradientColors 201 | ) 202 | } 203 | } 204 | selectedDrawable = getDrawable( 205 | selectedBackgroundColor, 206 | selectedStrokeColor, 207 | selectedGradientColors 208 | ) 209 | background = StateListDrawable().apply { 210 | if(type != TYPE_NONE) { 211 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 212 | } 213 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 214 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 215 | addState(intArrayOf(), normalDrawable) 216 | } 217 | } 218 | 219 | private fun getDrawable( 220 | backgroundColor: Int, 221 | strokeColor: Int, 222 | gradientColors: IntArray? = null 223 | ): GradientDrawable { 224 | // 背景色相关 225 | val drawable = GradientDrawable() 226 | setupColor(drawable, backgroundColor) 227 | 228 | // 形状相关 229 | (drawable.mutate() as GradientDrawable).shape = shape 230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 231 | drawable.innerRadius = innerRadius 232 | if (innerRadiusRatio > 0f) { 233 | drawable.innerRadiusRatio = innerRadiusRatio 234 | } 235 | drawable.thickness = thickness 236 | if (thicknessRatio > 0f) { 237 | drawable.thicknessRatio = thicknessRatio 238 | } 239 | } 240 | 241 | // 描边相关 242 | if (strokeColor != 0) { 243 | (drawable.mutate() as GradientDrawable).setStroke( 244 | strokeWidth, 245 | strokeColor, 246 | dashWidth, 247 | dashGap 248 | ) 249 | } 250 | 251 | // 圆角相关 252 | if (cornersRadius != 0.0f) { 253 | (drawable.mutate() as GradientDrawable).cornerRadius = cornersRadius 254 | } else { 255 | // 指定4个角点中每个角点的半径。对于每个角点,数组 256 | // 包含两个值,X半径,Y半径 257 | // 顺序为左上角、右上角、右下角、左下角 258 | (drawable.mutate() as GradientDrawable).cornerRadii = floatArrayOf( 259 | cornersTopLeftRadius, 260 | cornersTopLeftRadius, 261 | 262 | cornersTopRightRadius, 263 | cornersTopRightRadius, 264 | 265 | cornersBottomRightRadius, 266 | cornersBottomRightRadius, 267 | 268 | cornersBottomLeftRadius, 269 | cornersBottomLeftRadius, 270 | ) 271 | } 272 | 273 | // 渐变相关 274 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 275 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 276 | (drawable.mutate() as GradientDrawable).setGradientCenter( 277 | gradientCenterX, 278 | gradientCenterY 279 | ) 280 | } 281 | gradientColors?.let { colors -> 282 | (drawable.mutate() as GradientDrawable).colors = colors 283 | } 284 | var orientation: GradientDrawable.Orientation? = null 285 | when (gradientOrientation) { 286 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 287 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 288 | } 289 | GRADIENT_ORIENTATION_TR_BL -> { 290 | orientation = GradientDrawable.Orientation.TR_BL 291 | } 292 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 293 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 294 | } 295 | GRADIENT_ORIENTATION_BR_TL -> { 296 | orientation = GradientDrawable.Orientation.BR_TL 297 | } 298 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 299 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 300 | } 301 | GRADIENT_ORIENTATION_BL_TR -> { 302 | orientation = GradientDrawable.Orientation.BL_TR 303 | } 304 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 305 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 306 | } 307 | GRADIENT_ORIENTATION_TL_BR -> { 308 | orientation = GradientDrawable.Orientation.TL_BR 309 | } 310 | } 311 | orientation?.apply { 312 | (drawable.mutate() as GradientDrawable).orientation = this 313 | } 314 | return drawable 315 | } 316 | 317 | private fun setupColor(drawable: GradientDrawable, backgroundColor: Int) { 318 | if (backgroundColor != 0) { 319 | (drawable.mutate() as GradientDrawable).setColor(backgroundColor) 320 | } 321 | } 322 | 323 | fun setInterceptType(@InterceptType interceptType: Int) { 324 | this.interceptType = interceptType 325 | } 326 | 327 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 328 | return when (interceptType) { 329 | INTERCEPT_TYPE_TRUE -> { 330 | true 331 | } 332 | INTERCEPT_TYPE_FALSE -> { 333 | false 334 | } 335 | else -> { 336 | return super.onInterceptTouchEvent(ev) 337 | } 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /Silhouette——更方便的ShapeSelector实现方案.md: -------------------------------------------------------------------------------- 1 | # Silhouette——更方便的Shape/Selector实现方案 2 | 3 | ## 写在前面 4 | 首先祝大家新年快乐,开工大吉。 5 | 最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:**Silhouette**。另外,考虑到**Kotlin**越来越普及,作者在开发过程中也切实感受到**Kotlin**相较于**Java**带来的便利,后续的**IM**系列文章及项目考虑用**Kotlin**重写,而且考虑到由于工作业务需求过多可能出现断更的情况,所以打算一次性写完再放出来,避免大家学习不方便。 6 | 废话不多说,直接开始吧。 7 | 8 | ## Silhouette是什么? 9 | **Silhouette**意为“剪影”,取名并没有特别的含义,只是单纯地觉得意境较美。例如上一篇文章[Shine——更简单的Android网络请求库封装](https://juejin.cn/post/7054105794840625160)的网络请求库:[Shine](https://github.com/FreddyChen/Shine-Kotlin)即意为“闪耀”,也没有特别的含义,只是作者认为开源库起名较难,特意找一些比较优美的单词。 10 | **Silhouette**是一系列基于**GradientDrawable**及**StateListDrawable**封装的组件集合,主要用于实现在**Android Layout XML**中直接支持**Shape/Selector**等功能。 11 | 我们都知道在**Android**开发中,不同的**TextView**及**Button**各种样式(形状、背景色、描边、圆角、渐变等)的传统实现方式是在**drawable**文件夹中编写各种**shape/selector**等文件,这种方式至少会存在以下几种弊端: 12 | 1. **shape/selector**文件过多,项目体积增大; 13 | 2. **shape/selector**文件命名困难,命名规范时往往会存在功能重复的文件; 14 | 3. 功能存在局限性:例如**gradient**渐变色。传统**shape**方式只支持三种颜色过渡(**startColor/centerColor/endColor**),如果设计稿存在四种以上颜色渐变,**shape gradient**无能为力。再比如**TextView**在常态和按下态需要**同时改变背景色及文字颜色**时,传统方式只能在代码中动态设置等。 15 | 4. 开发效率低; 16 | 5. 难以维护等; 17 | 18 | 综上所述,我们迫切需要一个库来解决以上问题,**Silhouette**正具备这些能力。接下来,我们来具体看看**Silhouette**能做什么吧。 19 | 20 | ## Silhouette能做什么? 21 | 上面说到**Silhouette**是一系列组件集合,具体包含以下组件: 22 | * **SleTextButton** 23 | 基于**AppCompatTextView**封装; 24 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的能力 ; 25 | 具备不同状态(常态、按下态、不可点击态)下文字颜色指定等。 26 | 27 | * **SleImageButton** 28 | 基于**ShapeableImageView**封装; 29 | 通过指定**sle_ib_type**属性使**ImageView**支持按下态遮罩层、透明度改变、自定义图片,同时支持**CheckBox**功能; 30 | 通过指定**sle_ib_style**属性使**ImageView**支持**Normal**、圆角、圆形等形状。 31 | 32 | * **SleConstraintLayout** 33 | 基于**ConstraintLayout**封装; 34 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 35 | 36 | * **SleRelativeLayout** 37 | 基于**RelativeLayout**封装; 38 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 39 | 40 | * **SleLinearLayout** 41 | 基于**LinearLayout**封装; 42 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 43 | 44 | * **SleFrameLayout** 45 | 基于**FrameLayout**封装; 46 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 47 | 48 | ## 设计、封装思路及原理 49 | * 项目结构 50 | **com.freddy.silhouette** 51 | - **config**(配置相关,存放全局注解及公共常量、默认值等) 52 | - **ext**(**kotlin**扩展相关,可选择用或不用) 53 | - **utils**(工具类相关,可选择用或不用) 54 | - **widget**(控件相关) 55 | - **button** 56 | - **layout** 57 | 58 | 由此可见,项目结构非常简单,所以**Silhouette**也是一个比较轻量级的库。 59 | 60 | * 封装思路及原理 61 | 由于该库非常简单,实际上就是根据**Shape/Selector**进行自定义属性,从而利用**GradientDrawable**及**StateListDrawable**提供的**API**进行封装,不存在什么难度,在此就不展开讲了。 62 | 63 | 下面贴一下代码片段,基本上几个组件的实现原理都大同小异,都是利用**GradientDrawable**及**StateListDrawable**实现组件的**Shape**及**Selector**功能: 64 | ``` 65 | private fun init() { 66 | val normalDrawable = 67 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 68 | var pressedDrawable: GradientDrawable? = null 69 | var disabledDrawable: GradientDrawable? = null 70 | var selectedDrawable: GradientDrawable? = null 71 | when (type) { 72 | TYPE_MASK -> { 73 | pressedDrawable = getDrawable( 74 | normalBackgroundColor, 75 | normalStrokeColor, 76 | normalGradientColors 77 | ).apply { 78 | colorFilter = 79 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 80 | } 81 | disabledDrawable = 82 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 83 | } 84 | TYPE_SELECTOR -> { 85 | pressedDrawable = 86 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 87 | disabledDrawable = getDrawable( 88 | disabledBackgroundColor, 89 | disabledStrokeColor, 90 | disabledGradientColors 91 | ) 92 | } 93 | } 94 | selectedDrawable = getDrawable( 95 | selectedBackgroundColor, 96 | selectedStrokeColor, 97 | selectedGradientColors 98 | ) 99 | setTextColor(normalTextColor) 100 | background = StateListDrawable().apply { 101 | if (type != TYPE_NONE) { 102 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 103 | } 104 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 105 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 106 | addState(intArrayOf(), normalDrawable) 107 | } 108 | 109 | setOnTouchListener(this) 110 | } 111 | 112 | private fun getDrawable( 113 | backgroundColor: Int, 114 | strokeColor: Int, 115 | gradientColors: IntArray? = null 116 | ): GradientDrawable { 117 | // 背景色相关 118 | val drawable = GradientDrawable() 119 | setupColor(drawable, backgroundColor) 120 | 121 | // 形状相关 122 | (drawable.mutate() as GradientDrawable).shape = shape 123 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 124 | drawable.innerRadius = innerRadius 125 | if (innerRadiusRatio > 0f) { 126 | drawable.innerRadiusRatio = innerRadiusRatio 127 | } 128 | drawable.thickness = thickness 129 | if (thicknessRatio > 0f) { 130 | drawable.thicknessRatio = thicknessRatio 131 | } 132 | } 133 | 134 | // 描边相关 135 | if (strokeColor != 0) { 136 | (drawable.mutate() as GradientDrawable).setStroke( 137 | strokeWidth, 138 | strokeColor, 139 | dashWidth, 140 | dashGap 141 | ) 142 | } 143 | 144 | // 圆角相关 145 | setupCornersRadius( 146 | drawable, 147 | cornersRadius, 148 | cornersTopLeftRadius, 149 | cornersTopRightRadius, 150 | cornersBottomRightRadius, 151 | cornersBottomLeftRadius 152 | ) 153 | 154 | // 渐变相关 155 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 156 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 157 | (drawable.mutate() as GradientDrawable).setGradientCenter( 158 | gradientCenterX, 159 | gradientCenterY 160 | ) 161 | } 162 | gradientColors?.let { colors -> 163 | (drawable.mutate() as GradientDrawable).colors = colors 164 | } 165 | var orientation: GradientDrawable.Orientation? = null 166 | when (gradientOrientation) { 167 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 168 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 169 | } 170 | GRADIENT_ORIENTATION_TR_BL -> { 171 | orientation = GradientDrawable.Orientation.TR_BL 172 | } 173 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 174 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 175 | } 176 | GRADIENT_ORIENTATION_BR_TL -> { 177 | orientation = GradientDrawable.Orientation.BR_TL 178 | } 179 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 180 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 181 | } 182 | GRADIENT_ORIENTATION_BL_TR -> { 183 | orientation = GradientDrawable.Orientation.BL_TR 184 | } 185 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 186 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 187 | } 188 | GRADIENT_ORIENTATION_TL_BR -> { 189 | drawable.orientation = GradientDrawable.Orientation.TL_BR 190 | } 191 | } 192 | orientation?.apply { 193 | (drawable.mutate() as GradientDrawable).orientation = this 194 | } 195 | return drawable 196 | } 197 | ``` 198 | 感兴趣的同学可以到官方文档了解**GradientDrawable**及**StateListDrawable**的原理。 199 | 200 | ## 自定义属性列表 201 | 自定义属性分为**通用属性**和**特有属性**。 202 | * **通用属性** 203 | 204 | - 类型 205 | | 属性名称 | 类型 | 说明 | 备注 | 206 | | -- | :--: | :-- | -- | 207 | | sle_type | enum | 类型
mask:遮罩
selector:自定义样式
none:无 | 默认值:mask
默认的mask为90%透明度黑色,可通过sle_maskBackgroundColors属性设置
若不指定为selector,则自定义样式无效 | 208 | 209 | - 形状相关 210 | | 属性名称 | 类型 | 说明 | 备注 | 211 | | -- | :--: | :-- | :--: | 212 | | sle_shape | enum | 形状
rectangle:矩形
oval:椭圆形
line:线性形状
ring:环形 | 默认值:rectangle | 213 | | sle_innerRadius | dimension|reference | 尺寸,内环的半径 | shape="ring"可用 | 214 | | sle_innerRadiusRatio | float | 以环的宽度比率来表示内环的半径 | shape="ring"可用 | 215 | | sle_thickness | dimension|reference | 尺寸,环的厚度 | shape="ring"可用 | 216 | | sle_thicknessRatio | float | 以环的宽度比率来表示环的厚度 | shape="ring"可用 | 217 | 218 | - 背景色相关 219 | | 属性名称 | 类型 | 说明 | 备注 | 220 | | :-- | :--: | :--: | :--: | 221 | | sle_normalBackgroundColor | color|reference | 常态背景颜色 | / | 222 | | sle_pressedBackgroundColor | color|reference | 按下态背景颜色 | / | 223 | | sle_disabledBackgroundColor | color|reference | 不可点击态背景颜色 | 默认值:#CCCCCC | 224 | | sle_selectedBackgroundColor | color|reference | 选中态背景颜色 | / | 225 | 226 | - 描边相关 227 | | 属性名称 | 类型 | 说明 | 备注 | 228 | | :-- | :--: | :--: | :--: | 229 | | sle_normalStrokeColor | color|reference | 常态描边颜色 | / | 230 | | sle_pressedStrokeColor | color|reference | 按下态描边颜色 | / | 231 | | sle_disabledStrokeColor | color|reference | 不可点击态描边颜色 | / | 232 | | sle_selectedStrokeColor | color|reference | 选中态描边颜色 | / | 233 | | sle_strokeWidth | dimension|reference | 描边宽度 | / | 234 | | sle_dashWidth | dimension|reference | 虚线宽度 | / | 235 | | sle_dashGap | dimension|reference | 虚线间隔 | / | 236 | 237 | - 圆角相关 238 | | 属性名称 | 类型 | 说明 | 备注 | 239 | | :-- | :--: | :--: | :--: | 240 | | sle_cornersRadius | dimension|reference | 总圆角半径 | / | 241 | | sle_cornersTopLeftRadius | dimension|reference | 左上角圆角半径 | / | 242 | | sle_cornersTopRightRadius | dimension|reference | 右上角圆角半径 | / | 243 | | sle_cornersBottomLeftRadius | dimension|reference | 左下角圆角半径 | / | 244 | | sle_cornersBottomRightRadius | dimension|reference | 右下角圆角半径 | / | 245 | 246 | - 渐变相关 247 | | 属性名称 | 类型 | 说明 | 备注 | 248 | | :-- | :--: | :-- | :--: | 249 | | sle_normalGradientColors | reference | 常态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 250 | | sle_pressedGradientColors | reference | 按下态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 251 | | sle_disabledGradientColors | reference | 不可点击态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 252 | | sle_selectedGradientColors | reference | 选中态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 253 | | sle_gradientOrientation | enum | 渐变方向
TOP_BOTTOM:从上到下
TR_BL:从右上到左下
RIGHT_LEFT:从右到左
BR_TL:从右下到左上
BOTTOM_TOP:从下到上
BL_TR:从左下到右上
LEFT_RIGHT:从左到右
TL_BR:从左上到右下 | / | 254 | | sle_gradientType | enum | 渐变类型
linear:线性渐变
radial:圆形渐变,起始颜色从gradientCenterX、gradientCenterY点开始
sweep:A sweeping line gradient | / | 255 | | sle_gradientCenterX | float | 渐变中心放射点x坐标 | 注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点 | 256 | | sle_gradientCenterY | float | 渐变中心放射点y坐标 | 注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点 | 257 | | sle_gradientRadius | dimension|reference | 渐变半径 | 需要配合gradientType=radial使用,如果设置gradientType=radial而没有设置gradientRadius,将会报错 | 258 | 259 | - 其它 260 | | 属性名称 | 类型 | 说明 | 备注 | 261 | | :-- | :--: | :--: | :--: | 262 | | sle_maskBackgroundColor | color|reference | 当sle_type=mask时,按钮按下状态的遮罩颜色 | 默认值:90%透明度黑色(#1A000000) | 263 | | sle_cancelOffset | dimension|reference | 用于解决手指移出控件区域判断为cancel的偏移量 | 默认值:8dp | 264 | 265 | * **特有属性** 266 | - **SleConstraintLayout/SleRelativeLayout/SleFrameLayout/SleLinearLayout** 267 | | 属性名称 | 类型 | 说明 | 备注 | 268 | | :-- | :--: | :-- | :-- | 269 | | sle_interceptType | enum | 事件拦截类型
intercept_super:return super
intercept_true:return true
intercept_false:return false | Layout组件设置此值,可实现是否拦截事件,如果设置为intercept_true,事件将不传递到子控件,在某些场景比较实用 | 270 | 271 | - **SleTextButton** 272 | | 属性名称 | 类型 | 说明 | 备注 | 273 | | :-- | :--: | :--: | :--: | 274 | | sle_normalTextColor | color|reference | 常态文字颜色 | / | 275 | | sle_pressedTextColor | color|reference | 按下态文字颜色 | / | 276 | | sle_disabledTextColor | color|reference | 不可点击态文字颜色 | / | 277 | | sle_selectedTextColor | color|reference | 选中态文字颜色 | / | 278 | 279 | - **SleImageButton** 280 | | 属性名称 | 类型 | 说明 | 备注 | 281 | | :-- | :--: | :-- | :-- | 282 | | sle_ib_type | enum | 类型
mask:图片遮罩
alpha:图片透明度改变
selector:自定义图片
checkBox:CheckBox场景
none:无 | 1.指定为mask时,自定义图片资源无效;
2.指定为alpha时,sle_pressedAlpha/sle_disabledAlpha生效;
3.指定为selector时,sle_normalResId/sle_pressedResId/sle_disabledResId生效;
4.指定为checkBox时,sle_checkedResId/sle_uncheckedResId/sle_isChecked生效;
5.指定为none时,图片资源均不生效,圆角相关配置有效 | 283 | | sle_ib_style | enum | ImageView形状
normal:普通形状
rounded:圆角
oval:圆形 | 默认值:normal | 284 | | sle_normalResId | color|reference | 常态图片资源 | / | 285 | | sle_pressedResId | color|reference | 按下态图片资源 | / | 286 | | sle_disabledResId | color|reference | 不可点击态图片资源 | / | 287 | | sle_checkedResId | color|reference | 选中态checkBox图片资源 | / | 288 | | sle_uncheckedResId | color|reference | 非选中态checkBox图片资源 | / | 289 | | sle_isChecked | boolean | CheckBox是否选中 | 默认值:false | 290 | | sle_pressedAlpha | float | 按下态图片透明度 | 默认值:70% | 291 | | sle_disabledAlpha | float | 不可点击态图片透明度 | 默认值:30% | 292 | 293 | ## 使用方式 294 | 1. 添加依赖 295 | ``` 296 | implementation "io.github.freddychen:silhouette:$lastest_version" 297 | ``` 298 | Note:最新版本可在[maven central silhouette](https://search.maven.org/artifact/io.github.freddychen/silhouette)中找到。 299 | 300 | 2. 使用 301 | 由于自定义属性太多,在此就不一一列举了。下面给出几种常见的场景示例,大家可以根据自定义属性表自行编写: 302 | + 常态 303 | ![Silhouette Normal](https://raw.githubusercontent.com/FreddyChen/MarkdownPicBed/main/silhouette_normal.png) 304 | + 按下态 305 | ![Silhouette Pressed](https://raw.githubusercontent.com/FreddyChen/MarkdownPicBed/main/silhouette_pressed.png) 306 | 307 | 以上布局代码为: 308 | ``` 309 | 310 | 317 | 318 | 331 | 332 | 347 | 348 | 365 | 366 | 373 | 374 | 381 | 382 | 390 | 391 | 402 | 403 | 408 | 409 | 419 | 420 | 421 | 436 | 437 | 442 | 443 | 454 | 455 | 456 | ``` 457 | *Note:需要给组件设置**setOnClickListener**才能看到效果。* 458 | 至于更多的功能,就让大家去试试吧,篇幅有限,就不一一列举了。有任何疑问,欢迎通过**QQ群**或**微信公众号**联系我。 459 | 460 | ## 版本记录 461 | | 版本号 | 修改时间 | 版本说明 | 462 | | :--: | :--: | :--: | 463 | | 0.0.1 | 2022.02.10 | 首次提交 | 464 | | 0.0.2 | 2022.02.12 | 修改minSdk为19 | 465 | 466 | ## 写在最后 467 | 终于写完了,**Shape/Selector**在每个项目中基本都会用到,而且频率还不算低。**Silhouette**原理虽然简单,但确实能解决很多问题,这些都是平时开发中的积累,希望对大家能有所帮助。欢迎大家**star**和**fork**,让我们为**Android**开发共同贡献一份力量。另外如果有疑问欢迎加入我的QQ群:**1015178804**,同时也欢迎大家关注我的公众号:**FreddyChen**,让我们共同进步和成长。 468 | 469 | [GitHub地址](https://github.com/FreddyChen/Silhouette) -------------------------------------------------------------------------------- /silhouette/src/main/java/com/freddy/silhouette/widget/button/SleTextButton.kt: -------------------------------------------------------------------------------- 1 | package com.freddy.silhouette.widget.button 2 | 3 | import android.content.Context 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import android.graphics.drawable.DrawableContainer 7 | import android.graphics.drawable.GradientDrawable 8 | import android.graphics.drawable.StateListDrawable 9 | import android.os.Build 10 | import android.util.AttributeSet 11 | import android.view.MotionEvent 12 | import android.view.View 13 | import androidx.appcompat.widget.AppCompatTextView 14 | import com.freddy.silhouette.R 15 | import com.freddy.silhouette.config.* 16 | 17 | /** 18 | * 19 | * @author: FreddyChen 20 | * @date : 2022/02/07 06:12 21 | * @email : freddychencsc@gmail.com 22 | */ 23 | class SleTextButton : AppCompatTextView, View.OnTouchListener { 24 | 25 | @Type 26 | private var type: Int = TYPE_MASK 27 | 28 | @Shape 29 | private var shape: Int = GradientDrawable.RECTANGLE 30 | private var innerRadius: Int = 0 31 | private var innerRadiusRatio: Float = 0f 32 | private var thickness: Int = 0 33 | private var thicknessRatio: Float = 0f 34 | private var normalBackgroundColor: Int = 0 35 | private var pressedBackgroundColor: Int = 0 36 | private var disabledBackgroundColor: Int = 0 37 | private var selectedBackgroundColor: Int = 0 38 | private var strokeWidth: Int = 0 39 | private var dashWidth: Float = 0f 40 | private var dashGap: Float = 0f 41 | private var normalStrokeColor: Int = 0 42 | private var pressedStrokeColor: Int = 0 43 | private var disabledStrokeColor: Int = 0 44 | private var selectedStrokeColor: Int = 0 45 | private var normalTextColor: Int = 0 46 | private var pressedTextColor: Int = 0 47 | private var disabledTextColor: Int = 0 48 | private var selectedTextColor: Int = 0 49 | private var cornersRadius: Float = 0f 50 | private var cornersTopLeftRadius: Float = 0f 51 | private var cornersTopRightRadius: Float = 0f 52 | private var cornersBottomLeftRadius: Float = 0f 53 | private var cornersBottomRightRadius: Float = 0f 54 | private var normalGradientColors: IntArray? = null 55 | private var pressedGradientColors: IntArray? = null 56 | private var disabledGradientColors: IntArray? = null 57 | private var selectedGradientColors: IntArray? = null 58 | private var gradientOrientation: Int = GRADIENT_ORIENTATION_TOP_BOTTOM 59 | 60 | @GradientType 61 | private var gradientType: Int = GradientDrawable.LINEAR_GRADIENT 62 | private var gradientCenterX: Float = 0f 63 | private var gradientCenterY: Float = 0f 64 | private var gradientRadius: Float = 0f 65 | 66 | private var maskBackgroundColor: Int = DEFAULT_MASK_BACKGROUND_COLOR 67 | private var cancelOffset: Int = DEFAULT_CANCEL_OFFSET 68 | 69 | constructor(context: Context) : this(context, null) 70 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 71 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 72 | context, 73 | attrs, 74 | defStyleAttr 75 | ) { 76 | context.obtainStyledAttributes(attrs, R.styleable.SleTextButton, defStyleAttr, 0).apply { 77 | type = getInt(R.styleable.SleTextButton_sle_type, TYPE_MASK) 78 | shape = getInt(R.styleable.SleTextButton_sle_shape, GradientDrawable.RECTANGLE) 79 | innerRadius = getDimensionPixelSize(R.styleable.SleTextButton_sle_innerRadius, 0) 80 | innerRadiusRatio = getFloat(R.styleable.SleTextButton_sle_innerRadiusRatio, 0f) 81 | thickness = getDimensionPixelSize(R.styleable.SleTextButton_sle_thickness, 0) 82 | thicknessRatio = getFloat(R.styleable.SleTextButton_sle_thicknessRatio, 0f) 83 | normalBackgroundColor = 84 | getColor(R.styleable.SleTextButton_sle_normalBackgroundColor, 0) 85 | pressedBackgroundColor = 86 | getColor(R.styleable.SleTextButton_sle_pressedBackgroundColor, 0) 87 | disabledBackgroundColor = 88 | getColor( 89 | R.styleable.SleTextButton_sle_disabledBackgroundColor, 90 | DEFAULT_DISABLE_BACKGROUND_COLOR 91 | ) 92 | selectedBackgroundColor = 93 | getColor(R.styleable.SleTextButton_sle_selectedBackgroundColor, 0) 94 | strokeWidth = getDimensionPixelSize(R.styleable.SleTextButton_sle_strokeWidth, 0) 95 | dashWidth = getDimension(R.styleable.SleTextButton_sle_dashWidth, 0f) 96 | dashGap = getDimension(R.styleable.SleTextButton_sle_dashGap, 0f) 97 | normalStrokeColor = getColor(R.styleable.SleTextButton_sle_normalStrokeColor, 0) 98 | pressedStrokeColor = 99 | getColor(R.styleable.SleTextButton_sle_pressedStrokeColor, normalStrokeColor) 100 | disabledStrokeColor = 101 | getColor(R.styleable.SleTextButton_sle_disabledStrokeColor, normalStrokeColor) 102 | selectedStrokeColor = 103 | getColor(R.styleable.SleTextButton_sle_selectedStrokeColor, normalStrokeColor) 104 | normalTextColor = 105 | getColor(R.styleable.SleTextButton_sle_normalTextColor, currentTextColor) 106 | pressedTextColor = 107 | getColor(R.styleable.SleTextButton_sle_pressedTextColor, currentTextColor) 108 | disabledTextColor = 109 | getColor(R.styleable.SleTextButton_sle_disabledTextColor, currentTextColor) 110 | selectedTextColor = 111 | getColor(R.styleable.SleTextButton_sle_selectedTextColor,currentTextColor) 112 | cornersRadius = getDimension(R.styleable.SleTextButton_sle_cornersRadius, 0f) 113 | cornersTopLeftRadius = 114 | getDimension(R.styleable.SleTextButton_sle_cornersTopLeftRadius, 0f) 115 | cornersTopRightRadius = 116 | getDimension(R.styleable.SleTextButton_sle_cornersTopRightRadius, 0f) 117 | cornersBottomLeftRadius = 118 | getDimension(R.styleable.SleTextButton_sle_cornersBottomLeftRadius, 0f) 119 | cornersBottomRightRadius = 120 | getDimension(R.styleable.SleTextButton_sle_cornersBottomRightRadius, 0f) 121 | val normalGradientColorsResourceId = 122 | getResourceId(R.styleable.SleTextButton_sle_normalGradientColors, 0) 123 | if (normalGradientColorsResourceId != 0) { 124 | normalGradientColors = resources.getIntArray(normalGradientColorsResourceId) 125 | } 126 | val pressedGradientColorsResourceId = 127 | getResourceId(R.styleable.SleTextButton_sle_pressedGradientColors, 0) 128 | if (pressedGradientColorsResourceId != 0) { 129 | pressedGradientColors = resources.getIntArray(pressedGradientColorsResourceId) 130 | } 131 | val disabledGradientColorsResourceId = 132 | getResourceId(R.styleable.SleTextButton_sle_disabledGradientColors, 0) 133 | if (disabledGradientColorsResourceId != 0) { 134 | disabledGradientColors = resources.getIntArray(disabledGradientColorsResourceId) 135 | } 136 | val selectedGradientColorsResourceId = 137 | getResourceId(R.styleable.SleTextButton_sle_selectedGradientColors, 0) 138 | if (selectedGradientColorsResourceId != 0) { 139 | selectedGradientColors = resources.getIntArray(selectedGradientColorsResourceId) 140 | } 141 | gradientOrientation = getInt( 142 | R.styleable.SleTextButton_sle_gradientOrientation, 143 | GRADIENT_ORIENTATION_TOP_BOTTOM 144 | ) 145 | gradientType = 146 | getInt( 147 | R.styleable.SleTextButton_sle_gradientType, 148 | GradientDrawable.LINEAR_GRADIENT 149 | ) 150 | gradientCenterX = getDimension(R.styleable.SleTextButton_sle_gradientCenterX, 0f) 151 | gradientCenterY = getDimension(R.styleable.SleTextButton_sle_gradientCenterY, 0f) 152 | gradientRadius = getDimension(R.styleable.SleTextButton_sle_gradientRadius, 0f) 153 | maskBackgroundColor = getColor( 154 | R.styleable.SleTextButton_sle_maskBackgroundColor, 155 | DEFAULT_MASK_BACKGROUND_COLOR 156 | ) 157 | cancelOffset = getDimensionPixelSize( 158 | R.styleable.SleTextButton_sle_cancelOffset, 159 | DEFAULT_CANCEL_OFFSET 160 | ) 161 | recycle() 162 | } 163 | init() 164 | } 165 | 166 | // override fun onDraw(canvas: Canvas) { 167 | // val drawables = compoundDrawables 168 | // if (drawables.isNotEmpty()) { 169 | // val drawableLeft = drawables[0] 170 | // drawableLeft?.let { 171 | // val textWidth = paint.measureText(text.toString()) 172 | // val drawableWidth = drawableLeft.intrinsicWidth 173 | // val drawablePadding = compoundDrawablePadding 174 | // val bodyWidth = textWidth + drawableWidth + drawablePadding 175 | // canvas.translate((width - bodyWidth) * 1.0f / 2, 0.0f) 176 | // gravity = Gravity.CENTER_VERTICAL 177 | // } 178 | // } 179 | // super.onDraw(canvas) 180 | // } 181 | 182 | private fun init() { 183 | val normalDrawable = 184 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 185 | var pressedDrawable: GradientDrawable? = null 186 | var disabledDrawable: GradientDrawable? = null 187 | var selectedDrawable: GradientDrawable? = null 188 | when (type) { 189 | TYPE_MASK -> { 190 | pressedDrawable = getDrawable( 191 | normalBackgroundColor, 192 | normalStrokeColor, 193 | normalGradientColors 194 | ).apply { 195 | colorFilter = 196 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 197 | } 198 | disabledDrawable = 199 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 200 | } 201 | TYPE_SELECTOR -> { 202 | pressedDrawable = 203 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 204 | disabledDrawable = getDrawable( 205 | disabledBackgroundColor, 206 | disabledStrokeColor, 207 | disabledGradientColors 208 | ) 209 | } 210 | } 211 | selectedDrawable = getDrawable( 212 | selectedBackgroundColor, 213 | selectedStrokeColor, 214 | selectedGradientColors 215 | ) 216 | setTextColor(normalTextColor) 217 | background = StateListDrawable().apply { 218 | if (type != TYPE_NONE) { 219 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 220 | } 221 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 222 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 223 | addState(intArrayOf(), normalDrawable) 224 | } 225 | 226 | setOnTouchListener(this) 227 | } 228 | 229 | private fun getDrawable( 230 | backgroundColor: Int, 231 | strokeColor: Int, 232 | gradientColors: IntArray? = null 233 | ): GradientDrawable { 234 | // 背景色相关 235 | val drawable = GradientDrawable() 236 | setupColor(drawable, backgroundColor) 237 | 238 | // 形状相关 239 | (drawable.mutate() as GradientDrawable).shape = shape 240 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 241 | drawable.innerRadius = innerRadius 242 | if (innerRadiusRatio > 0f) { 243 | drawable.innerRadiusRatio = innerRadiusRatio 244 | } 245 | drawable.thickness = thickness 246 | if (thicknessRatio > 0f) { 247 | drawable.thicknessRatio = thicknessRatio 248 | } 249 | } 250 | 251 | // 描边相关 252 | if (strokeColor != 0) { 253 | (drawable.mutate() as GradientDrawable).setStroke( 254 | strokeWidth, 255 | strokeColor, 256 | dashWidth, 257 | dashGap 258 | ) 259 | } 260 | 261 | // 圆角相关 262 | setupCornersRadius( 263 | drawable, 264 | cornersRadius, 265 | cornersTopLeftRadius, 266 | cornersTopRightRadius, 267 | cornersBottomRightRadius, 268 | cornersBottomLeftRadius 269 | ) 270 | 271 | // 渐变相关 272 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 273 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 274 | (drawable.mutate() as GradientDrawable).setGradientCenter( 275 | gradientCenterX, 276 | gradientCenterY 277 | ) 278 | } 279 | gradientColors?.let { colors -> 280 | (drawable.mutate() as GradientDrawable).colors = colors 281 | } 282 | var orientation: GradientDrawable.Orientation? = null 283 | when (gradientOrientation) { 284 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 285 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 286 | } 287 | GRADIENT_ORIENTATION_TR_BL -> { 288 | orientation = GradientDrawable.Orientation.TR_BL 289 | } 290 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 291 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 292 | } 293 | GRADIENT_ORIENTATION_BR_TL -> { 294 | orientation = GradientDrawable.Orientation.BR_TL 295 | } 296 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 297 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 298 | } 299 | GRADIENT_ORIENTATION_BL_TR -> { 300 | orientation = GradientDrawable.Orientation.BL_TR 301 | } 302 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 303 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 304 | } 305 | GRADIENT_ORIENTATION_TL_BR -> { 306 | drawable.orientation = GradientDrawable.Orientation.TL_BR 307 | } 308 | } 309 | orientation?.apply { 310 | (drawable.mutate() as GradientDrawable).orientation = this 311 | } 312 | return drawable 313 | } 314 | 315 | fun setTextColor( 316 | normalTextColor: Int = this.normalTextColor, 317 | pressedTextColor: Int = this.pressedTextColor, 318 | disabledTextColor: Int = this.disabledTextColor 319 | ) { 320 | if (normalTextColor != 0) setTextColor(normalTextColor) 321 | this.normalTextColor = normalTextColor 322 | this.pressedTextColor = pressedTextColor 323 | this.disabledTextColor = disabledTextColor 324 | } 325 | 326 | fun setCornersRadius( 327 | cornersRadius: Float = this.cornersRadius, 328 | cornersTopLeftRadius: Float = this.cornersTopLeftRadius, 329 | cornersTopRightRadius: Float = this.cornersTopRightRadius, 330 | cornersBottomRightRadius: Float = this.cornersBottomRightRadius, 331 | cornersBottomLeftRadius: Float = this.cornersBottomLeftRadius, 332 | ) { 333 | if (background !is StateListDrawable) return 334 | if (background.constantState !is DrawableContainer.DrawableContainerState) return 335 | val dcs: DrawableContainer.DrawableContainerState = 336 | background.constantState as DrawableContainer.DrawableContainerState 337 | val children = dcs.children 338 | if (children.isNullOrEmpty()) return 339 | this.cornersRadius = cornersRadius 340 | this.cornersTopLeftRadius = cornersTopLeftRadius 341 | this.cornersTopLeftRadius = cornersTopLeftRadius 342 | this.cornersBottomRightRadius = cornersBottomRightRadius 343 | this.cornersBottomLeftRadius = cornersBottomLeftRadius 344 | children.forEach continuing@{ drawable -> 345 | if (drawable !is GradientDrawable) return@continuing 346 | setupCornersRadius( 347 | drawable, 348 | cornersRadius, 349 | cornersTopLeftRadius, 350 | cornersTopRightRadius, 351 | cornersBottomRightRadius, 352 | cornersBottomLeftRadius 353 | ) 354 | } 355 | } 356 | 357 | fun setStroke( 358 | strokeWidth: Int = this.strokeWidth, 359 | dashWidth: Float = this.dashWidth, 360 | dashGap: Float = this.dashGap, 361 | normalStrokeColor: Int = this.normalStrokeColor, 362 | pressedStrokeColor: Int = this.pressedStrokeColor, 363 | disabledStrokeColor: Int = this.disabledStrokeColor, 364 | selectedStrokeColor: Int = this.selectedStrokeColor 365 | ) { 366 | this.strokeWidth = strokeWidth 367 | this.dashWidth = dashWidth 368 | this.dashGap = dashGap 369 | this.normalStrokeColor = normalStrokeColor 370 | this.pressedStrokeColor = pressedStrokeColor 371 | this.disabledStrokeColor = disabledStrokeColor 372 | this.selectedStrokeColor = selectedStrokeColor 373 | } 374 | 375 | private fun setupCornersRadius( 376 | drawable: GradientDrawable, 377 | cornersRadius: Float, 378 | cornersTopLeftRadius: Float, 379 | cornersTopRightRadius: Float, 380 | cornersBottomRightRadius: Float, 381 | cornersBottomLeftRadius: Float 382 | ) { 383 | if (cornersRadius > 0f) { 384 | (drawable.mutate() as GradientDrawable).cornerRadius = cornersRadius 385 | } else { 386 | // 指定4个角点中每个角点的半径。对于每个角点,数组 387 | // 包含两个值,X半径,Y半径 388 | // 顺序为左上角、右上角、右下角、左下角 389 | (drawable.mutate() as GradientDrawable).cornerRadii = floatArrayOf( 390 | cornersTopLeftRadius, 391 | cornersTopLeftRadius, 392 | 393 | cornersTopRightRadius, 394 | cornersTopRightRadius, 395 | 396 | cornersBottomRightRadius, 397 | cornersBottomRightRadius, 398 | 399 | cornersBottomLeftRadius, 400 | cornersBottomLeftRadius, 401 | ) 402 | } 403 | } 404 | 405 | /** 406 | * 由于无法获取对应的state,所以设置背景色的话会把所有状态drawable的背景色都覆盖掉 407 | */ 408 | fun setColor(backgroundColor: Int) { 409 | if (background !is StateListDrawable) return 410 | if (background.constantState !is DrawableContainer.DrawableContainerState) return 411 | val dcs: DrawableContainer.DrawableContainerState = 412 | background.constantState as DrawableContainer.DrawableContainerState 413 | val children = dcs.children 414 | if (children.isNullOrEmpty()) return 415 | this.normalBackgroundColor = backgroundColor 416 | this.pressedBackgroundColor = backgroundColor 417 | this.disabledBackgroundColor = backgroundColor 418 | this.selectedBackgroundColor = backgroundColor 419 | children.forEach continuing@{ drawable -> 420 | if (drawable !is GradientDrawable) return@continuing 421 | setupColor(drawable, backgroundColor) 422 | } 423 | } 424 | 425 | private fun setupColor(drawable: GradientDrawable, backgroundColor: Int) { 426 | if (backgroundColor != 0) { 427 | (drawable.mutate() as GradientDrawable).setColor(backgroundColor) 428 | } 429 | } 430 | 431 | override fun setEnabled(enabled: Boolean) { 432 | super.setEnabled(enabled) 433 | if (normalTextColor == 0 || disabledTextColor == 0) return 434 | setTextColor(if (enabled) normalTextColor else disabledTextColor) 435 | } 436 | 437 | override fun onTouch(v: View, event: MotionEvent): Boolean { 438 | if(type != TYPE_SELECTOR) { 439 | return false 440 | } 441 | 442 | if(!isEnabled || !isClickable) { 443 | return false 444 | } 445 | 446 | if(normalTextColor == 0 || pressedTextColor == 0 || (normalTextColor == pressedTextColor)) { 447 | return false 448 | } 449 | 450 | when (event.action) { 451 | MotionEvent.ACTION_DOWN -> { 452 | setTextColor(pressedTextColor) 453 | } 454 | 455 | MotionEvent.ACTION_MOVE -> { 456 | val currentX = event.x 457 | val currentY = event.y 458 | if (currentX < (0 - DEFAULT_CANCEL_OFFSET) || currentX > (width + DEFAULT_CANCEL_OFFSET) || currentY < (0 - DEFAULT_CANCEL_OFFSET) || currentY > (height + DEFAULT_CANCEL_OFFSET)) { 459 | setTextColor(normalTextColor) 460 | } 461 | } 462 | 463 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 464 | setTextColor(normalTextColor) 465 | } 466 | } 467 | return false 468 | } 469 | 470 | /** 471 | * 用于在被选中时修改文字的颜色 472 | */ 473 | override fun setSelected(selected: Boolean) { 474 | super.setSelected(selected) 475 | setTextColor(if (selected) selectedTextColor else normalTextColor) 476 | } 477 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silhouette 2 | 封装的Android常用控件,比如:SleTextButton、SleImageButton、SleConstraintLayout、SleFrameLayout、SleLinearLayout、SleRelativeLayout等。使控件具备Shape、Selector等功能,省去编写shape或selector文件的繁琐步骤。另外支持N种颜色渐变,弥补原生shape文件只支持三种颜色(startColor/centerColor/endColor)的不足等。 3 | 4 | ## 文章链接 5 | [Silhouette——更方便的Shape/Selector实现方案](https://juejin.cn/post/7063098095969501191/) 6 | 7 | ## 写在前面 8 | 首先祝大家新年快乐,开工大吉。 9 | 最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:**Silhouette**。另外,考虑到**Kotlin**越来越普及,作者在开发过程中也切实感受到**Kotlin**相较于**Java**带来的便利,后续的**IM**系列文章及项目考虑用**Kotlin**重写,而且考虑到由于工作业务需求过多可能出现断更的情况,所以打算一次性写完再放出来,避免大家学习不方便。 10 | 废话不多说,直接开始吧。 11 | 12 | ## Silhouette是什么? 13 | **Silhouette**意为“剪影”,取名并没有特别的含义,只是单纯地觉得意境较美。例如上一篇文章[Shine——更简单的Android网络请求库封装](https://juejin.cn/post/7054105794840625160)的网络请求库:[Shine](https://github.com/FreddyChen/Shine-Kotlin)即意为“闪耀”,也没有特别的含义,只是作者认为开源库起名较难,特意找一些比较优美的单词。 14 | **Silhouette**是一系列基于**GradientDrawable**及**StateListDrawable**封装的组件集合,主要用于实现在**Android Layout XML**中直接支持**Shape/Selector**等功能。 15 | 我们都知道在**Android**开发中,不同的**TextView**及**Button**各种样式(形状、背景色、描边、圆角、渐变等)的传统实现方式是在**drawable**文件夹中编写各种**shape/selector**等文件,这种方式至少会存在以下几种弊端: 16 | 1. **shape/selector**文件过多,项目体积增大; 17 | 2. **shape/selector**文件命名困难,命名规范时往往会存在功能重复的文件; 18 | 3. 功能存在局限性:例如**gradient**渐变色。传统**shape**方式只支持三种颜色过渡(**startColor/centerColor/endColor**),如果设计稿存在四种以上颜色渐变,**shape gradient**无能为力。再比如**TextView**在常态和按下态需要**同时改变背景色及文字颜色**时,传统方式只能在代码中动态设置等。 19 | 4. 开发效率低; 20 | 5. 难以维护等; 21 | 22 | 综上所述,我们迫切需要一个库来解决以上问题,**Silhouette**正具备这些能力。接下来,我们来具体看看**Silhouette**能做什么吧。 23 | 24 | ## Silhouette能做什么? 25 | 上面说到**Silhouette**是一系列组件集合,具体包含以下组件: 26 | * **SleTextButton** 27 | 基于**AppCompatTextView**封装; 28 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的能力 ; 29 | 具备不同状态(常态、按下态、不可点击态)下文字颜色指定等。 30 | 31 | * **SleImageButton** 32 | 基于**ShapeableImageView**封装; 33 | 通过指定**sle_ib_type**属性使**ImageView**支持按下态遮罩层、透明度改变、自定义图片,同时支持**CheckBox**功能; 34 | 通过指定**sle_ib_style**属性使**ImageView**支持**Normal**、圆角、圆形等形状。 35 | 36 | * **SleConstraintLayout** 37 | 基于**ConstraintLayout**封装; 38 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 39 | 40 | * **SleRelativeLayout** 41 | 基于**RelativeLayout**封装; 42 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 43 | 44 | * **SleLinearLayout** 45 | 基于**LinearLayout**封装; 46 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 47 | 48 | * **SleFrameLayout** 49 | 基于**FrameLayout**封装; 50 | 具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。 51 | 52 | ## 设计、封装思路及原理 53 | * 项目结构 54 | **com.freddy.silhouette** 55 | - **config**(配置相关,存放全局注解及公共常量、默认值等) 56 | - **ext**(**kotlin**扩展相关,可选择用或不用) 57 | - **utils**(工具类相关,可选择用或不用) 58 | - **widget**(控件相关) 59 | - **button** 60 | - **layout** 61 | 62 | 由此可见,项目结构非常简单,所以**Silhouette**也是一个比较轻量级的库。 63 | 64 | * 封装思路及原理 65 | 由于该库非常简单,实际上就是根据**Shape/Selector**进行自定义属性,从而利用**GradientDrawable**及**StateListDrawable**提供的**API**进行封装,不存在什么难度,在此就不展开讲了。 66 | 67 | 下面贴一下代码片段,基本上几个组件的实现原理都大同小异,都是利用**GradientDrawable**及**StateListDrawable**实现组件的**Shape**及**Selector**功能: 68 | ``` 69 | private fun init() { 70 | val normalDrawable = 71 | getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors) 72 | var pressedDrawable: GradientDrawable? = null 73 | var disabledDrawable: GradientDrawable? = null 74 | var selectedDrawable: GradientDrawable? = null 75 | when (type) { 76 | TYPE_MASK -> { 77 | pressedDrawable = getDrawable( 78 | normalBackgroundColor, 79 | normalStrokeColor, 80 | normalGradientColors 81 | ).apply { 82 | colorFilter = 83 | PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP) 84 | } 85 | disabledDrawable = 86 | getDrawable(disabledBackgroundColor, disabledBackgroundColor) 87 | } 88 | TYPE_SELECTOR -> { 89 | pressedDrawable = 90 | getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors) 91 | disabledDrawable = getDrawable( 92 | disabledBackgroundColor, 93 | disabledStrokeColor, 94 | disabledGradientColors 95 | ) 96 | } 97 | } 98 | selectedDrawable = getDrawable( 99 | selectedBackgroundColor, 100 | selectedStrokeColor, 101 | selectedGradientColors 102 | ) 103 | setTextColor(normalTextColor) 104 | background = StateListDrawable().apply { 105 | if (type != TYPE_NONE) { 106 | addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable) 107 | } 108 | addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable) 109 | addState(intArrayOf(android.R.attr.state_selected), selectedDrawable) 110 | addState(intArrayOf(), normalDrawable) 111 | } 112 | 113 | setOnTouchListener(this) 114 | } 115 | 116 | private fun getDrawable( 117 | backgroundColor: Int, 118 | strokeColor: Int, 119 | gradientColors: IntArray? = null 120 | ): GradientDrawable { 121 | // 背景色相关 122 | val drawable = GradientDrawable() 123 | setupColor(drawable, backgroundColor) 124 | 125 | // 形状相关 126 | (drawable.mutate() as GradientDrawable).shape = shape 127 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 128 | drawable.innerRadius = innerRadius 129 | if (innerRadiusRatio > 0f) { 130 | drawable.innerRadiusRatio = innerRadiusRatio 131 | } 132 | drawable.thickness = thickness 133 | if (thicknessRatio > 0f) { 134 | drawable.thicknessRatio = thicknessRatio 135 | } 136 | } 137 | 138 | // 描边相关 139 | if (strokeColor != 0) { 140 | (drawable.mutate() as GradientDrawable).setStroke( 141 | strokeWidth, 142 | strokeColor, 143 | dashWidth, 144 | dashGap 145 | ) 146 | } 147 | 148 | // 圆角相关 149 | setupCornersRadius( 150 | drawable, 151 | cornersRadius, 152 | cornersTopLeftRadius, 153 | cornersTopRightRadius, 154 | cornersBottomRightRadius, 155 | cornersBottomLeftRadius 156 | ) 157 | 158 | // 渐变相关 159 | (drawable.mutate() as GradientDrawable).gradientType = gradientType 160 | if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) { 161 | (drawable.mutate() as GradientDrawable).setGradientCenter( 162 | gradientCenterX, 163 | gradientCenterY 164 | ) 165 | } 166 | gradientColors?.let { colors -> 167 | (drawable.mutate() as GradientDrawable).colors = colors 168 | } 169 | var orientation: GradientDrawable.Orientation? = null 170 | when (gradientOrientation) { 171 | GRADIENT_ORIENTATION_TOP_BOTTOM -> { 172 | orientation = GradientDrawable.Orientation.TOP_BOTTOM 173 | } 174 | GRADIENT_ORIENTATION_TR_BL -> { 175 | orientation = GradientDrawable.Orientation.TR_BL 176 | } 177 | GRADIENT_ORIENTATION_RIGHT_LEFT -> { 178 | orientation = GradientDrawable.Orientation.RIGHT_LEFT 179 | } 180 | GRADIENT_ORIENTATION_BR_TL -> { 181 | orientation = GradientDrawable.Orientation.BR_TL 182 | } 183 | GRADIENT_ORIENTATION_BOTTOM_TOP -> { 184 | orientation = GradientDrawable.Orientation.BOTTOM_TOP 185 | } 186 | GRADIENT_ORIENTATION_BL_TR -> { 187 | orientation = GradientDrawable.Orientation.BL_TR 188 | } 189 | GRADIENT_ORIENTATION_LEFT_RIGHT -> { 190 | orientation = GradientDrawable.Orientation.LEFT_RIGHT 191 | } 192 | GRADIENT_ORIENTATION_TL_BR -> { 193 | drawable.orientation = GradientDrawable.Orientation.TL_BR 194 | } 195 | } 196 | orientation?.apply { 197 | (drawable.mutate() as GradientDrawable).orientation = this 198 | } 199 | return drawable 200 | } 201 | ``` 202 | 感兴趣的同学可以到官方文档了解**GradientDrawable**及**StateListDrawable**的原理。 203 | 204 | ## 自定义属性列表 205 | 自定义属性分为**通用属性**和**特有属性**。 206 | * **通用属性** 207 | 208 | - 类型 209 | | 属性名称 | 类型 | 说明 | 备注 | 210 | | -- | :--: | :-- | -- | 211 | | sle_type | enum | 类型
mask:遮罩
selector:自定义样式
none:无 | 默认值:mask
默认的mask为90%透明度黑色,可通过sle_maskBackgroundColors属性设置
若不指定为selector,则自定义样式无效 | 212 | 213 | - 形状相关 214 | | 属性名称 | 类型 | 说明 | 备注 | 215 | | -- | :--: | :-- | :--: | 216 | | sle_shape | enum | 形状
rectangle:矩形
oval:椭圆形
line:线性形状
ring:环形 | 默认值:rectangle | 217 | | sle_innerRadius | dimension|reference | 尺寸,内环的半径 | shape="ring"可用 | 218 | | sle_innerRadiusRatio | float | 以环的宽度比率来表示内环的半径 | shape="ring"可用 | 219 | | sle_thickness | dimension|reference | 尺寸,环的厚度 | shape="ring"可用 | 220 | | sle_thicknessRatio | float | 以环的宽度比率来表示环的厚度 | shape="ring"可用 | 221 | 222 | - 背景色相关 223 | | 属性名称 | 类型 | 说明 | 备注 | 224 | | :-- | :--: | :--: | :--: | 225 | | sle_normalBackgroundColor | color|reference | 常态背景颜色 | / | 226 | | sle_pressedBackgroundColor | color|reference | 按下态背景颜色 | / | 227 | | sle_disabledBackgroundColor | color|reference | 不可点击态背景颜色 | 默认值:#CCCCCC | 228 | | sle_selectedBackgroundColor | color|reference | 选中态背景颜色 | / | 229 | 230 | - 描边相关 231 | | 属性名称 | 类型 | 说明 | 备注 | 232 | | :-- | :--: | :--: | :--: | 233 | | sle_normalStrokeColor | color|reference | 常态描边颜色 | / | 234 | | sle_pressedStrokeColor | color|reference | 按下态描边颜色 | / | 235 | | sle_disabledStrokeColor | color|reference | 不可点击态描边颜色 | / | 236 | | sle_selectedStrokeColor | color|reference | 选中态描边颜色 | / | 237 | | sle_strokeWidth | dimension|reference | 描边宽度 | / | 238 | | sle_dashWidth | dimension|reference | 虚线宽度 | / | 239 | | sle_dashGap | dimension|reference | 虚线间隔 | / | 240 | 241 | - 圆角相关 242 | | 属性名称 | 类型 | 说明 | 备注 | 243 | | :-- | :--: | :--: | :--: | 244 | | sle_cornersRadius | dimension|reference | 总圆角半径 | / | 245 | | sle_cornersTopLeftRadius | dimension|reference | 左上角圆角半径 | / | 246 | | sle_cornersTopRightRadius | dimension|reference | 右上角圆角半径 | / | 247 | | sle_cornersBottomLeftRadius | dimension|reference | 左下角圆角半径 | / | 248 | | sle_cornersBottomRightRadius | dimension|reference | 右下角圆角半径 | / | 249 | 250 | - 渐变相关 251 | | 属性名称 | 类型 | 说明 | 备注 | 252 | | :-- | :--: | :-- | :--: | 253 | | sle_normalGradientColors | reference | 常态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 254 | | sle_pressedGradientColors | reference | 按下态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 255 | | sle_disabledGradientColors | reference | 不可点击态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 256 | | sle_selectedGradientColors | reference | 选中态渐变背景色 | 支持在res/array下定义数组实现多个颜色渐变 | 257 | | sle_gradientOrientation | enum | 渐变方向
TOP_BOTTOM:从上到下
TR_BL:从右上到左下
RIGHT_LEFT:从右到左
BR_TL:从右下到左上
BOTTOM_TOP:从下到上
BL_TR:从左下到右上
LEFT_RIGHT:从左到右
TL_BR:从左上到右下 | / | 258 | | sle_gradientType | enum | 渐变类型
linear:线性渐变
radial:圆形渐变,起始颜色从gradientCenterX、gradientCenterY点开始
sweep:A sweeping line gradient | / | 259 | | sle_gradientCenterX | float | 渐变中心放射点x坐标 | 注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点 | 260 | | sle_gradientCenterY | float | 渐变中心放射点y坐标 | 注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点 | 261 | | sle_gradientRadius | dimension|reference | 渐变半径 | 需要配合gradientType=radial使用,如果设置gradientType=radial而没有设置gradientRadius,将会报错 | 262 | 263 | - 其它 264 | | 属性名称 | 类型 | 说明 | 备注 | 265 | | :-- | :--: | :--: | :--: | 266 | | sle_maskBackgroundColor | color|reference | 当sle_type=mask时,按钮按下状态的遮罩颜色 | 默认值:90%透明度黑色(#1A000000) | 267 | | sle_cancelOffset | dimension|reference | 用于解决手指移出控件区域判断为cancel的偏移量 | 默认值:8dp | 268 | 269 | * **特有属性** 270 | - **SleConstraintLayout/SleRelativeLayout/SleFrameLayout/SleLinearLayout** 271 | | 属性名称 | 类型 | 说明 | 备注 | 272 | | :-- | :--: | :-- | :-- | 273 | | sle_interceptType | enum | 事件拦截类型
intercept_super:return super
intercept_true:return true
intercept_false:return false | Layout组件设置此值,可实现是否拦截事件,如果设置为intercept_true,事件将不传递到子控件,在某些场景比较实用 | 274 | 275 | - **SleTextButton** 276 | | 属性名称 | 类型 | 说明 | 备注 | 277 | | :-- | :--: | :--: | :--: | 278 | | sle_normalTextColor | color|reference | 常态文字颜色 | / | 279 | | sle_pressedTextColor | color|reference | 按下态文字颜色 | / | 280 | | sle_disabledTextColor | color|reference | 不可点击态文字颜色 | / | 281 | | sle_selectedTextColor | color|reference | 选中态文字颜色 | / | 282 | 283 | - **SleImageButton** 284 | | 属性名称 | 类型 | 说明 | 备注 | 285 | | :-- | :--: | :-- | :-- | 286 | | sle_ib_type | enum | 类型
mask:图片遮罩
alpha:图片透明度改变
selector:自定义图片
checkBox:CheckBox场景
none:无 | 1.指定为mask时,自定义图片资源无效;
2.指定为alpha时,sle_pressedAlpha/sle_disabledAlpha生效;
3.指定为selector时,sle_normalResId/sle_pressedResId/sle_disabledResId生效;
4.指定为checkBox时,sle_checkedResId/sle_uncheckedResId/sle_isChecked生效;
5.指定为none时,图片资源均不生效,圆角相关配置有效 | 287 | | sle_ib_style | enum | ImageView形状
normal:普通形状
rounded:圆角
oval:圆形 | 默认值:normal | 288 | | sle_normalResId | color|reference | 常态图片资源 | / | 289 | | sle_pressedResId | color|reference | 按下态图片资源 | / | 290 | | sle_disabledResId | color|reference | 不可点击态图片资源 | / | 291 | | sle_checkedResId | color|reference | 选中态checkBox图片资源 | / | 292 | | sle_uncheckedResId | color|reference | 非选中态checkBox图片资源 | / | 293 | | sle_isChecked | boolean | CheckBox是否选中 | 默认值:false | 294 | | sle_pressedAlpha | float | 按下态图片透明度 | 默认值:70% | 295 | | sle_disabledAlpha | float | 不可点击态图片透明度 | 默认值:30% | 296 | 297 | ## 使用方式 298 | 1. 添加依赖 299 | ``` 300 | implementation "io.github.freddychen:silhouette:$lastest_version" 301 | ``` 302 | Note:最新版本可在[maven central silhouette](https://search.maven.org/artifact/io.github.freddychen/silhouette)中找到。 303 | 304 | 2. 使用 305 | 由于自定义属性太多,在此就不一一列举了。下面给出几种常见的场景示例,大家可以根据自定义属性表自行编写: 306 | + 常态 307 | ![Silhouette Normal](https://raw.githubusercontent.com/FreddyChen/MarkdownPicBed/main/silhouette_normal.png) 308 | + 按下态 309 | ![Silhouette Pressed](https://raw.githubusercontent.com/FreddyChen/MarkdownPicBed/main/silhouette_pressed.png) 310 | 311 | 以上布局代码为: 312 | ``` 313 | 314 | 321 | 322 | 335 | 336 | 351 | 352 | 369 | 370 | 377 | 378 | 385 | 386 | 394 | 395 | 406 | 407 | 412 | 413 | 423 | 424 | 425 | 440 | 441 | 446 | 447 | 458 | 459 | 460 | ``` 461 | *Note:需要给组件设置**setOnClickListener**才能看到效果。* 462 | 至于更多的功能,就让大家去试试吧,篇幅有限,就不一一列举了。有任何疑问,欢迎通过**QQ群**或**微信公众号**联系我。 463 | 464 | ## 版本记录 465 | | 版本号 | 修改时间 | 版本说明 | 466 | | :--: | :--: | :--: | 467 | | 0.0.1 | 2022.02.10 | 首次提交 | 468 | | 0.0.2 | 2022.02.12 | 修改minSdk为19 | 469 | 470 | ## 写在最后 471 | 终于写完了,**Shape/Selector**在每个项目中基本都会用到,而且频率还不算低。**Silhouette**原理虽然简单,但确实能解决很多问题,这些都是平时开发中的积累,希望对大家能有所帮助。欢迎大家**star**和**fork**,让我们为**Android**开发共同贡献一份力量。另外如果有疑问欢迎加入我的QQ群:**1015178804**,同时也欢迎大家关注我的公众号:**FreddyChen**,让我们共同进步和成长。 472 | 473 | # License 474 | 475 | 476 | Copyright 2022, chenshichao 477 | 478 | Licensed under the Apache License, Version 2.0 (the "License"); 479 | you may not use this file except in compliance with the License. 480 | You may obtain a copy of the License at 481 | 482 | http://www.apache.org/licenses/LICENSE-2.0 483 | 484 | Unless required by applicable law or agreed to in writing, software 485 | distributed under the License is distributed on an "AS IS" BASIS, 486 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 487 | See the License for the specific language governing permissions and 488 | limitations under the License. 489 | 490 | 491 | --------------------------------------------------------------------------------