├── 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 | 
304 | + 按下态
305 | 
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 | 
308 | + 按下态
309 | 
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 |
--------------------------------------------------------------------------------