├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── taylor │ │ └── com │ │ └── ui │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── taylor │ │ │ └── com │ │ │ ├── adapter │ │ │ ├── MyAdapter.kt │ │ │ └── MyViewHolder.kt │ │ │ ├── bean │ │ │ ├── ColorBean.kt │ │ │ └── User.kt │ │ │ ├── dsl │ │ │ ├── Layout.kt │ │ │ ├── LayoutBuilder.kt │ │ │ ├── LayoutConstants.kt │ │ │ ├── LayoutExtra.kt │ │ │ ├── LayoutHelperFun.kt │ │ │ ├── SelectorBuilder.kt │ │ │ └── ShapeBuilder.kt │ │ │ ├── ui │ │ │ ├── FirstFragment.kt │ │ │ └── MainActivity.kt │ │ │ └── views │ │ │ ├── LineFeedLayout.kt │ │ │ ├── OneViewGroup.kt │ │ │ ├── PercentLayout.kt │ │ │ ├── ProgressBar.kt │ │ │ ├── Selector.kt │ │ │ └── SelectorGroup.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_orange_btn.xml │ │ ├── diamond_tag.webp │ │ ├── ic_back_black.webp │ │ ├── ic_launcher_background.xml │ │ ├── ic_member_more.png │ │ ├── tag_checked_shape.xml │ │ └── user_portrait_gender_female.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── taylor │ └── com │ └── ui │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Layout_dsl -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 123 |
124 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 1.8 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Layout_DSL 2 | Build Android layout dynamically with kotlin, get rid of xml file, which has poor performance 3 | 4 | 5 | ```kotlin 6 | class MainActivity : AppCompatActivity() { 7 | // build layout dynamically by DSL 8 | private val contentView by lazy { 9 | ConstraintLayout { 10 | layout_width = match_parent 11 | layout_height = match_parent 12 | 13 | TextView { 14 | layout_width = wrap_content 15 | layout_height = wrap_content 16 | text = "commit" 17 | textSize = 30f 18 | gravity = gravity_center 19 | center_horizontal = true 20 | top_toTopOf = parent_id 21 | padding = 10 22 | } 23 | } 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | // set layout to content view 29 | setContentView(contentView) 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | 9 | defaultConfig { 10 | applicationId "taylor.com.layout_dsl" 11 | minSdkVersion 17 12 | targetSdkVersion 29 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 | 26 | // To inline the bytecode built with JVM target 1.8 into 27 | // bytecode that is being built with JVM target 1.6. (e.g. navArgs) 28 | 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | 38 | } 39 | 40 | dependencies { 41 | implementation fileTree(dir: 'libs', include: ['*.jar']) 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 43 | implementation 'androidx.appcompat:appcompat:1.1.0' 44 | implementation 'androidx.core:core-ktx:1.2.0' 45 | implementation 'com.google.android.material:material:1.1.0' 46 | implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2' 47 | implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 50 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.0.1' 52 | //Glide 53 | implementation 'com.github.bumptech.glide:glide:3.7.0' 54 | implementation 'jp.wasabeef:glide-transformations:2.0.0' 55 | implementation 'androidx.viewpager2:viewpager2:1.0.0' 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/taylor/com/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.ui 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("taylor.com.layout_dsl", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/adapter/MyAdapter.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | import taylor.com.bean.User 6 | import taylor.com.dsl.* 7 | 8 | /** 9 | * show how use layout dsl in [RecyclerView.Adapter] 10 | */ 11 | class MyAdapter(var myBean: List?) : RecyclerView.Adapter() { 12 | 13 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { 14 | val itemView = parent.context.run { 15 | ConstraintLayout { 16 | layout_height = 90 17 | layout_width = match_parent 18 | margin_end = 20 19 | margin_start =20 20 | background_color = "#eeeeee" 21 | 22 | TextView { 23 | layout_id = "tvContent" 24 | layout_width = match_parent 25 | layout_height = wrap_content 26 | textSize = 15f 27 | gravity = gravity_center 28 | start_toStartOf = parent_id 29 | top_toTopOf = parent_id 30 | } 31 | 32 | View { 33 | layout_id = "vDivider" 34 | layout_width = match_parent 35 | layout_height = 1 36 | top_toBottomOf = "tvContent" 37 | background_color = "#888888" 38 | } 39 | 40 | TextView { 41 | layout_id = "tvStart" 42 | layout_width = wrap_content 43 | layout_height = wrap_content 44 | textSize = 26f 45 | textColor ="#3F4658" 46 | text = "start" 47 | start_toStartOf = parent_id 48 | top_toBottomOf = "vDivider" 49 | margin_top = 20 50 | } 51 | 52 | TextView { 53 | layout_id = "tvEnd" 54 | layout_width = wrap_content 55 | layout_height = wrap_content 56 | textSize = 26f 57 | textColor ="#3F4658" 58 | text = "end" 59 | end_toEndOf = parent_id 60 | align_vertical_to = "tvStart" 61 | } 62 | 63 | 64 | } 65 | } 66 | return MyViewHolder(itemView) 67 | } 68 | 69 | override fun getItemCount(): Int { 70 | return myBean?.size ?: 0 71 | } 72 | 73 | override fun onBindViewHolder(holder: MyViewHolder, position: Int) { 74 | myBean?.get(position)?.let { holder.bind(it) } 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/adapter/MyViewHolder.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.adapter 2 | 3 | import android.graphics.Color 4 | import android.view.View 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import taylor.com.bean.User 8 | import taylor.com.dsl.find 9 | 10 | class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 11 | 12 | fun bind(user: User?) { 13 | itemView.apply { 14 | find("tvContent")?.apply { 15 | text = user?.name ?: "no name" 16 | val color = if (user?.gender == 1) Color.parseColor("#b300ff00") else Color.parseColor("#b3ff00ff") 17 | setBackgroundColor(color) 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/bean/ColorBean.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.bean 2 | 3 | data class ColorBean(val color:String?) -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/bean/User.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.bean 2 | 3 | data class User (var name:String,var age:Int,var gender:Int = 0) -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/Layout.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Bitmap 5 | import android.graphics.Color 6 | import android.graphics.drawable.Drawable 7 | import android.graphics.drawable.GradientDrawable 8 | import android.graphics.drawable.StateListDrawable 9 | import android.text.Editable 10 | import android.text.InputFilter 11 | import android.text.InputFilter.LengthFilter 12 | import android.view.KeyEvent 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import android.widget.* 16 | import androidx.appcompat.content.res.AppCompatResources 17 | import androidx.constraintlayout.helper.widget.Flow 18 | import androidx.constraintlayout.widget.* 19 | import androidx.core.content.ContextCompat 20 | import androidx.core.content.res.ResourcesCompat 21 | import androidx.core.view.MarginLayoutParamsCompat 22 | import androidx.core.widget.NestedScrollView 23 | import androidx.recyclerview.widget.RecyclerView 24 | import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat 25 | 26 | // 27 | inline var View.layout_id: String 28 | get() { 29 | return "" 30 | } 31 | set(value) { 32 | id = value.toLayoutId() 33 | } 34 | inline var View.padding_top: Number 35 | get() { 36 | return 0 37 | } 38 | set(value) { 39 | setPadding(paddingLeft, value.dp, paddingRight, paddingBottom) 40 | } 41 | 42 | inline var View.padding_bottom: Number 43 | get() { 44 | return 0 45 | } 46 | set(value) { 47 | setPadding(paddingLeft, paddingTop, paddingRight, value.dp) 48 | } 49 | 50 | inline var View.padding_start: Number 51 | get() { 52 | return 0 53 | } 54 | set(value) { 55 | setPadding(value.dp, paddingTop, paddingRight, paddingBottom) 56 | } 57 | 58 | inline var View.padding_end: Number 59 | get() { 60 | return 0 61 | } 62 | set(value) { 63 | setPadding(paddingLeft, paddingTop, value.dp, paddingBottom) 64 | } 65 | 66 | inline var View.padding: Number 67 | get() { 68 | return 0 69 | } 70 | set(value) { 71 | setPadding(value.dp, value.dp, value.dp, value.dp) 72 | } 73 | 74 | inline var View.padding_horizontal: Number 75 | get() { 76 | return 0 77 | } 78 | set(value) { 79 | padding_start = value 80 | padding_end = value 81 | } 82 | 83 | inline var View.padding_vertical: Number 84 | get() { 85 | return 0 86 | } 87 | set(value) { 88 | padding_top = value 89 | padding_bottom = value 90 | } 91 | 92 | inline var View.layout_width: Number 93 | get() { 94 | return 0 95 | } 96 | set(value) { 97 | val w = if (value.dp > 0) value.dp else value.toInt() 98 | val h = layoutParams?.height ?: 0 99 | updateLayoutParams { 100 | width = w 101 | height = h 102 | } 103 | } 104 | 105 | inline var View.layout_height: Number 106 | get() { 107 | return 0 108 | } 109 | set(value) { 110 | val w = layoutParams?.width ?: 0 111 | val h = if (value.dp > 0) value.dp else value.toInt() 112 | updateLayoutParams { 113 | width = w 114 | height = h 115 | } 116 | } 117 | 118 | inline var View.alignParentStart: Boolean 119 | get() { 120 | return false 121 | } 122 | set(value) { 123 | if (!value) return 124 | layoutParams = RelativeLayout.LayoutParams(layoutParams.width, layoutParams.height).apply { 125 | (layoutParams as? RelativeLayout.LayoutParams)?.rules?.forEachIndexed { index, i -> 126 | addRule(index, i) 127 | } 128 | addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE) 129 | } 130 | } 131 | 132 | inline var View.alignParentEnd: Boolean 133 | get() { 134 | return false 135 | } 136 | set(value) { 137 | if (!value) return 138 | layoutParams = RelativeLayout.LayoutParams(layoutParams.width, layoutParams.height).apply { 139 | (layoutParams as? RelativeLayout.LayoutParams)?.rules?.forEachIndexed { index, i -> 140 | addRule(index, i) 141 | } 142 | addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE) 143 | } 144 | } 145 | 146 | inline var View.centerVertical: Boolean 147 | get() { 148 | return false 149 | } 150 | set(value) { 151 | if (!value) return 152 | layoutParams = RelativeLayout.LayoutParams(layoutParams.width, layoutParams.height).apply { 153 | (layoutParams as? RelativeLayout.LayoutParams)?.rules?.forEachIndexed { index, i -> 154 | addRule(index, i) 155 | } 156 | addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE) 157 | } 158 | } 159 | 160 | inline var View.centerInParent: Boolean 161 | get() { 162 | return false 163 | } 164 | set(value) { 165 | if (!value) return 166 | layoutParams = RelativeLayout.LayoutParams(layoutParams.width, layoutParams.height).apply { 167 | (layoutParams as? RelativeLayout.LayoutParams)?.rules?.forEachIndexed { index, i -> 168 | addRule(index, i) 169 | } 170 | addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE) 171 | } 172 | } 173 | 174 | inline var View.weight: Float 175 | get() { 176 | return 0f 177 | } 178 | set(value) { 179 | updateLayoutParams { 180 | gravity = (layoutParams as? LinearLayout.LayoutParams)?.gravity ?: -1 181 | weight = value 182 | } 183 | } 184 | inline var View.layout_gravity: Int 185 | get() { 186 | return -1 187 | } 188 | set(value) { 189 | updateLayoutParams { 190 | weight = (layoutParams as? LinearLayout.LayoutParams)?.weight ?: 0f 191 | gravity = value 192 | } 193 | } 194 | 195 | inline var View.toCircleOf: String 196 | get() { 197 | return "" 198 | } 199 | set(value) { 200 | updateLayoutParams { 201 | circleConstraint = value.toLayoutId() 202 | } 203 | } 204 | 205 | inline var View.circle_radius: Int 206 | get() { 207 | return -1 208 | } 209 | set(value) { 210 | updateLayoutParams { 211 | circleRadius = value.dp 212 | } 213 | } 214 | 215 | inline var View.circle_angle: Float 216 | get() { 217 | return -1f 218 | } 219 | set(value) { 220 | updateLayoutParams { 221 | circleAngle = value 222 | } 223 | } 224 | 225 | inline var View.start_toStartOf: String 226 | get() { 227 | return "" 228 | } 229 | set(value) { 230 | updateLayoutParams { 231 | startToStart = value.toLayoutId() 232 | startToEnd = -1 233 | } 234 | } 235 | 236 | inline var View.start_toStartViewOf: View? 237 | get() { 238 | return null 239 | } 240 | set(value) { 241 | updateLayoutParams { 242 | startToStart = value?.id ?: -1 243 | startToEnd = -1 244 | } 245 | } 246 | 247 | inline var View.start_toEndOf: String 248 | get() { 249 | return "" 250 | } 251 | set(value) { 252 | updateLayoutParams { 253 | startToEnd = value.toLayoutId() 254 | startToStart = -1 255 | } 256 | } 257 | 258 | inline var View.start_toEndViewOf: View? 259 | get() { 260 | return null 261 | } 262 | set(value) { 263 | updateLayoutParams { 264 | startToEnd = value?.id ?: -1 265 | startToStart = -1 266 | } 267 | } 268 | 269 | inline var View.top_toBottomOf: String 270 | get() { 271 | return "" 272 | } 273 | set(value) { 274 | updateLayoutParams { 275 | topToBottom = value.toLayoutId() 276 | topToTop = -1 277 | } 278 | } 279 | 280 | inline var View.top_toBottomViewOf: View? 281 | get() { 282 | return null 283 | } 284 | set(value) { 285 | updateLayoutParams { 286 | topToBottom = value?.id ?: -1 287 | topToTop = -1 288 | } 289 | } 290 | 291 | inline var View.top_toTopOf: String 292 | get() { 293 | return "" 294 | } 295 | set(value) { 296 | updateLayoutParams { 297 | topToTop = value.toLayoutId() 298 | topToBottom = -1 299 | } 300 | } 301 | 302 | inline var View.top_toTopViewOf: View? 303 | get() { 304 | return null 305 | } 306 | set(value) { 307 | 308 | updateLayoutParams { 309 | topToTop = value?.id ?: -1 310 | topToBottom = -1 311 | } 312 | } 313 | 314 | inline var View.end_toEndOf: String 315 | get() { 316 | return "" 317 | } 318 | set(value) { 319 | updateLayoutParams { 320 | endToEnd = value.toLayoutId() 321 | endToStart = -1 322 | } 323 | } 324 | 325 | inline var View.end_toEndViewOf: View? 326 | get() { 327 | return null 328 | } 329 | set(value) { 330 | updateLayoutParams { 331 | endToEnd = value?.id ?: -1 332 | endToStart = -1 333 | } 334 | } 335 | 336 | inline var View.end_toStartOf: String 337 | get() { 338 | return "" 339 | } 340 | set(value) { 341 | updateLayoutParams { 342 | endToStart = value.toLayoutId() 343 | endToEnd = -1 344 | } 345 | } 346 | 347 | inline var View.end_toStartViewOf: View? 348 | get() { 349 | return null 350 | } 351 | set(value) { 352 | updateLayoutParams { 353 | endToStart = value?.id ?: -1 354 | endToEnd = -1 355 | } 356 | } 357 | 358 | inline var View.bottom_toBottomOf: String 359 | get() { 360 | return "" 361 | } 362 | set(value) { 363 | updateLayoutParams { 364 | bottomToBottom = value.toLayoutId() 365 | bottomToTop = -1 366 | } 367 | } 368 | 369 | inline var View.bottom_toBottomViewOf: View? 370 | get() { 371 | return null 372 | } 373 | set(value) { 374 | updateLayoutParams { 375 | bottomToBottom = value?.id ?: -1 376 | bottomToTop = -1 377 | } 378 | } 379 | 380 | inline var View.bottom_toTopOf: String 381 | get() { 382 | return "" 383 | } 384 | set(value) { 385 | updateLayoutParams { 386 | bottomToTop = value.toLayoutId() 387 | bottomToBottom = -1 388 | } 389 | } 390 | 391 | inline var View.bottom_toTopViewOf: View? 392 | get() { 393 | return null 394 | } 395 | set(value) { 396 | updateLayoutParams { 397 | bottomToTop = value?.id ?: -1 398 | bottomToBottom = -1 399 | } 400 | } 401 | 402 | inline var View.horizontal_chain_style: Int 403 | get() { 404 | return -1 405 | } 406 | set(value) { 407 | updateLayoutParams { 408 | horizontalChainStyle = value 409 | } 410 | } 411 | 412 | inline var View.vertical_chain_style: Int 413 | get() { 414 | return -1 415 | } 416 | set(value) { 417 | updateLayoutParams { 418 | verticalChainStyle = value 419 | } 420 | } 421 | 422 | inline var View.horizontal_bias: Float 423 | get() { 424 | return -1f 425 | } 426 | set(value) { 427 | updateLayoutParams { 428 | horizontalBias = value 429 | } 430 | } 431 | 432 | inline var View.dimension_radio: String 433 | get() { 434 | return "" 435 | } 436 | set(value) { 437 | updateLayoutParams { 438 | dimensionRatio = value 439 | } 440 | } 441 | 442 | inline var View.vertical_bias: Float 443 | get() { 444 | return -1f 445 | } 446 | set(value) { 447 | updateLayoutParams { 448 | verticalBias = value 449 | } 450 | } 451 | 452 | inline var View.center_horizontal: Boolean 453 | get() { 454 | return false 455 | } 456 | set(value) { 457 | if (!value) return 458 | start_toStartOf = parent_id 459 | end_toEndOf = parent_id 460 | } 461 | 462 | inline var View.center_vertical: Boolean 463 | get() { 464 | return false 465 | } 466 | set(value) { 467 | if (!value) return 468 | top_toTopOf = parent_id 469 | bottom_toBottomOf = parent_id 470 | } 471 | 472 | inline var View.align_vertical_to: String 473 | get() { 474 | return "" 475 | } 476 | set(value) { 477 | top_toTopOf = value 478 | bottom_toBottomOf = value 479 | } 480 | 481 | inline var View.align_horizontal_to: String 482 | get() { 483 | return "" 484 | } 485 | set(value) { 486 | start_toStartOf = value 487 | end_toEndOf = value 488 | } 489 | 490 | inline var View.width_percentage: Float 491 | get() { 492 | return -1f 493 | } 494 | set(value) { 495 | updateLayoutParams { 496 | width = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT 497 | matchConstraintPercentWidth = value 498 | } 499 | } 500 | 501 | inline var View.height_percentage: Float 502 | get() { 503 | return -1f 504 | } 505 | set(value) { 506 | updateLayoutParams { 507 | height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT 508 | matchConstraintPercentHeight = value 509 | } 510 | } 511 | 512 | inline var View.background_color: String 513 | get() { 514 | return "" 515 | } 516 | set(value) { 517 | setBackgroundColor(Color.parseColor(value)) 518 | } 519 | 520 | inline var View.background_color_res: Int 521 | get() { 522 | return -1 523 | } 524 | set(value) { 525 | setBackgroundColor(ContextCompat.getColor(context, value)) 526 | } 527 | 528 | inline var View.background_res: Int 529 | get() { 530 | return -1 531 | } 532 | set(value) { 533 | background = AppCompatResources.getDrawable(context, value) 534 | } 535 | 536 | inline var View.background_vector: Int 537 | get() { 538 | return -1 539 | } 540 | set(value) { 541 | val drawable = VectorDrawableCompat.create(context.getResources(), value, null) 542 | background = drawable 543 | } 544 | 545 | inline var View.background_drawable: Drawable? 546 | get() { 547 | return null 548 | } 549 | set(value) { 550 | value?.let { background = it } 551 | } 552 | 553 | inline var View.background_drawable_state_list: List> 554 | get() { 555 | return listOf(intArrayOf() to GradientDrawable()) 556 | } 557 | set(value) { 558 | background = StateListDrawable().apply { 559 | value.forEach { pair -> 560 | addState(pair.first, pair.second) 561 | } 562 | } 563 | } 564 | 565 | inline var View.margin_top: Number 566 | get() { 567 | return -1 568 | } 569 | set(value) { 570 | updateLayoutParams { 571 | topMargin = value.dp 572 | } 573 | } 574 | 575 | inline var View.margin_bottom: Number 576 | get() { 577 | return -1 578 | } 579 | set(value) { 580 | updateLayoutParams { 581 | bottomMargin = value.dp 582 | } 583 | } 584 | 585 | inline var View.margin_start: Number 586 | get() { 587 | return -1 588 | } 589 | set(value) { 590 | updateLayoutParams { 591 | MarginLayoutParamsCompat.setMarginStart(this, value.dp) 592 | } 593 | } 594 | 595 | inline var View.margin_end: Number 596 | get() { 597 | return -1 598 | } 599 | set(value) { 600 | updateLayoutParams { 601 | MarginLayoutParamsCompat.setMarginEnd(this, value.dp) 602 | } 603 | } 604 | 605 | inline var View.margin_horizontal: Number 606 | get() { 607 | return -1 608 | } 609 | set(value) { 610 | updateLayoutParams { 611 | MarginLayoutParamsCompat.setMarginEnd(this, value.dp) 612 | MarginLayoutParamsCompat.setMarginStart(this, value.dp) 613 | } 614 | } 615 | 616 | inline var View.margin_vertical: Number 617 | get() { 618 | return -1 619 | } 620 | set(value) { 621 | updateLayoutParams { 622 | topMargin = value.dp 623 | bottomMargin = value.dp 624 | } 625 | } 626 | 627 | inline var View.gone_margin_end: Number 628 | get() { 629 | return -1 630 | } 631 | set(value) { 632 | updateLayoutParams { 633 | goneEndMargin = value.dp 634 | } 635 | } 636 | 637 | inline var View.gone_margin_start: Number 638 | get() { 639 | return -1 640 | } 641 | set(value) { 642 | updateLayoutParams { 643 | goneStartMargin = value.dp 644 | } 645 | } 646 | 647 | inline var View.gone_margin_top: Number 648 | get() { 649 | return -1 650 | } 651 | set(value) { 652 | updateLayoutParams { 653 | goneTopMargin = value.dp 654 | } 655 | } 656 | 657 | inline var View.gone_margin_bottom: Number 658 | get() { 659 | return -1 660 | } 661 | set(value) { 662 | updateLayoutParams { 663 | goneBottomMargin = value.dp 664 | } 665 | } 666 | 667 | inline var View.guide_percentage: Float 668 | get() { 669 | return -1f 670 | } 671 | set(value) { 672 | updateLayoutParams { 673 | guidePercent = value 674 | } 675 | } 676 | 677 | inline var View.guide_orientation: Int 678 | get() { 679 | return 1 680 | } 681 | set(value) { 682 | updateLayoutParams { 683 | orientation = value 684 | } 685 | } 686 | 687 | inline var View.layout_visibility: Int 688 | get() { 689 | return -1 690 | } 691 | set(value) { 692 | 693 | visibility = value 694 | } 695 | 696 | /** 697 | * bind async data 698 | */ 699 | inline var View.bindLiveData: LiveDataBinder? 700 | get() { 701 | return null 702 | } 703 | set(value) { 704 | observe(value?.liveData) { 705 | value?.action?.invoke(it) 706 | } 707 | } 708 | 709 | /** 710 | * old fashion for binding data 711 | */ 712 | inline var View.bind: Binder? 713 | get() { 714 | return null 715 | } 716 | set(value) { 717 | value?.action?.invoke(this, value.data) 718 | } 719 | 720 | /** 721 | * bind sync data 722 | */ 723 | inline var View.bindData: () -> Unit 724 | get() { 725 | return {} 726 | } 727 | set(value) { 728 | value() 729 | } 730 | 731 | inline var View.fitsSystemWindows: Boolean 732 | get() { 733 | return false 734 | } 735 | set(value) { 736 | fitsSystemWindows = value 737 | } 738 | 739 | /** 740 | * use this attribute to build shape dynamically, getting rid of "shape.xml" 741 | */ 742 | inline var View.shape: GradientDrawable 743 | get() { 744 | return GradientDrawable() 745 | } 746 | set(value) { 747 | background = value 748 | } 749 | 750 | inline var ImageView.src: Int 751 | get() { 752 | return -1 753 | } 754 | set(value) { 755 | setImageDrawable(AppCompatResources.getDrawable(context, value)) 756 | } 757 | 758 | inline var ImageView.imageDrawable: Drawable? 759 | get() { 760 | return null 761 | } 762 | set(value) { 763 | setImageDrawable(value) 764 | } 765 | 766 | inline var ImageView.bitmap: Bitmap? 767 | get() { 768 | return null 769 | } 770 | set(value) { 771 | setImageBitmap(value) 772 | } 773 | 774 | inline var TextView.maxLength: Int 775 | get() { 776 | return 1 777 | } 778 | set(value) { 779 | filters = arrayOf(LengthFilter(value)) 780 | } 781 | 782 | inline var TextView.text_res: Int 783 | get() { 784 | return -1 785 | } 786 | set(value) { 787 | setText(value) 788 | } 789 | 790 | inline var TextView.hint_color: String 791 | get() { 792 | return "" 793 | } 794 | set(value) { 795 | setHintTextColor(Color.parseColor(value)) 796 | } 797 | 798 | inline var TextView.hint_color_res: Int 799 | get() { 800 | return -1 801 | } 802 | set(value) { 803 | setHintTextColor(ContextCompat.getColor(context, value)) 804 | } 805 | 806 | inline var TextView.hint_text_res: Int 807 | get() { 808 | return -1 809 | } 810 | set(value) { 811 | setHint(value) 812 | } 813 | 814 | inline var TextView.hint_text: String 815 | get() { 816 | return "" 817 | } 818 | set(value) { 819 | setHint(value) 820 | } 821 | 822 | inline var TextView.line_space_multiplier: Float 823 | get() { 824 | return -1f 825 | } 826 | set(value) { 827 | setLineSpacing(lineSpacingExtra, value) 828 | } 829 | 830 | inline var TextView.line_space_extra: Float 831 | get() { 832 | return -1f 833 | } 834 | set(value) { 835 | setLineSpacing(value, lineSpacingMultiplier) 836 | } 837 | 838 | inline var TextView.textStyle: Int 839 | get() { 840 | return -1 841 | } 842 | set(value) = setTypeface(typeface, value) 843 | 844 | inline var TextView.textColor: String 845 | get() { 846 | return "" 847 | } 848 | set(value) { 849 | setTextColor(Color.parseColor(value)) 850 | } 851 | 852 | inline var TextView.text_color_res: Int 853 | get() { 854 | return -1 855 | } 856 | set(value) { 857 | setTextColor(ContextCompat.getColor(context, value)) 858 | } 859 | 860 | inline var TextView.fontFamily: Int 861 | get() { 862 | return 1 863 | } 864 | set(value) { 865 | try { 866 | typeface = ResourcesCompat.getFont(context, value) 867 | } catch (e: Resources.NotFoundException) { 868 | } 869 | } 870 | 871 | inline var TextView.drawable_start: Int 872 | get() { 873 | return -1 874 | } 875 | set(value) { 876 | setCompoundDrawablesRelativeWithIntrinsicBounds(value, 0, 0, 0) 877 | } 878 | 879 | inline var TextView.drawable_end: Int 880 | get() { 881 | return -1 882 | } 883 | set(value) { 884 | setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, value, 0) 885 | } 886 | 887 | inline var TextView.drawable_top: Int 888 | get() { 889 | return -1 890 | } 891 | set(value) { 892 | setCompoundDrawablesRelativeWithIntrinsicBounds(0, value, 0, 0) 893 | } 894 | 895 | inline var TextView.drawable_bottom: Int 896 | get() { 897 | return -1 898 | } 899 | set(value) { 900 | setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, value) 901 | } 902 | 903 | inline var TextView.drawable_padding: Int 904 | get() { 905 | return 0 906 | } 907 | set(value) { 908 | compoundDrawablePadding = value.dp 909 | } 910 | 911 | inline var TextView.onTextChange: TextWatcher 912 | get() { 913 | return TextWatcher() 914 | } 915 | set(value) { 916 | val textWatcher = object : android.text.TextWatcher { 917 | override fun afterTextChanged(s: Editable?) { 918 | value.afterTextChanged.invoke(s) 919 | } 920 | 921 | override fun beforeTextChanged( 922 | text: CharSequence?, 923 | start: Int, 924 | count: Int, 925 | after: Int 926 | ) { 927 | value.beforeTextChanged.invoke(text, start, count, after) 928 | } 929 | 930 | override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { 931 | value.onTextChanged.invoke(text, start, before, count) 932 | } 933 | } 934 | addTextChangedListener(textWatcher) 935 | } 936 | 937 | inline var TextView.onEditorAction: EditorActionListener 938 | get() { 939 | return EditorActionListener() 940 | } 941 | set(value) { 942 | val editorActionListener = object : TextView.OnEditorActionListener { 943 | override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { 944 | return value.onEditorAction(v, actionId, event) 945 | } 946 | } 947 | setOnEditorActionListener(editorActionListener) 948 | } 949 | 950 | inline var Button.textAllCaps: Boolean 951 | get() { 952 | return false 953 | } 954 | set(value) { 955 | isAllCaps = value 956 | } 957 | 958 | 959 | inline var NestedScrollView.fadeScrollBar: Boolean 960 | get() { 961 | return false 962 | } 963 | set(value) { 964 | isScrollbarFadingEnabled = true 965 | } 966 | 967 | inline var ConstraintHelper.referenceIds: String 968 | get() { 969 | return "" 970 | } 971 | set(value) { 972 | referencedIds = value.split(",").map { it.toLayoutId() }.toIntArray() 973 | } 974 | 975 | inline var Flow.flow_horizontalGap: Int 976 | get() { 977 | return 0 978 | } 979 | set(value) { 980 | setHorizontalGap(value.dp) 981 | } 982 | 983 | inline var Flow.flow_verticalGap: Int 984 | get() { 985 | return 0 986 | } 987 | set(value) { 988 | setVerticalGap(value.dp) 989 | } 990 | 991 | inline var Flow.flow_wrapMode: Int 992 | get() { 993 | return 0 994 | } 995 | set(value) { 996 | setWrapMode(value) 997 | } 998 | 999 | inline var Flow.flow_padding: Int 1000 | get() { 1001 | return 0 1002 | } 1003 | set(value) { 1004 | setPadding(value.dp) 1005 | } 1006 | inline var Flow.flow_paddingRight: Int 1007 | get() { 1008 | return 0 1009 | } 1010 | set(value) { 1011 | setPaddingRight(value.dp) 1012 | } 1013 | 1014 | inline var Flow.flow_paddingTop: Int 1015 | get() { 1016 | return 0 1017 | } 1018 | set(value) { 1019 | setPaddingTop(value.dp) 1020 | } 1021 | inline var Flow.flow_paddingBottom: Int 1022 | get() { 1023 | return 0 1024 | } 1025 | set(value) { 1026 | setPaddingBottom(value.dp) 1027 | } 1028 | inline var Flow.flow_paddingLeft: Int 1029 | get() { 1030 | return 0 1031 | } 1032 | set(value) { 1033 | setPaddingLeft(value.dp) 1034 | } 1035 | inline var Flow.flow_orientation: Int 1036 | get() { 1037 | return 0 1038 | } 1039 | set(value) { 1040 | setOrientation(value) 1041 | } 1042 | inline var Flow.flow_horizontalStyle: Int 1043 | get() { 1044 | return 0 1045 | } 1046 | set(value) { 1047 | setHorizontalStyle(value) 1048 | } 1049 | inline var Flow.flow_verticalStyle: Int 1050 | get() { 1051 | return 0 1052 | } 1053 | set(value) { 1054 | setVerticalStyle(value) 1055 | } 1056 | inline var Flow.flow_horizontalBias: Float 1057 | get() { 1058 | return 0f 1059 | } 1060 | set(value) { 1061 | setHorizontalBias(value) 1062 | } 1063 | inline var Flow.flow_verticalBias: Float 1064 | get() { 1065 | return 0f 1066 | } 1067 | set(value) { 1068 | setVerticalBias(value) 1069 | } 1070 | inline var Flow.flow_horizontalAlign: Int 1071 | get() { 1072 | return 0 1073 | } 1074 | set(value) { 1075 | setHorizontalAlign(value) 1076 | } 1077 | inline var Flow.flow_verticalAlign: Int 1078 | get() { 1079 | return 0 1080 | } 1081 | set(value) { 1082 | setVerticalAlign(value) 1083 | } 1084 | inline var Flow.flow_maxElementWrap: Int 1085 | get() { 1086 | return 0 1087 | } 1088 | set(value) { 1089 | setMaxElementsWrap(value) 1090 | } 1091 | 1092 | 1093 | inline var ConstraintHelper.reference_ids: List 1094 | get() { 1095 | return emptyList() 1096 | } 1097 | set(value) { 1098 | referencedIds = value.map { it.toLayoutId() }.toIntArray() 1099 | } 1100 | 1101 | inline var Barrier.barrier_direction: Int 1102 | get() { 1103 | return -1 1104 | } 1105 | set(value) { 1106 | type = value 1107 | } 1108 | 1109 | var View.onClick: (View) -> Unit 1110 | get() { 1111 | return {} 1112 | } 1113 | set(value) { 1114 | setOnClickListener { v -> value(v) } 1115 | } 1116 | 1117 | var View.shakelessClick: (View) -> Unit 1118 | get() { 1119 | return {} 1120 | } 1121 | set(value) { 1122 | setShakelessClickListener(1000) { 1123 | value(it) 1124 | } 1125 | } 1126 | 1127 | var RecyclerView.onItemClick: (View, Int, Float, Float) -> Boolean 1128 | get() { 1129 | return { _, _, _, _ -> false } 1130 | } 1131 | set(value) { 1132 | setOnItemClickListener(value) 1133 | } 1134 | 1135 | var RecyclerView.onItemLongClick: (View, Int, Float, Float) -> Unit 1136 | get() { 1137 | return { _, _, _, _ -> } 1138 | } 1139 | set(value) { 1140 | setOnItemLongClickListener(value) 1141 | } 1142 | 1143 | var RecyclerView.hasFixedSize: Boolean 1144 | get() { 1145 | return false 1146 | } 1147 | set(value) { 1148 | setHasFixedSize(value) 1149 | } 1150 | // -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/LayoutBuilder.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.* 7 | import androidx.appcompat.view.ContextThemeWrapper 8 | import androidx.appcompat.widget.* 9 | import androidx.constraintlayout.helper.widget.Flow 10 | import androidx.constraintlayout.helper.widget.Layer 11 | import androidx.constraintlayout.widget.Barrier 12 | import androidx.constraintlayout.widget.ConstraintLayout 13 | import androidx.constraintlayout.widget.Guideline 14 | import androidx.core.widget.NestedScrollView 15 | import androidx.fragment.app.Fragment 16 | import androidx.fragment.app.FragmentContainerView 17 | import androidx.recyclerview.widget.RecyclerView 18 | import androidx.viewpager2.widget.ViewPager2 19 | 20 | /** 21 | * the extension functions and field in this file help you to build layout dynamically, 22 | * which has a better performance than xml files and more readable than normal java and kotlin code 23 | * 24 | * using this dsl to build view in kotlin like the following: 25 | * private val rootView by lazy { 26 | * ConstraintLayout { 27 | * layout_width = match_parent 28 | * layout_height = match_parent 29 | * 30 | * ImageView { 31 | * layout_id = "ivBack" 32 | * layout_width = 40 33 | * layout_height = 40 34 | * margin_start = 20 35 | * margin_top = 20 36 | * src = R.drawable.ic_back_black 37 | * start_toStartOf = parent_id 38 | * top_toTopOf = parent_id 39 | * onClick = onBackClick 40 | * } 41 | * 42 | * TextView { 43 | * layout_width = wrap_content 44 | * layout_height = wrap_content 45 | * text = "commit" 46 | * textSize = 30f 47 | * layout_visibility = gone 48 | * textStyle = bold 49 | * align_vertical_to = "ivBack" 50 | * center_horizontal = true 51 | * } 52 | * } 53 | * } 54 | */ 55 | 56 | 57 | /** 58 | * create [AppCompatTextView] instance within a [ViewGroup] 59 | * @param style an style int value defined in xml 60 | * @param autoAdd whether add [AppCompatTextView] into [ViewGroup] automatically 61 | * @param init set attributes for this view in this lambda 62 | */ 63 | inline fun ViewGroup.TextView( 64 | style: Int? = null, 65 | autoAdd: Boolean = true, 66 | init: AppCompatTextView.() -> Unit 67 | ): TextView { 68 | val textView = 69 | if (style != null) AppCompatTextView(ContextThemeWrapper(context, style)) 70 | else AppCompatTextView(context) 71 | return textView.apply(init).also { if (autoAdd) addView(it) } 72 | } 73 | 74 | /** 75 | * create [AppCompatImageView] instance within a [ViewGroup] 76 | * @param style an style int value defined in xml 77 | * @param autoAdd whether add [AppCompatImageView] into [ViewGroup] automatically 78 | * @param init set attributes for this view in this lambda 79 | */ 80 | inline fun ViewGroup.ImageView( 81 | style: Int? = null, 82 | autoAdd: Boolean = true, 83 | init: AppCompatImageView.() -> Unit 84 | ): ImageView { 85 | val imageView = 86 | if (style != null) AppCompatImageView( 87 | ContextThemeWrapper(context, style) 88 | ) else AppCompatImageView(context) 89 | return imageView.apply(init).also { if (autoAdd) addView(it) } 90 | } 91 | 92 | /** 93 | * create [AppCompatButton] instance within a [ViewGroup] 94 | * @param style an style int value defined in xml 95 | * @param autoAdd whether add [AppCompatButton] into [ViewGroup] automatically 96 | * @param init set attributes for this view in this lambda 97 | */ 98 | inline fun ViewGroup.Button( 99 | style: Int? = null, 100 | autoAdd: Boolean = true, 101 | init: AppCompatButton.() -> Unit 102 | ): Button { 103 | val button = 104 | if (style != null) AppCompatButton( 105 | ContextThemeWrapper(context, style) 106 | ) else AppCompatButton(context) 107 | return button.apply(init).also { if (autoAdd) addView(it) } 108 | } 109 | 110 | /** 111 | * create [View] instance within a [ViewGroup] 112 | * @param style an style int value defined in xml 113 | * @param autoAdd whether add [View] into [ViewGroup] automatically 114 | * @param init set attributes for this view in this lambda 115 | */ 116 | inline fun ViewGroup.View( 117 | style: Int? = null, 118 | autoAdd: Boolean = true, 119 | init: View.() -> Unit 120 | ): View { 121 | val view = 122 | if (style != null) View( 123 | ContextThemeWrapper(context, style) 124 | ) else View(context) 125 | return view.apply(init).also { if (autoAdd) addView(it) } 126 | } 127 | 128 | /** 129 | * create [RelativeLayout] instance within a [ViewGroup] 130 | * @param style an style int value defined in xml 131 | * @param autoAdd whether add [RelativeLayout] into [ViewGroup] automatically 132 | * @param init set attributes for this view in this lambda 133 | */ 134 | inline fun ViewGroup.RelativeLayout( 135 | style: Int? = null, 136 | autoAdd: Boolean = true, 137 | init: RelativeLayout.() -> Unit 138 | ): RelativeLayout { 139 | val relativeLayout = 140 | if (style != null) RelativeLayout( 141 | ContextThemeWrapper(context, style) 142 | ) else RelativeLayout(context) 143 | return relativeLayout.apply(init).also { if (autoAdd) addView(it) } 144 | } 145 | 146 | /** 147 | * create [LinearLayout] instance within a [ViewGroup] 148 | * @param style an style int value defined in xml 149 | * @param autoAdd whether add [LinearLayoutCompat] into [ViewGroup] automatically 150 | * @param init set attributes for this view in this lambda 151 | */ 152 | inline fun ViewGroup.LinearLayout( 153 | style: Int? = null, 154 | autoAdd: Boolean = true, 155 | init: LinearLayout.() -> Unit 156 | ): LinearLayout { 157 | val linearLayout = 158 | if (style != null) LinearLayout( 159 | ContextThemeWrapper(context, style) 160 | ) else LinearLayout(context) 161 | return linearLayout.apply(init).also { if (autoAdd) addView(it) } 162 | } 163 | 164 | /** 165 | * create [NestedScrollView] instance within a [ViewGroup] 166 | * @param style an style int value defined in xml 167 | * @param autoAdd whether add [NestedScrollView] into [ViewGroup] automatically 168 | * @param init set attributes for this view in this lambda 169 | */ 170 | inline fun ViewGroup.NestedScrollView( 171 | style: Int? = null, 172 | autoAdd: Boolean = true, 173 | init: NestedScrollView.() -> Unit 174 | ): NestedScrollView { 175 | val nestedScrollView = 176 | if (style != null) NestedScrollView( 177 | ContextThemeWrapper(context, style) 178 | ) else NestedScrollView(context) 179 | return nestedScrollView.apply(init).also { if (autoAdd) addView(it) } 180 | } 181 | 182 | /** 183 | * create [RecyclerView] instance within a [ViewGroup] 184 | * @param style an style int value defined in xml 185 | * @param autoAdd whether add [RecyclerView] into [ViewGroup] automatically 186 | * @param init set attributes for this view in this lambda 187 | */ 188 | inline fun ViewGroup.RecyclerView( 189 | style: Int? = null, 190 | autoAdd: Boolean = true, 191 | init: RecyclerView.() -> Unit 192 | ): RecyclerView { 193 | val recyclerView = 194 | if (style != null) RecyclerView( 195 | ContextThemeWrapper(context, style) 196 | ) else RecyclerView(context) 197 | return recyclerView.apply(init).also { if (autoAdd) addView(it) } 198 | } 199 | 200 | /** 201 | * create [ConstraintLayout] instance within a [ViewGroup] 202 | * @param style an style int value defined in xml 203 | * @param autoAdd whether add [ConstraintLayout] into [ViewGroup] automatically 204 | * @param init set attributes for this view in this lambda 205 | */ 206 | inline fun ViewGroup.ConstraintLayout( 207 | style: Int? = null, 208 | autoAdd: Boolean = true, 209 | init: ConstraintLayout.() -> Unit 210 | ): ConstraintLayout { 211 | val constraintLayout = 212 | if (style != null) ConstraintLayout( 213 | ContextThemeWrapper(context, style) 214 | ) else ConstraintLayout(context) 215 | return constraintLayout.apply(init).also { if (autoAdd) addView(it) } 216 | } 217 | 218 | /** 219 | * create [FrameLayout] instance within a [ViewGroup] 220 | * @param style an style int value defined in xml 221 | * @param autoAdd whether add [FrameLayout] into [ViewGroup] automatically 222 | * @param init set attributes for this view in this lambda 223 | */ 224 | inline fun ViewGroup.FrameLayout( 225 | style: Int? = null, 226 | autoAdd: Boolean = true, 227 | init: FrameLayout.() -> Unit 228 | ): FrameLayout { 229 | val frameLayout = 230 | if (style != null) FrameLayout( 231 | ContextThemeWrapper(context, style) 232 | ) else FrameLayout(context) 233 | return frameLayout.apply(init).also { if (autoAdd) addView(it) } 234 | } 235 | 236 | /** 237 | * create [ViewFlipper] instance within a [ViewGroup] 238 | * @param style an style int value defined in xml 239 | * @param autoAdd whether add [ViewFlipper] into [ViewGroup] automatically 240 | * @param init set attributes for this view in this lambda 241 | */ 242 | inline fun ViewGroup.ViewFlipper( 243 | style: Int? = null, 244 | autoAdd: Boolean = true, 245 | init: ViewFlipper.() -> Unit 246 | ): ViewFlipper { 247 | val viewFlipper = 248 | if (style != null) ViewFlipper( 249 | ContextThemeWrapper(context, style) 250 | ) else ViewFlipper(context) 251 | return viewFlipper.apply(init).also { if (autoAdd) addView(it) } 252 | } 253 | 254 | /** 255 | * create [AppCompatEditText] instance within a [ViewGroup] 256 | * @param style an style int value defined in xml 257 | * @param autoAdd whether add [AppCompatEditText] into [ViewGroup] automatically 258 | * @param init set attributes for this view in this lambda 259 | */ 260 | inline fun ViewGroup.EditText( 261 | style: Int? = null, 262 | autoAdd: Boolean = true, 263 | init: AppCompatEditText.() -> Unit 264 | ): AppCompatEditText { 265 | val editText = 266 | if (style != null) AppCompatEditText( 267 | ContextThemeWrapper(context, style) 268 | ) else AppCompatEditText(context) 269 | return editText.apply(init).also { if (autoAdd) addView(it) } 270 | } 271 | 272 | /** 273 | * create [HorizontalScrollView] instance within a [ViewGroup] 274 | * @param style an style int value defined in xml 275 | * @param autoAdd whether add [HorizontalScrollView] into [ViewGroup] automatically 276 | * @param init set attributes for this view in this lambda 277 | */ 278 | inline fun ViewGroup.HorizontalScrollView( 279 | style: Int? = null, 280 | autoAdd: Boolean = true, 281 | init: HorizontalScrollView.() -> Unit 282 | ): HorizontalScrollView { 283 | val horizontalScrollView = 284 | if (style != null) HorizontalScrollView( 285 | ContextThemeWrapper(context, style) 286 | ) else HorizontalScrollView(context) 287 | return horizontalScrollView.apply(init).also { if (autoAdd) addView(it) } 288 | } 289 | 290 | /** 291 | * create [ViewPager2] instance within a [ViewGroup] 292 | * @param style an style int value defined in xml 293 | * @param autoAdd whether add [ViewPager2] into [ViewGroup] automatically 294 | * @param init set attributes for this view in this lambda 295 | */ 296 | inline fun ViewGroup.ViewPager2( 297 | style: Int? = null, 298 | autoAdd: Boolean = true, 299 | init: ViewPager2.() -> Unit 300 | ): ViewPager2 { 301 | val viewPager2 = 302 | if (style != null) ViewPager2( 303 | ContextThemeWrapper(context, style) 304 | ) else ViewPager2(context) 305 | return viewPager2.apply(init).also { if (autoAdd) addView(it) } 306 | } 307 | /** 308 | * create [FragmentContainerView] instance within a [ViewGroup] 309 | * @param style an style int value defined in xml 310 | * @param autoAdd whether add [FragmentContainerView] into [ViewGroup] automatically 311 | * @param init set attributes for this view in this lambda 312 | */ 313 | inline fun ViewGroup.FragmentContainerView( 314 | style: Int? = null, 315 | autoAdd: Boolean = true, 316 | init: FragmentContainerView.() -> Unit 317 | ): FragmentContainerView { 318 | val fragmentContainerView = 319 | if (style != null) FragmentContainerView( 320 | ContextThemeWrapper(context, style) 321 | ) else FragmentContainerView(context) 322 | return fragmentContainerView.apply(init).also { if (autoAdd) addView(it) } 323 | } 324 | 325 | /** 326 | * create [Guideline] instance within a [ConstraintLayout] 327 | * @param style an style int value defined in xml 328 | * @param autoAdd whether add [Guideline] into [ConstraintLayout] automatically 329 | * @param init set attributes for this view in this lambda 330 | */ 331 | inline fun ConstraintLayout.Guideline( 332 | style: Int? = null, 333 | autoAdd: Boolean = true, 334 | init: Guideline.() -> Unit 335 | ): Guideline { 336 | val guideline = 337 | if (style != null) Guideline( 338 | ContextThemeWrapper(context, style) 339 | ) else Guideline(context) 340 | return guideline.apply(init).also { if (autoAdd) addView(it) } 341 | } 342 | 343 | /** 344 | * create [Barrier] instance within a [ConstraintLayout] 345 | * @param style an style int value defined in xml 346 | * @param autoAdd whether add [Barrier] into [ConstraintLayout] automatically 347 | * @param init set attributes for this view in this lambda 348 | */ 349 | inline fun ConstraintLayout.Barrier( 350 | style: Int? = null, 351 | autoAdd: Boolean = true, 352 | init: Barrier.() -> Unit 353 | ): Barrier { 354 | val barrier = 355 | if (style != null) Barrier( 356 | ContextThemeWrapper(context, style) 357 | ) else Barrier(context) 358 | return barrier.apply(init).also { if (autoAdd) addView(it) } 359 | } 360 | 361 | /** 362 | * create [Flow] instance within a [ConstraintLayout] 363 | * @param style an style int value defined in xml 364 | * @param autoAdd whether add [Flow] into [ConstraintLayout] automatically 365 | * @param init set attributes for this view in this lambda 366 | */ 367 | inline fun ConstraintLayout.Flow( 368 | style: Int? = null, 369 | autoAdd: Boolean = true, 370 | init: Flow.() -> Unit 371 | ): Flow { 372 | val flow = 373 | if (style != null) Flow( 374 | ContextThemeWrapper(context, style) 375 | ) else Flow(context) 376 | return flow.apply(init).also { if (autoAdd) addView(it) } 377 | } 378 | 379 | /** 380 | * create [Layer] instance within a [ConstraintLayout] 381 | * @param style an style int value defined in xml 382 | * @param autoAdd whether add [Layer] into [ConstraintLayout] automatically 383 | * @param init set attributes for this view in this lambda 384 | */ 385 | inline fun ConstraintLayout.Layer( 386 | style: Int? = null, 387 | autoAdd: Boolean = true, 388 | init: Layer.() -> Unit 389 | ): Layer { 390 | val layer = 391 | if (style != null) Layer( 392 | ContextThemeWrapper(context, style) 393 | ) else Layer(context) 394 | return layer.apply(init).also { if (autoAdd) addView(it) } 395 | } 396 | 397 | /** 398 | * create [ConstraintLayout] instance 399 | * @param style an style int value defined in xml 400 | * @param init set attributes for this view in this lambda 401 | */ 402 | inline fun Context.ConstraintLayout( 403 | style: Int? = null, 404 | init: ConstraintLayout.() -> Unit 405 | ): ConstraintLayout { 406 | val constraintLayout = 407 | if (style != null) ConstraintLayout( 408 | ContextThemeWrapper(this, style) 409 | ) else ConstraintLayout(this) 410 | return constraintLayout.apply(init) 411 | } 412 | 413 | /** 414 | * create [LinearLayout] instance 415 | * @param style an style int value defined in xml 416 | * @param init set attributes for this view in this lambda 417 | */ 418 | inline fun Context.LinearLayout( 419 | style: Int? = null, 420 | init: LinearLayout.() -> Unit 421 | ): LinearLayout { 422 | val LinearLayout = 423 | if (style != null) LinearLayout( 424 | ContextThemeWrapper(this, style) 425 | ) else LinearLayout(this) 426 | return LinearLayout.apply(init) 427 | } 428 | 429 | /** 430 | * create [FrameLayout] instance 431 | * @param style an style int value defined in xml 432 | * @param init set attributes for this view in this lambda 433 | */ 434 | inline fun Context.FrameLayout(style: Int? = null, init: FrameLayout.() -> Unit): FrameLayout { 435 | val frameLayout = 436 | if (style != null) FrameLayout( 437 | ContextThemeWrapper(this, style) 438 | ) else FrameLayout(this) 439 | return frameLayout.apply(init) 440 | } 441 | 442 | /** 443 | * create [NestedScrollView] instance 444 | * @param style an style int value defined in xml 445 | * @param init set attributes for this view in this lambda 446 | */ 447 | inline fun Context.NestedScrollView( 448 | style: Int? = null, 449 | init: NestedScrollView.() -> Unit 450 | ): NestedScrollView { 451 | val nestedScrollView = 452 | if (style != null) NestedScrollView( 453 | ContextThemeWrapper(this, style) 454 | ) else NestedScrollView(this) 455 | return nestedScrollView.apply(init) 456 | } 457 | 458 | /** 459 | * create [AppCompatTextView] instance 460 | * @param style an style int value defined in xml 461 | * @param init set attributes for this view in this lambda 462 | */ 463 | inline fun Context.TextView( 464 | style: Int? = null, 465 | init: AppCompatTextView.() -> Unit 466 | ): AppCompatTextView { 467 | val textView = 468 | if (style != null) AppCompatTextView( 469 | ContextThemeWrapper(this, style) 470 | ) else AppCompatTextView(this) 471 | return textView.apply(init) 472 | 473 | } 474 | 475 | /** 476 | * create [AppCompatButton] instance 477 | * @param style an style int value defined in xml 478 | * @param init set attributes for this view in this lambda 479 | */ 480 | inline fun Context.Button(style: Int? = null, init: AppCompatButton.() -> Unit): AppCompatButton { 481 | val button = 482 | if (style != null) AppCompatButton( 483 | ContextThemeWrapper(this, style) 484 | ) else AppCompatButton(this) 485 | return button.apply(init) 486 | } 487 | 488 | /** 489 | * create [AppCompatImageView] instance 490 | * @param style an style int value defined in xml 491 | * @param init set attributes for this view in this lambda 492 | */ 493 | inline fun Context.ImageView( 494 | style: Int? = null, 495 | init: AppCompatImageView.() -> Unit 496 | ): AppCompatImageView { 497 | val imageView = 498 | if (style != null) AppCompatImageView( 499 | ContextThemeWrapper(this, style) 500 | ) else AppCompatImageView(this) 501 | return imageView.apply(init) 502 | } 503 | 504 | /** 505 | * create [View] instance 506 | * @param style an style int value defined in xml 507 | * @param init set attributes for this view in this lambda 508 | */ 509 | inline fun Context.View(style: Int? = null, init: View.() -> Unit): View { 510 | val view = 511 | if (style != null) View( 512 | ContextThemeWrapper(this, style) 513 | ) else View(this) 514 | return view.apply(init) 515 | } 516 | 517 | /** 518 | * create [AppCompatEditText] instance 519 | * @param style an style int value defined in xml 520 | * @param init set attributes for this view in this lambda 521 | */ 522 | inline fun Context.EditText( 523 | style: Int? = null, 524 | init: AppCompatEditText.() -> Unit 525 | ): AppCompatEditText { 526 | val editText = 527 | if (style != null) AppCompatEditText( 528 | ContextThemeWrapper(this, style) 529 | ) else AppCompatEditText(this) 530 | return editText.apply(init) 531 | } 532 | 533 | /** 534 | * create [ViewFlipper] instance 535 | * @param style an style int value defined in xml 536 | * @param init set attributes for this view in this lambda 537 | */ 538 | inline fun Context.ViewFlipper(style: Int? = null, init: ViewFlipper.() -> Unit): ViewFlipper { 539 | val viewFlipper = 540 | if (style != null) ViewFlipper( 541 | ContextThemeWrapper(this, style) 542 | ) else ViewFlipper(this) 543 | return viewFlipper.apply(init) 544 | 545 | } 546 | 547 | /** 548 | * create [HorizontalScrollView] instance 549 | * @param style an style int value defined in xml 550 | * @param init set attributes for this view in this lambda 551 | */ 552 | inline fun Context.HorizontalScrollView( 553 | style: Int? = null, 554 | init: HorizontalScrollView.() -> Unit 555 | ): HorizontalScrollView { 556 | val horizontalScrollView = 557 | if (style != null) HorizontalScrollView( 558 | ContextThemeWrapper(this, style) 559 | ) else HorizontalScrollView(this) 560 | return horizontalScrollView.apply(init) 561 | } 562 | 563 | /** 564 | * create [ViewPager2] instance 565 | * @param style an style int value defined in xml 566 | * @param init set attributes for this view in this lambda 567 | */ 568 | inline fun Context.ViewPager2( 569 | style: Int? = null, 570 | init: ViewPager2.() -> Unit 571 | ): ViewPager2 { 572 | val viewPager2 = 573 | if (style != null) ViewPager2( 574 | ContextThemeWrapper(this, style) 575 | ) else ViewPager2(this) 576 | return viewPager2.apply(init) 577 | } 578 | 579 | /** 580 | * create [ViewPager2] instance 581 | * @param style an style int value defined in xml 582 | * @param init set attributes for this view in this lambda 583 | */ 584 | inline fun Context.RecyclerView( 585 | style: Int? = null, 586 | init: RecyclerView.() -> Unit 587 | ): RecyclerView { 588 | val recyclerView = 589 | if (style != null) RecyclerView( 590 | ContextThemeWrapper(this, style) 591 | ) else RecyclerView(this) 592 | return recyclerView.apply(init) 593 | } 594 | 595 | 596 | /** 597 | * create [FragmentContainerView] instance 598 | * @param style an style int value defined in xml 599 | * @param init set attributes for this view in this lambda 600 | */ 601 | inline fun Context.FragmentContainerView( 602 | style: Int? = null, 603 | init: FragmentContainerView.() -> Unit 604 | ): FragmentContainerView { 605 | val fragmentContainerView = 606 | if (style != null) FragmentContainerView( 607 | ContextThemeWrapper(this, style) 608 | ) else FragmentContainerView(this) 609 | return fragmentContainerView.apply(init) 610 | } 611 | 612 | /** 613 | * create [ConstraintLayout] instance within [Fragment] 614 | * @param style an style int value defined in xml 615 | * @param init set attributes for this view in this lambda 616 | */ 617 | inline fun Fragment.ConstraintLayout( 618 | style: Int? = null, 619 | init: ConstraintLayout.() -> Unit 620 | ): ConstraintLayout? = context?.let { 621 | if (style != null) ConstraintLayout( 622 | ContextThemeWrapper(it, style) 623 | ) else ConstraintLayout(it) 624 | }?.apply(init) 625 | 626 | /** 627 | * create [LinearLayout] instance within [Fragment] 628 | * @param style an style int value defined in xml 629 | * @param init set attributes for this view in this lambda 630 | */ 631 | inline fun Fragment.LinearLayout( 632 | style: Int? = null, 633 | init: LinearLayout.() -> Unit 634 | ): LinearLayout? = context?.let { 635 | if (style != null) LinearLayout( 636 | ContextThemeWrapper(it, style) 637 | ) else LinearLayout(it) 638 | }?.apply(init) 639 | 640 | /** 641 | * create [FrameLayout] instance within [Fragment] 642 | * @param style an style int value defined in xml 643 | * @param init set attributes for this view in this lambda 644 | */ 645 | inline fun Fragment.FrameLayout( 646 | style: Int? = null, 647 | init: FrameLayout.() -> Unit 648 | ): FrameLayout? = context?.let { 649 | if (style != null) FrameLayout( 650 | ContextThemeWrapper(it, style) 651 | ) else FrameLayout(it) 652 | }?.apply(init) 653 | 654 | /** 655 | * create [NestedScrollView] instance within [Fragment] 656 | * @param style an style int value defined in xml 657 | * @param init set attributes for this view in this lambda 658 | */ 659 | inline fun Fragment.NestedScrollView( 660 | style: Int? = null, 661 | init: FrameLayout.() -> Unit 662 | ): NestedScrollView? = context?.let { 663 | if (style != null) NestedScrollView( 664 | ContextThemeWrapper(it, style) 665 | ) else NestedScrollView(it) 666 | }?.apply(init) 667 | 668 | /** 669 | * create [AppCompatTextView] instance within [Fragment] 670 | * @param style an style int value defined in xml 671 | * @param init set attributes for this view in this lambda 672 | */ 673 | inline fun Fragment.TextView( 674 | style: Int? = null, 675 | init: AppCompatTextView.() -> Unit 676 | ): AppCompatTextView? = context?.let { 677 | if (style != null) AppCompatTextView( 678 | ContextThemeWrapper(it, style) 679 | ) else AppCompatTextView(it) 680 | }?.apply(init) 681 | 682 | /** 683 | * create [AppCompatButton] instance within [Fragment] 684 | * @param style an style int value defined in xml 685 | * @param init set attributes for this view in this lambda 686 | */ 687 | inline fun Fragment.Button( 688 | style: Int? = null, 689 | init: AppCompatButton.() -> Unit 690 | ): AppCompatButton? = context?.let { 691 | if (style != null) AppCompatButton( 692 | ContextThemeWrapper(it, style) 693 | ) else AppCompatButton(it) 694 | }?.apply(init) 695 | 696 | /** 697 | * create [AppCompatImageView] instance within [Fragment] 698 | * @param style an style int value defined in xml 699 | * @param init set attributes for this view in this lambda 700 | */ 701 | inline fun Fragment.ImageView( 702 | style: Int? = null, 703 | init: AppCompatImageView.() -> Unit 704 | ): AppCompatImageView? = context?.let { 705 | if (style != null) AppCompatImageView( 706 | ContextThemeWrapper(it, style) 707 | ) else AppCompatImageView(it) 708 | }?.apply(init) 709 | 710 | /** 711 | * create [View] instance within [Fragment] 712 | * @param style an style int value defined in xml 713 | * @param init set attributes for this view in this lambda 714 | */ 715 | inline fun Fragment.View( 716 | style: Int? = null, 717 | init: View.() -> Unit 718 | ): View? = context?.let { 719 | if (style != null) View( 720 | ContextThemeWrapper(it, style) 721 | ) else View(it) 722 | }?.apply(init) 723 | 724 | /** 725 | * create [ViewFlipper] instance within [Fragment] 726 | * @param style an style int value defined in xml 727 | * @param init set attributes for this view in this lambda 728 | */ 729 | inline fun Fragment.ViewFlipper( 730 | style: Int? = null, 731 | init: ViewFlipper.() -> Unit 732 | ): ViewFlipper? = context?.let { 733 | if (style != null) ViewFlipper( 734 | ContextThemeWrapper(it, style) 735 | ) else ViewFlipper(it) 736 | }?.apply(init) 737 | 738 | /** 739 | * create [AppCompatEditText] instance within [Fragment] 740 | * @param style an style int value defined in xml 741 | * @param init set attributes for this view in this lambda 742 | */ 743 | inline fun Fragment.EditText( 744 | style: Int? = null, 745 | init: AppCompatEditText.() -> Unit 746 | ): AppCompatEditText? = context?.let { 747 | if (style != null) AppCompatEditText( 748 | ContextThemeWrapper(it, style) 749 | ) else AppCompatEditText(it) 750 | }?.apply(init) 751 | 752 | /** 753 | * create [HorizontalScrollView] instance within [Fragment] 754 | * @param style an style int value defined in xml 755 | * @param init set attributes for this view in this lambda 756 | */ 757 | inline fun Fragment.HorizontalScrollView( 758 | style: Int? = null, 759 | init: HorizontalScrollView.() -> Unit 760 | ): HorizontalScrollView? = context?.let { 761 | if (style != null) HorizontalScrollView( 762 | ContextThemeWrapper(it, style) 763 | ) else HorizontalScrollView(it) 764 | }?.apply(init) 765 | 766 | /** 767 | * create [ViewPager2] instance within [Fragment] 768 | * @param style an style int value defined in xml 769 | * @param init set attributes for this view in this lambda 770 | */ 771 | inline fun Fragment.ViewPager2( 772 | style: Int? = null, 773 | init: ViewPager2.() -> Unit 774 | ): ViewPager2? = context?.let { 775 | if (style != null) ViewPager2( 776 | ContextThemeWrapper(it, style) 777 | ) else ViewPager2(it) 778 | }?.apply(init) 779 | 780 | /** 781 | * create [ViewPager2] instance within [Fragment] 782 | * @param style an style int value defined in xml 783 | * @param init set attributes for this view in this lambda 784 | */ 785 | inline fun Fragment.RecyclerView( 786 | style: Int? = null, 787 | init: RecyclerView.() -> Unit 788 | ): RecyclerView? = context?.let { 789 | if (style != null) RecyclerView( 790 | ContextThemeWrapper(it, style) 791 | ) else RecyclerView(it) 792 | }?.apply(init) 793 | 794 | 795 | /** 796 | * create [FragmentContainerView] instance within [Fragment] 797 | * @param style an style int value defined in xml 798 | * @param init set attributes for this view in this lambda 799 | */ 800 | inline fun Fragment.FragmentContainerView( 801 | style: Int? = null, 802 | init: FragmentContainerView.() -> Unit 803 | ): FragmentContainerView? = context?.let { 804 | if (style != null) FragmentContainerView( 805 | ContextThemeWrapper(it, style) 806 | ) else FragmentContainerView(it) 807 | }?.apply(init) -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/LayoutConstants.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.graphics.Typeface 4 | import android.graphics.drawable.GradientDrawable 5 | import android.text.InputType 6 | import android.text.TextUtils 7 | import android.view.Gravity 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.ImageView 11 | import android.widget.LinearLayout 12 | import androidx.constraintlayout.helper.widget.Flow 13 | import androidx.constraintlayout.widget.Barrier 14 | import androidx.constraintlayout.widget.ConstraintLayout 15 | import androidx.constraintlayout.widget.ConstraintProperties 16 | 17 | val match_parent = ViewGroup.LayoutParams.MATCH_PARENT 18 | val wrap_content = ViewGroup.LayoutParams.WRAP_CONTENT 19 | 20 | val visible = View.VISIBLE 21 | val gone = View.GONE 22 | val invisible = View.INVISIBLE 23 | 24 | val horizontal = LinearLayout.HORIZONTAL 25 | val vertical = LinearLayout.VERTICAL 26 | 27 | val bold = Typeface.BOLD 28 | val normal = Typeface.NORMAL 29 | val italic = Typeface.ITALIC 30 | val bold_italic = Typeface.BOLD_ITALIC 31 | 32 | val gravity_center = Gravity.CENTER 33 | val gravity_left = Gravity.LEFT 34 | val gravity_right = Gravity.RIGHT 35 | val gravity_bottom = Gravity.BOTTOM 36 | val gravity_top = Gravity.TOP 37 | val gravity_center_horizontal = Gravity.CENTER_HORIZONTAL 38 | val gravity_center_vertical = Gravity.CENTER_VERTICAL 39 | 40 | val scale_fit_xy = ImageView.ScaleType.FIT_XY 41 | val scale_center_crop = ImageView.ScaleType.CENTER_CROP 42 | val scale_center = ImageView.ScaleType.CENTER 43 | val scale_center_inside = ImageView.ScaleType.CENTER_INSIDE 44 | val scale_fit_center = ImageView.ScaleType.FIT_CENTER 45 | val scale_fit_end = ImageView.ScaleType.FIT_END 46 | val scale_matrix = ImageView.ScaleType.MATRIX 47 | val scale_fit_start = ImageView.ScaleType.FIT_START 48 | 49 | val constraint_start = ConstraintProperties.START 50 | val constraint_end = ConstraintProperties.END 51 | val constraint_top = ConstraintProperties.TOP 52 | val constraint_bottom = ConstraintProperties.BOTTOM 53 | val constraint_baseline = ConstraintProperties.BASELINE 54 | val constraint_parent = ConstraintProperties.PARENT_ID 55 | 56 | val spread = ConstraintLayout.LayoutParams.CHAIN_SPREAD 57 | val packed = ConstraintLayout.LayoutParams.CHAIN_PACKED 58 | val spread_inside = ConstraintLayout.LayoutParams.CHAIN_SPREAD_INSIDE 59 | 60 | val barrier_left = Barrier.LEFT 61 | val barrier_top = Barrier.TOP 62 | val barrier_right = Barrier.RIGHT 63 | val barrier_bottom = Barrier.BOTTOM 64 | val barrier_start = Barrier.START 65 | val barrier_end = Barrier.END 66 | 67 | val wrap_none = Flow.WRAP_NONE 68 | val wrap_chain = Flow.WRAP_CHAIN 69 | val wrap_aligned = Flow.WRAP_ALIGNED 70 | 71 | val gradient_top_bottom = GradientDrawable.Orientation.TOP_BOTTOM 72 | val gradient_tr_bl = GradientDrawable.Orientation.TR_BL 73 | val gradient_right_left = GradientDrawable.Orientation.RIGHT_LEFT 74 | val gradient_br_tl = GradientDrawable.Orientation.BR_TL 75 | val gradient_bottom_top = GradientDrawable.Orientation.BOTTOM_TOP 76 | val gradient_bl_tr = GradientDrawable.Orientation.BL_TR 77 | val gradient_left_right = GradientDrawable.Orientation.LEFT_RIGHT 78 | val gradient_tl_br = GradientDrawable.Orientation.TL_BR 79 | 80 | val shape_rectangle = GradientDrawable.RECTANGLE 81 | val shape_oval = GradientDrawable.OVAL 82 | val shape_line = GradientDrawable.LINE 83 | val shape_ring = GradientDrawable.RING 84 | 85 | val gradient_type_linear = GradientDrawable.LINEAR_GRADIENT 86 | val gradient_type_radial = GradientDrawable.RADIAL_GRADIENT 87 | val gradient_type_sweep = GradientDrawable.SWEEP_GRADIENT 88 | 89 | val state_enable = android.R.attr.state_enabled 90 | val state_disable = -android.R.attr.state_enabled 91 | val state_pressed = android.R.attr.state_pressed 92 | val state_unpressed = -android.R.attr.state_pressed 93 | val state_focused = android.R.attr.state_focused 94 | val state_unfocused = -android.R.attr.state_focused 95 | val state_selected = android.R.attr.state_selected 96 | val state_unselected = -android.R.attr.state_selected 97 | 98 | val input_type_number = InputType.TYPE_CLASS_NUMBER 99 | 100 | val wrap_mode_chain = Flow.WRAP_CHAIN 101 | val wrap_mode_none = Flow.WRAP_NONE 102 | val wrap_mode_aligned = Flow.WRAP_ALIGNED 103 | 104 | val ellipsize_end = TextUtils.TruncateAt.END 105 | val ellipsize_marquee = TextUtils.TruncateAt.MARQUEE 106 | val ellipsize_middle = TextUtils.TruncateAt.MIDDLE 107 | val ellipsize_start = TextUtils.TruncateAt.START 108 | 109 | val parent_id = "0" 110 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/LayoutExtra.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.appcompat.view.ContextThemeWrapper 7 | import androidx.fragment.app.Fragment 8 | import taylor.com.views.Selector 9 | import taylor.com.views.LineFeedLayout 10 | import taylor.com.views.PercentLayout 11 | import taylor.com.views.ProgressBar 12 | 13 | 14 | /** 15 | * layout dsl for customized view 16 | */ 17 | inline fun ViewGroup.LineFeedLayout(autoAdd: Boolean = true, init: LineFeedLayout.() -> Unit) = 18 | LineFeedLayout(context).apply(init).also { if (autoAdd) addView(it) } 19 | 20 | inline fun Context.LineFeedLayout(init: LineFeedLayout.() -> Unit): LineFeedLayout = 21 | LineFeedLayout(this).apply(init) 22 | 23 | inline fun Fragment.LineFeedLayout(init: LineFeedLayout.() -> Unit) = 24 | context?.let { LineFeedLayout(it).apply(init) } 25 | 26 | inline fun ViewGroup.Selector(autoAdd: Boolean = true, init: Selector.() -> Unit) = 27 | Selector(context).apply(init).also { addView(it) } 28 | 29 | inline fun Context.Selector(init: Selector.() -> Unit): Selector = 30 | Selector(this).apply(init) 31 | 32 | inline fun Fragment.Selector(init: Selector.() -> Unit) = 33 | context?.let { Selector(it).apply(init) } 34 | 35 | inline fun ViewGroup.ProgressBar(autoAdd: Boolean = true, init: ProgressBar.() -> Unit) = 36 | ProgressBar(context).apply(init).also { if (autoAdd) addView(it) } 37 | 38 | inline fun Context.ProgressBar(init: ProgressBar.() -> Unit): ProgressBar = 39 | ProgressBar(this).apply(init) 40 | 41 | inline fun Fragment.ProgressBar(init: ProgressBar.() -> Unit) = 42 | context?.let { ProgressBar(it).apply(init) } 43 | 44 | /** 45 | * create [PercentLayout] instance within a [ViewGroup] 46 | * @param style an style int value defined in xml 47 | * @param autoAdd whether add [PercentLayout] into [ViewGroup] automatically 48 | * @param init set attributes for this view in this lambda 49 | */ 50 | inline fun ViewGroup.PercentLayout( 51 | style: Int? = null, 52 | autoAdd: Boolean = true, 53 | init: PercentLayout.() -> Unit 54 | ): PercentLayout { 55 | val percentLayout = 56 | if (style != null) PercentLayout( 57 | ContextThemeWrapper(context, style) 58 | ) else PercentLayout(context) 59 | return percentLayout.apply(init).also { if (autoAdd) addView(it) } 60 | } 61 | 62 | /** 63 | * create [PercentLayout] instance 64 | * @param style an style int value defined in xml 65 | * @param init set attributes for this view in this lambda 66 | */ 67 | inline fun Context.PercentLayout(style: Int? = null, init: PercentLayout.() -> Unit): PercentLayout { 68 | val percentLayout = 69 | if (style != null) PercentLayout( 70 | ContextThemeWrapper(this, style) 71 | ) else PercentLayout(this) 72 | return percentLayout.apply(init) 73 | } 74 | 75 | /** 76 | * create [PercentLayout] instance within [Fragment] 77 | * @param style an style int value defined in xml 78 | * @param init set attributes for this view in this lambda 79 | */ 80 | inline fun Fragment.PercentLayout( 81 | style: Int? = null, 82 | init: PercentLayout.() -> Unit 83 | ): PercentLayout? = context?.let { 84 | if (style != null) PercentLayout( 85 | ContextThemeWrapper(it, style) 86 | ) else PercentLayout(it) 87 | }?.apply(init) 88 | 89 | inline var LineFeedLayout.horizontal_gap: Int 90 | get() { 91 | return -1 92 | } 93 | set(value) { 94 | horizontalGap = value.dp 95 | } 96 | 97 | inline var LineFeedLayout.vertical_gap: Int 98 | get() { 99 | return -1 100 | } 101 | set(value) { 102 | verticalGap = value.dp 103 | } 104 | 105 | 106 | inline var View.left_percent: Float 107 | get() { 108 | return -1f 109 | } 110 | set(value) { 111 | updateLayoutParams { 112 | leftPercent = value 113 | } 114 | } 115 | 116 | inline var View.top_percent: Float 117 | get() { 118 | return -1f 119 | } 120 | set(value) { 121 | updateLayoutParams { 122 | topPercent = value 123 | } 124 | } 125 | 126 | inline var View.start_to_start_of_percent: String 127 | get() { 128 | return "" 129 | } 130 | set(value) { 131 | updateLayoutParams { 132 | startToStartOf = value.toLayoutId() 133 | } 134 | } 135 | 136 | inline var View.start_to_end_of_percent: String 137 | get() { 138 | return "" 139 | } 140 | set(value) { 141 | updateLayoutParams { 142 | startToEndOf = value.toLayoutId() 143 | } 144 | } 145 | 146 | inline var View.end_to_end_of_percent: String 147 | get() { 148 | return "" 149 | } 150 | set(value) { 151 | updateLayoutParams { 152 | endToEndOf = value.toLayoutId() 153 | } 154 | } 155 | 156 | inline var View.end_to_start_of_percent: String 157 | get() { 158 | return "" 159 | } 160 | set(value) { 161 | updateLayoutParams { 162 | endToStartOf = value.toLayoutId() 163 | } 164 | } 165 | 166 | inline var View.top_to_top_of_percent: String 167 | get() { 168 | return "" 169 | } 170 | set(value) { 171 | updateLayoutParams { 172 | topToTopOf = value.toLayoutId() 173 | } 174 | } 175 | 176 | inline var View.top_to_bottom_of_percent: String 177 | get() { 178 | return "" 179 | } 180 | set(value) { 181 | updateLayoutParams { 182 | topToBottomOf = value.toLayoutId() 183 | } 184 | } 185 | 186 | inline var View.bottom_to_bottom_of_percent: String 187 | get() { 188 | return "" 189 | } 190 | set(value) { 191 | updateLayoutParams { 192 | bottomToBottomOf = value.toLayoutId() 193 | } 194 | } 195 | 196 | inline var View.bottom_to_top_of_percent: String 197 | get() { 198 | return "" 199 | } 200 | set(value) { 201 | updateLayoutParams { 202 | bottomToTopOf = value.toLayoutId() 203 | } 204 | } 205 | 206 | inline var View.center_vertical_of_percent: String 207 | get() { 208 | return "" 209 | } 210 | set(value) { 211 | updateLayoutParams { 212 | centerVerticalOf = value.toLayoutId() 213 | } 214 | } 215 | 216 | inline var View.center_horizontal_of_percent: String 217 | get() { 218 | return "" 219 | } 220 | set(value) { 221 | updateLayoutParams { 222 | centerHorizontalOf = value.toLayoutId() 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/LayoutHelperFun.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.res.Resources 5 | import android.graphics.Color 6 | import android.graphics.Rect 7 | import android.graphics.drawable.ColorDrawable 8 | import android.os.Build 9 | import android.text.Editable 10 | import android.util.TypedValue 11 | import android.view.* 12 | import android.widget.EditText 13 | import android.widget.TextView 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.constraintlayout.helper.widget.Flow 16 | import androidx.constraintlayout.widget.ConstraintLayout 17 | import androidx.constraintlayout.widget.ConstraintProperties 18 | import androidx.constraintlayout.widget.ConstraintSet 19 | import androidx.coordinatorlayout.widget.ViewGroupUtils 20 | import androidx.fragment.app.DialogFragment 21 | import androidx.lifecycle.LifecycleOwner 22 | import androidx.lifecycle.LiveData 23 | import androidx.recyclerview.widget.RecyclerView 24 | import kotlinx.coroutines.* 25 | import kotlinx.coroutines.channels.Channel 26 | import kotlinx.coroutines.channels.SendChannel 27 | import kotlinx.coroutines.channels.actor 28 | import kotlinx.coroutines.flow.collect 29 | import kotlinx.coroutines.flow.consumeAsFlow 30 | import kotlinx.coroutines.flow.debounce 31 | import kotlin.math.abs 32 | 33 | val Int.dp: Int 34 | get() = TypedValue.applyDimension( 35 | TypedValue.COMPLEX_UNIT_DIP, 36 | this.toFloat(), 37 | Resources.getSystem().displayMetrics 38 | ).toInt() 39 | 40 | 41 | val Float.dp: Float 42 | get() = TypedValue.applyDimension( 43 | TypedValue.COMPLEX_UNIT_DIP, 44 | this, 45 | Resources.getSystem().displayMetrics 46 | ) 47 | 48 | val Float.sp: Float 49 | get() = TypedValue.applyDimension( 50 | TypedValue.COMPLEX_UNIT_SP, 51 | this, 52 | Resources.getSystem().displayMetrics 53 | ) 54 | 55 | val Number.dp: Int 56 | get() = TypedValue.applyDimension( 57 | TypedValue.COMPLEX_UNIT_DIP, 58 | this.toFloat(), 59 | Resources.getSystem().displayMetrics 60 | ).toInt() 61 | 62 | fun ViewGroup.MarginLayoutParams.toConstraintLayoutParam() = 63 | ConstraintLayout.LayoutParams(width, height).also { it -> 64 | it.topMargin = this.topMargin 65 | it.bottomMargin = this.bottomMargin 66 | it.marginStart = this.marginStart 67 | it.marginEnd = this.marginEnd 68 | } 69 | 70 | /** 71 | * update LayoutParams according to it's type 72 | */ 73 | inline fun View.updateLayoutParams(block: T.() -> Unit) { 74 | layoutParams = (layoutParams as? T)?.apply(block) ?: kotlin.run { 75 | val width = layoutParams?.width ?: 0 76 | val height = layoutParams?.height ?: 0 77 | val lp = ViewGroup.LayoutParams(width,height) 78 | new(lp).apply(block) 79 | } 80 | } 81 | 82 | /** 83 | * create a new instance of [T] with [params] 84 | */ 85 | inline fun new(vararg params: Any): T = 86 | T::class.java.getDeclaredConstructor(*params.map { it::class.java }.toTypedArray()).also { it.isAccessible = true }.newInstance(*params) 87 | 88 | fun String.toLayoutId(): Int { 89 | var id = hashCode() 90 | if (this == parent_id) id = 0 91 | return abs(id) 92 | } 93 | 94 | fun DialogFragment.fullScreenMode() { 95 | dialog?.window?.apply { 96 | attributes?.apply { 97 | width = WindowManager.LayoutParams.MATCH_PARENT 98 | height = WindowManager.LayoutParams.MATCH_PARENT 99 | } 100 | decorView.setPadding(0, 0, 0, 0) 101 | setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 102 | } 103 | } 104 | 105 | fun View.find(id: String): T? = findViewById(id.toLayoutId()) 106 | 107 | fun AppCompatActivity.find(id: String): T? = findViewById(id.toLayoutId()) 108 | 109 | @SuppressLint("RestrictedApi") 110 | fun View.expand(dx: Int, dy: Int) { 111 | class MultiTouchDelegate(bound: Rect? = null, delegateView: View) : TouchDelegate(bound, delegateView) { 112 | val delegateViewMap = mutableMapOf() 113 | private var delegateView: View? = null 114 | 115 | override fun onTouchEvent(event: MotionEvent): Boolean { 116 | val x = event.x.toInt() 117 | val y = event.y.toInt() 118 | var handled = false 119 | when (event.actionMasked) { 120 | MotionEvent.ACTION_DOWN -> { 121 | delegateView = findDelegateViewUnder(x, y) 122 | } 123 | MotionEvent.ACTION_CANCEL -> { 124 | delegateView = null 125 | } 126 | } 127 | delegateView?.let { 128 | event.setLocation(it.width / 2f, it.height / 2f) 129 | handled = it.dispatchTouchEvent(event) 130 | } 131 | return handled 132 | } 133 | 134 | private fun findDelegateViewUnder(x: Int, y: Int): View? { 135 | delegateViewMap.forEach { entry -> if (entry.value.contains(x, y)) return entry.key } 136 | return null 137 | } 138 | } 139 | 140 | val parentView = parent as? ViewGroup 141 | parentView ?: return 142 | 143 | if (parentView.touchDelegate == null) parentView.touchDelegate = MultiTouchDelegate(delegateView = this) 144 | post { 145 | val rect = Rect() 146 | ViewGroupUtils.getDescendantRect(parentView, this, rect) 147 | rect.inset(- dx, - dy) 148 | (parentView.touchDelegate as? MultiTouchDelegate)?.delegateViewMap?.put(this, rect) 149 | } 150 | } 151 | 152 | /** 153 | * build a horizontal or vertical chain in [ConstraintLayout] 154 | * [startView] is the starting point of the chain 155 | * [endView] is the endding point of the chain 156 | * [views] is the body of the chain 157 | * [orientation] could be [horizontal] or [vertical] 158 | */ 159 | fun ConstraintLayout.buildChain( 160 | startView: View, 161 | views: List, 162 | endView: View?, 163 | orientation: Int, 164 | outMarginStart: Int, 165 | outMarinEnd: Int, 166 | innerMargin: Int 167 | ) { 168 | if (views.isNullOrEmpty()) return 169 | var preView = startView 170 | var startSide = if (orientation == horizontal) constraint_start else constraint_top 171 | var endSide = if (orientation == horizontal) constraint_end else constraint_bottom 172 | 173 | val firstView = views.first() 174 | val isStartViewParent = firstView.isChildOf(startView) 175 | val isEndViewParent = firstView.isChildOf(endView) 176 | 177 | // deal with the first view 178 | ConstraintProperties(firstView) 179 | .connect( 180 | startSide, 181 | if (isStartViewParent) ConstraintProperties.PARENT_ID else preView.id, 182 | if (isStartViewParent) startSide else endSide, 183 | outMarginStart 184 | ) 185 | .apply() 186 | 187 | preView = firstView 188 | 189 | (1 until views.size).map { views[it] }.forEach { currentView -> 190 | ConstraintProperties(currentView) 191 | .connect(startSide, preView.id, endSide, innerMargin) 192 | .apply() 193 | ConstraintProperties(preView) 194 | .connect(endSide, currentView.id, startSide, innerMargin) 195 | .apply() 196 | preView = currentView 197 | } 198 | 199 | // deal with the last view 200 | ConstraintProperties(preView) 201 | .connect( 202 | endSide, 203 | if (isEndViewParent) ConstraintProperties.PARENT_ID else endView?.id 204 | ?: ConstraintSet.UNSET, 205 | if (isEndViewParent) endSide else startSide, 206 | outMarinEnd 207 | ) 208 | .apply() 209 | } 210 | 211 | 212 | fun ViewGroup.setOnItemClickListener(listener: (View, Int) -> Unit) { 213 | val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { 214 | private val touchFrame = Rect() 215 | fun pointToPosition(x: Int, y: Int): Int { 216 | (0 until childCount).map { getChildAt(it) }.forEachIndexed { index, child -> 217 | if (child.visibility == visible && child !is Flow) { 218 | child.getHitRect(touchFrame) 219 | if (touchFrame.contains(x, y)) return index 220 | } 221 | } 222 | return -1 223 | } 224 | 225 | override fun onShowPress(e: MotionEvent?) { 226 | } 227 | 228 | override fun onSingleTapUp(e: MotionEvent?): Boolean { 229 | e ?: return false 230 | pointToPosition(e.x.toInt(), e.y.toInt()).takeIf { it != -1 }?.let { index -> 231 | listener(getChildAt(index), index) 232 | } 233 | return false 234 | } 235 | 236 | override fun onDown(e: MotionEvent?): Boolean { 237 | return true 238 | } 239 | 240 | override fun onFling( 241 | e1: MotionEvent?, 242 | e2: MotionEvent?, 243 | velocityX: Float, 244 | velocityY: Float 245 | ): Boolean { 246 | return false 247 | } 248 | 249 | override fun onScroll( 250 | e1: MotionEvent?, 251 | e2: MotionEvent?, 252 | distanceX: Float, 253 | distanceY: Float 254 | ): Boolean { 255 | return false 256 | } 257 | 258 | override fun onLongPress(e: MotionEvent?) { 259 | } 260 | }) 261 | 262 | setOnTouchListener { _, event -> 263 | gestureDetector.onTouchEvent(event) 264 | true 265 | } 266 | } 267 | 268 | fun View.isChildOf(view: View?) = view?.findViewById(this.id) != null 269 | 270 | fun View.observe(liveData: LiveData?, action: (T) -> Unit) { 271 | (context as? LifecycleOwner)?.let { owner -> 272 | liveData?.observe(owner, { action(it) }) 273 | } 274 | } 275 | 276 | fun RecyclerView.setOnItemLongClickListener(listener: (View, Int, Float, Float) -> Unit) { 277 | addOnItemTouchListener(object : RecyclerView.OnItemTouchListener { 278 | val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { 279 | override fun onShowPress(e: MotionEvent?) { 280 | } 281 | 282 | override fun onSingleTapUp(e: MotionEvent?): Boolean { 283 | return false 284 | } 285 | 286 | override fun onDown(e: MotionEvent?): Boolean { 287 | return false 288 | } 289 | 290 | override fun onFling( 291 | e1: MotionEvent?, 292 | e2: MotionEvent?, 293 | velocityX: Float, 294 | velocityY: Float 295 | ): Boolean { 296 | return false 297 | } 298 | 299 | override fun onScroll( 300 | e1: MotionEvent?, 301 | e2: MotionEvent?, 302 | distanceX: Float, 303 | distanceY: Float 304 | ): Boolean { 305 | return false 306 | } 307 | 308 | override fun onLongPress(e: MotionEvent?) { 309 | e?.let { 310 | findChildViewUnder(it.x, it.y)?.let { child -> 311 | val realX = if (child.left >= 0 ) it.x - child.left else it.x 312 | val realY = if (child.top >= 0) it.y - child.top else it.y 313 | listener( 314 | child, 315 | getChildAdapterPosition(child), 316 | realX, 317 | realY 318 | ) 319 | } 320 | } 321 | } 322 | }) 323 | 324 | override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { 325 | 326 | } 327 | 328 | override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { 329 | gestureDetector.onTouchEvent(e) 330 | return false 331 | } 332 | 333 | override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { 334 | } 335 | }) 336 | } 337 | 338 | fun RecyclerView.setOnItemClickListener(listener: (View, Int, Float, Float) -> Boolean) { 339 | addOnItemTouchListener(object : RecyclerView.OnItemTouchListener { 340 | val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { 341 | override fun onShowPress(e: MotionEvent?) { 342 | } 343 | 344 | override fun onSingleTapUp(e: MotionEvent?): Boolean { 345 | e?.let { 346 | findChildViewUnder(it.x, it.y)?.let { child -> 347 | val realX = if (child.left >= 0 ) it.x - child.left else it.x 348 | val realY = if (child.top >= 0) it.y - child.top else it.y 349 | return listener( 350 | child, 351 | getChildAdapterPosition(child), 352 | realX, 353 | realY 354 | ) 355 | } 356 | } 357 | return false 358 | } 359 | 360 | override fun onDown(e: MotionEvent?): Boolean { 361 | return false 362 | } 363 | 364 | override fun onFling( 365 | e1: MotionEvent?, 366 | e2: MotionEvent?, 367 | velocityX: Float, 368 | velocityY: Float 369 | ): Boolean { 370 | return false 371 | } 372 | 373 | override fun onScroll( 374 | e1: MotionEvent?, 375 | e2: MotionEvent?, 376 | distanceX: Float, 377 | distanceY: Float 378 | ): Boolean { 379 | return false 380 | } 381 | 382 | override fun onLongPress(e: MotionEvent?) { 383 | } 384 | }) 385 | 386 | override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { 387 | 388 | } 389 | 390 | override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { 391 | gestureDetector.onTouchEvent(e) 392 | return false 393 | } 394 | 395 | override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { 396 | } 397 | }) 398 | } 399 | 400 | /** 401 | * get relative position of this [View] relative to [otherView] 402 | */ 403 | fun View.getRelativeRectTo(otherView: View): Rect { 404 | val parentRect = Rect().also { otherView.getGlobalVisibleRect(it) } 405 | val childRect = Rect().also { getGlobalVisibleRect(it) } 406 | return childRect.relativeTo(parentRect) 407 | } 408 | 409 | /** 410 | * listen click action for the child view of [RecyclerView]'s item 411 | */ 412 | inline fun View.onChildViewClick( 413 | vararg layoutId: String, // the id of the child view of RecyclerView's item 414 | x: Float, // the x coordinate of click point 415 | y: Float,// the y coordinate of click point, 416 | clickAction: ((View?) -> Unit) 417 | ) { 418 | var clickedView: View? = null 419 | layoutId 420 | .map { id -> 421 | find(id)?.takeIf { it.visibility == visible }?.let { view -> 422 | view.getRelativeRectTo(this).also { rect -> 423 | if (rect.contains(x.toInt(), y.toInt())) { 424 | clickedView = view 425 | } 426 | } 427 | } ?: Rect() 428 | } 429 | .fold(Rect()) { init, rect -> init.apply { union(rect) } } 430 | .takeIf { it.contains(x.toInt(), y.toInt()) } 431 | ?.let { clickAction.invoke(clickedView) } 432 | } 433 | 434 | /** 435 | * listen click action for the child view of [RecyclerView]'s item 436 | */ 437 | inline fun View.onChildViewClick( 438 | vararg layoutId: Int, // the id of the child view of RecyclerView's item 439 | x: Float, // the x coordinate of click point 440 | y: Float,// the y coordinate of click point, 441 | clickAction: ((View?) -> Unit) 442 | ) { 443 | var clickedView: View? = null 444 | layoutId 445 | .map { id -> 446 | findViewById(id)?.takeIf { it.visibility == visible }?.let { view -> 447 | view.getRelativeRectTo(this).also { rect -> 448 | if (rect.contains(x.toInt(), y.toInt())) { 449 | clickedView = view 450 | } 451 | } 452 | } ?: Rect() 453 | } 454 | .fold(Rect()) { init, rect -> init.apply { union(rect) } } 455 | .takeIf { it.contains(x.toInt(), y.toInt()) } 456 | ?.let { clickAction.invoke(clickedView) } 457 | } 458 | 459 | /** 460 | * a new View.OnClickListener which prevents click shaking 461 | */ 462 | fun View.setShakelessClickListener(threshold: Long, onClick: (View) -> Unit) { 463 | class Click( 464 | var view: View? = null, 465 | var clickTime: Long = -1, 466 | var onClick: ((View?) -> Unit)? = null 467 | ) { 468 | fun isShake(click: Click) = abs(clickTime - click.clickTime) < threshold 469 | } 470 | 471 | val mainScope = MainScope() 472 | val clickActor = mainScope.actor(capacity = Channel.UNLIMITED) { 473 | var preClick: Click = Click() 474 | for (click in channel) { 475 | if (!click.isShake(preClick)) { 476 | click.onClick?.invoke(click.view) 477 | } 478 | preClick = click 479 | } 480 | }.autoDispose(this) 481 | setOnClickListener { view -> 482 | mainScope.launch { 483 | clickActor.send( 484 | Click(view, System.currentTimeMillis()) { onClick(view) } 485 | ) 486 | }.autoDispose(this) 487 | } 488 | } 489 | 490 | /** 491 | * a debounce listener for [EditText]'s text change event 492 | */ 493 | fun EditText.setDebounceListener(timeoutMillis: Long, action: (CharSequence) -> Unit) { 494 | val mainScope = MainScope() 495 | val textChangeActor = mainScope.actor(capacity = Channel.UNLIMITED) { 496 | consumeAsFlow().debounce(timeoutMillis).collect { action.invoke(text) } 497 | }.autoDispose(this) 498 | 499 | onTextChange = textWatcher { 500 | onTextChanged = change@{ text: CharSequence?, _: Int, _: Int, _: Int -> 501 | mainScope.launch { 502 | text.toString().trim().let { textChangeActor.send(it) } 503 | }.autoDispose(this@setDebounceListener) 504 | } 505 | } 506 | } 507 | 508 | 509 | /** 510 | * avoid memory leak for View and activity when activity has finished while coroutine is still running 511 | */ 512 | fun Job.autoDispose(view: View?): Job { 513 | view ?: return this 514 | 515 | val listener = object : View.OnAttachStateChangeListener { 516 | override fun onViewDetachedFromWindow(v: View?) { 517 | cancel() 518 | v?.removeOnAttachStateChangeListener(this) 519 | } 520 | 521 | override fun onViewAttachedToWindow(v: View?) = Unit 522 | } 523 | 524 | view.addOnAttachStateChangeListener(listener) 525 | invokeOnCompletion { 526 | view.removeOnAttachStateChangeListener(listener) 527 | } 528 | return this 529 | } 530 | 531 | /** 532 | * avoid memory leak 533 | */ 534 | fun SendChannel.autoDispose(view: View?): SendChannel { 535 | view ?: return this 536 | 537 | val isAttached = 538 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null 539 | val listener = object : View.OnAttachStateChangeListener { 540 | override fun onViewDetachedFromWindow(v: View?) { 541 | close() 542 | v?.removeOnAttachStateChangeListener(this) 543 | } 544 | 545 | override fun onViewAttachedToWindow(v: View?) = Unit 546 | } 547 | 548 | view.addOnAttachStateChangeListener(listener) 549 | invokeOnClose { 550 | view.removeOnAttachStateChangeListener(listener) 551 | } 552 | return this 553 | } 554 | 555 | val View.viewScope: CoroutineScope 556 | get() { 557 | val key = "ViewScope".hashCode() 558 | var scope = getTag(key) as? CoroutineScope 559 | if (scope == null) { 560 | scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 561 | setTag(key, scope) 562 | val listener = object : View.OnAttachStateChangeListener { 563 | override fun onViewAttachedToWindow(v: View?) { 564 | } 565 | 566 | override fun onViewDetachedFromWindow(v: View?) { 567 | scope.cancel() 568 | } 569 | 570 | } 571 | addOnAttachStateChangeListener(listener) 572 | } 573 | return scope 574 | } 575 | 576 | 577 | /** 578 | * get the relative rect of the [Rect] according to the [otherRect] ,considering the [otherRect]'s left and top is zero 579 | */ 580 | fun Rect.relativeTo(otherRect: Rect): Rect { 581 | val relativeLeft = left - otherRect.left 582 | val relativeTop = top - otherRect.top 583 | val relativeRight = relativeLeft + right - left 584 | val relativeBottom = relativeTop + bottom - top 585 | return Rect(relativeLeft, relativeTop, relativeRight, relativeBottom) 586 | } 587 | // 588 | 589 | 590 | // 591 | class TextWatcher( 592 | var beforeTextChanged: ( 593 | text: CharSequence?, 594 | start: Int, 595 | count: Int, 596 | after: Int 597 | ) -> Unit = { _, _, _, _ -> }, 598 | var onTextChanged: ( 599 | text: CharSequence?, 600 | start: Int, 601 | count: Int, 602 | after: Int 603 | ) -> Unit = { _, _, _, _ -> }, 604 | var afterTextChanged: (text: Editable?) -> Unit = {} 605 | ) 606 | 607 | fun textWatcher(init: TextWatcher.() -> Unit): TextWatcher = TextWatcher().apply(init) 608 | 609 | class EditorActionListener( 610 | var onEditorAction: ( 611 | textView: TextView?, 612 | actionId: Int, 613 | keyEvent: KeyEvent? 614 | ) -> Boolean = { _, _, _ -> false } 615 | ) 616 | 617 | fun editorAction(init: EditorActionListener.() -> Unit): EditorActionListener = 618 | EditorActionListener().apply(init) 619 | 620 | /** 621 | * helper class for data binding 622 | */ 623 | class LiveDataBinder(var liveData: LiveData<*>? = null, var action: ((Any?) -> Unit)? = null) 624 | 625 | fun liveDataBinder(liveData: LiveData<*>?, init: LiveDataBinder.() -> Unit): LiveDataBinder = 626 | LiveDataBinder(liveData).apply(init) 627 | 628 | class Binder(var data: Any?, var action: ((View, Any?) -> Unit)? = null) 629 | // 630 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/SelectorBuilder.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.graphics.drawable.StateListDrawable 5 | 6 | /** 7 | * helper function for building [StateListDrawable] 8 | */ 9 | inline fun selector(init: StateListDrawable.() -> Unit) = StateListDrawable().apply(init) 10 | 11 | inline var StateListDrawable.items: Map 12 | get() { 13 | return emptyMap() 14 | } 15 | set(value) { 16 | value.forEach { entry -> addState(entry.key, entry.value) } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/dsl/ShapeBuilder.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.dsl 2 | 3 | import android.content.res.ColorStateList 4 | import android.graphics.Color 5 | import android.graphics.Rect 6 | import android.graphics.drawable.GradientDrawable 7 | import android.os.Build 8 | import androidx.annotation.RequiresApi 9 | 10 | /** 11 | * helper attribute for building [GradientDrawable] 12 | */ 13 | inline var GradientDrawable.solid_color: String 14 | get() { 15 | return "" 16 | } 17 | set(value) { 18 | setColor(Color.parseColor(value)) 19 | } 20 | 21 | /** 22 | * color res should be returned by ContextCompat.getColor() 23 | */ 24 | inline var GradientDrawable.solid_color_res: Int 25 | get() { 26 | return -1 27 | } 28 | set(value) { 29 | setColor(value) 30 | } 31 | 32 | inline var GradientDrawable.corner_radius: Int 33 | get() { 34 | return -1 35 | } 36 | set(value) { 37 | cornerRadius = value.dp.toFloat() 38 | } 39 | 40 | inline var GradientDrawable.corner_radii: IntArray 41 | get() { 42 | return intArrayOf() 43 | } 44 | set(value) { 45 | cornerRadii = value.map { it.dp.toFloat() }.toFloatArray() 46 | } 47 | 48 | inline var GradientDrawable.gradient_colors: List 49 | get() { 50 | return emptyList() 51 | } 52 | set(value) { 53 | colors = value.map { Color.parseColor(it) }.toIntArray() 54 | } 55 | 56 | /** 57 | * color res should be returned by ContextCompat.getColor() 58 | */ 59 | inline var GradientDrawable.gradient_colors_res: List 60 | get() { 61 | return emptyList() 62 | } 63 | set(value) { 64 | colors = value.toIntArray() 65 | } 66 | 67 | inline var GradientDrawable.padding_start: Int 68 | get() { 69 | return -1 70 | } 71 | @RequiresApi(Build.VERSION_CODES.Q) 72 | set(value) { 73 | val paddingRect = Rect().also { getPadding(it) } 74 | setPadding(value.dp, paddingRect.top, paddingRect.right, paddingRect.bottom) 75 | } 76 | 77 | inline var GradientDrawable.padding_end: Int 78 | get() { 79 | return -1 80 | } 81 | @RequiresApi(Build.VERSION_CODES.Q) 82 | set(value) { 83 | val paddingRect = Rect().also { getPadding(it) } 84 | setPadding(paddingRect.left, paddingRect.top, value.dp, paddingRect.bottom) 85 | } 86 | 87 | inline var GradientDrawable.padding_top: Int 88 | get() { 89 | return -1 90 | } 91 | @RequiresApi(Build.VERSION_CODES.Q) 92 | set(value) { 93 | val paddingRect = Rect().also { getPadding(it) } 94 | setPadding(paddingRect.left, value.dp, paddingRect.right, paddingRect.bottom) 95 | } 96 | 97 | inline var GradientDrawable.padding_bottom: Int 98 | get() { 99 | return -1 100 | } 101 | @RequiresApi(Build.VERSION_CODES.Q) 102 | set(value) { 103 | val paddingRect = Rect().also { getPadding(it) } 104 | setPadding(paddingRect.left, paddingRect.top, paddingRect.right, value.dp) 105 | } 106 | 107 | inline var GradientDrawable.strokeAttr: Stroke? 108 | get() { 109 | return null 110 | } 111 | set(value) { 112 | value?.apply { setStroke(width.dp, Color.parseColor(color), dashWidth.dp, dashGap.dp) } 113 | } 114 | 115 | inline var GradientDrawable.strokeAttr_res: Stroke? 116 | get() { 117 | return null 118 | } 119 | set(value) { 120 | value?.apply { setStroke(width.dp, color_res, dashWidth.dp, dashGap.dp) } 121 | } 122 | 123 | inline var GradientDrawable.color_state_list: List> 124 | get() { 125 | return listOf(intArrayOf() to "#000000") 126 | } 127 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 128 | set(value) { 129 | val states = mutableListOf() 130 | val colors = mutableListOf() 131 | value.forEach { pair -> 132 | states.add(pair.first) 133 | colors.add(Color.parseColor(pair.second)) 134 | } 135 | color = ColorStateList(states.toTypedArray(), colors.toIntArray()) 136 | } 137 | 138 | /** 139 | * color res should be returned by ContextCompat.getColor() 140 | */ 141 | inline var GradientDrawable.color_state_list_res: List> 142 | get() { 143 | return listOf(intArrayOf() to 0) 144 | } 145 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 146 | set(value) { 147 | val states = mutableListOf() 148 | val colors = mutableListOf() 149 | value.forEach { pair -> 150 | states.add(pair.first) 151 | colors.add(pair.second) 152 | } 153 | color = ColorStateList(states.toTypedArray(), colors.toIntArray()) 154 | } 155 | 156 | /** 157 | * helper function for building [GradientDrawable] 158 | */ 159 | inline fun shape(init: GradientDrawable.() -> Unit) = GradientDrawable().apply(init) 160 | 161 | /** 162 | * helper class for set stroke for [GradientDrawable] 163 | * color res should be returned by ContextCompat.getColor() 164 | */ 165 | data class Stroke( 166 | var width: Number = 0f, 167 | var color: String = "#000000", 168 | var color_res:Int = 0, 169 | var dashWidth: Float = 0f, 170 | var dashGap: Float = 0f 171 | ) 172 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/ui/FirstFragment.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.ui 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.text.Spannable 7 | import android.text.SpannableStringBuilder 8 | import android.text.style.ForegroundColorSpan 9 | import android.util.Log 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.widget.Toast 14 | import androidx.fragment.app.Fragment 15 | import androidx.lifecycle.MutableLiveData 16 | import androidx.recyclerview.widget.LinearLayoutManager 17 | import androidx.recyclerview.widget.RecyclerView 18 | import com.bumptech.glide.Glide 19 | import com.bumptech.glide.request.animation.GlideAnimation 20 | import com.bumptech.glide.request.target.SimpleTarget 21 | import taylor.com.adapter.MyAdapter 22 | import taylor.com.bean.User 23 | import taylor.com.dsl.* 24 | 25 | /** 26 | * show how to use layout dsl in [Fragment] 27 | */ 28 | class FirstFragment : Fragment() { 29 | 30 | private val diamondUrl = 31 | "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2571315283,182922750&fm=26&gp=0.jpg" 32 | private val coinUrl = 33 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1589731033455&di=4266c84e59efb61a7f82c71b305954db&imgtype=0&src=http%3A%2F%2Fpic.90sjimg.com%2Fdesign%2F00%2F23%2F31%2F57%2F591be2a6807c3.png" 34 | 35 | private val nameLiveData = MutableLiveData() 36 | private val nameColorLiveData = MutableLiveData() 37 | private val avatarLiveData = MutableLiveData() 38 | private val commitLiveData = MutableLiveData() 39 | private val contentLiveData = MutableLiveData().apply { value = false } 40 | 41 | private lateinit var rv: RecyclerView 42 | 43 | private val target = object : SimpleTarget() { 44 | override fun onResourceReady( 45 | resource: Bitmap?, 46 | glideAnimation: GlideAnimation? 47 | ) { 48 | avatarLiveData.value = resource 49 | } 50 | } 51 | 52 | private val rootView by lazy { 53 | ConstraintLayout { 54 | layout_width = match_parent 55 | layout_height = match_parent 56 | 57 | ImageView { 58 | layout_id = "ivBack" 59 | layout_width = 40 60 | layout_height = 40 61 | margin_start = 20 62 | margin_top = 20 63 | src = R.drawable.ic_back_black 64 | start_toStartOf = parent_id 65 | top_toTopOf = parent_id 66 | onClick = onBackClick 67 | } 68 | 69 | TextView(R.style.myTextView) { 70 | layout_id = "tvCommit" 71 | layout_width = wrap_content 72 | layout_height = wrap_content 73 | text = "commit" 74 | gravity = gravity_center 75 | textStyle = bold 76 | align_vertical_to = "ivBack" 77 | center_horizontal = true 78 | padding = 10 79 | onClick = { _: View -> 80 | Toast.makeText(context, "on click", Toast.LENGTH_LONG).show() 81 | } 82 | shape = shape { 83 | corner_radius = 10 84 | shape = shape_rectangle 85 | gradientType = gradient_type_linear 86 | orientation = gradient_left_right 87 | gradient_colors = listOf("#ffff00", "#0000ff") 88 | strokeAttr = Stroke(5, "#000000", dashWidth = 2f, dashGap = 3f) 89 | color_state_list = listOf( 90 | intArrayOf(state_enable) to "#007EFF", 91 | intArrayOf(state_disable) to "#FDB2DA" 92 | ) 93 | } 94 | bindLiveData = liveDataBinder(commitLiveData) { 95 | action = { 96 | (it as? Boolean)?.let { 97 | isEnabled = it 98 | } 99 | } 100 | } 101 | } 102 | 103 | ImageView { 104 | layout_width = 40 105 | layout_height = 40 106 | src = R.drawable.ic_member_more 107 | align_vertical_to = "ivBack" 108 | end_toEndOf = parent_id 109 | margin_end = 20 110 | } 111 | 112 | View { 113 | layout_id = "vDivider" 114 | layout_width = match_parent 115 | layout_height = 1 116 | margin_top = 10 117 | background_color = "#eeeeee" 118 | top_toBottomOf = "tvCommit" 119 | } 120 | 121 | Layer { 122 | layout_id = "layer" 123 | layout_width = wrap_content 124 | layout_height = wrap_content 125 | referenceIds = "ivDiamond,tvName,tvContent,ivAvatar,tvTime,tvSub" 126 | background_res = R.drawable.tag_checked_shape 127 | start_toStartOf = "ivDiamond" 128 | top_toTopOf = "ivDiamond" 129 | bottom_toBottomOf = "tvTime" 130 | end_toEndOf = "tvTime" 131 | } 132 | 133 | ImageView { 134 | layout_id = "ivDiamond" 135 | layout_width = 40 136 | layout_height = 40 137 | margin_start = 20 138 | margin_top = 40 139 | src = R.drawable.diamond_tag 140 | start_toStartOf = "ivBack" 141 | top_toBottomOf = "vDivider" 142 | margin_top = 10 143 | onClick = { _: View -> 144 | commitLiveData.postValue(true) 145 | } 146 | } 147 | 148 | TextView { 149 | layout_id = "tvName" 150 | layout_width = wrap_content 151 | layout_height = wrap_content 152 | margin_start = 5 153 | gravity = gravity_center 154 | padding = 10 155 | textSize = 20f 156 | textStyle = bold 157 | align_vertical_to = "ivDiamond" 158 | start_toEndOf = "ivDiamond" 159 | bindLiveData = liveDataBinder(nameLiveData) { 160 | action = { 161 | text = it as CharSequence 162 | } 163 | } 164 | bind = Binder("init title") { _, data -> 165 | text = data.toString() 166 | } 167 | 168 | onClick = { _: View -> 169 | commitLiveData.postValue(false) 170 | } 171 | 172 | } 173 | 174 | TextView { 175 | layout_id = "tvContent" 176 | layout_width = 0 177 | layout_height = wrap_content 178 | margin_top = 5 179 | text = "The changes were merged into release with so many bugs" 180 | textSize = 23f 181 | start_toStartOf = "ivDiamond" 182 | top_toBottomOf = "ivDiamond" 183 | end_toStartOf = "ivAvatar" 184 | bindLiveData = liveDataBinder(contentLiveData) { 185 | action = { 186 | (it as? Boolean)?.let { 187 | isEnabled = it 188 | } 189 | } 190 | } 191 | background_drawable_state_list = listOf( 192 | intArrayOf(state_enable) to shape { 193 | shape = shape_rectangle 194 | corner_radius = 10 195 | solid_color = "#FDB2DA" 196 | }, 197 | intArrayOf(state_disable) to shape { 198 | shape = shape_rectangle 199 | corner_radius = 10 200 | solid_color = "#80FDB2DA" 201 | } 202 | ) 203 | } 204 | 205 | ImageView { 206 | layout_id = "ivAvatar" 207 | layout_width = 100 208 | layout_height = 100 209 | margin_end = 20 210 | src = R.drawable.user_portrait_gender_female 211 | end_toEndOf = parent_id 212 | start_toEndOf = "tvContent" 213 | top_toTopOf = "tvContent" 214 | onClick = { 215 | contentLiveData.postValue(contentLiveData.value?.not()) 216 | } 217 | } 218 | 219 | TextView { 220 | layout_id = "tvSub" 221 | layout_width = wrap_content 222 | layout_height = wrap_content 223 | text = "merge it with mercy" 224 | textColor = "#c4747E8B" 225 | textSize = 18f 226 | start_toStartOf = "ivDiamond" 227 | top_toBottomOf = "tvContent" 228 | } 229 | 230 | TextView { 231 | layout_id = "tvTime" 232 | layout_width = wrap_content 233 | layout_height = wrap_content 234 | margin_top = 20 235 | text = "2020.04.30" 236 | end_toEndOf = "ivAvatar" 237 | top_toBottomOf = "ivAvatar" 238 | } 239 | 240 | TextView { 241 | layout_id = "tvCancel" 242 | layout_width = wrap_content 243 | layout_height = wrap_content 244 | margin_end = 30 245 | background_res = R.drawable.bg_orange_btn 246 | padding_start = 30 247 | padding_top = 10 248 | padding_end = 30 249 | padding_bottom = 10 250 | text = "cancel" 251 | margin_bottom = 20 252 | textSize = 20f 253 | textStyle = bold 254 | bottom_toBottomOf = parent_id 255 | end_toStartOf = "tvOk" 256 | start_toStartOf = parent_id 257 | horizontal_chain_style = packed 258 | onClick = onCancelClick 259 | } 260 | 261 | TextView { 262 | layout_id = "tvOk" 263 | layout_width = wrap_content 264 | layout_height = wrap_content 265 | background_res = R.drawable.bg_orange_btn 266 | padding_start = 30 267 | padding_top = 10 268 | margin_bottom = 20 269 | padding_end = 30 270 | padding_bottom = 10 271 | text = "Ok" 272 | textSize = 20f 273 | textStyle = bold 274 | bottom_toBottomOf = parent_id 275 | end_toEndOf = parent_id 276 | horizontal_chain_style = packed 277 | start_toEndOf = "tvCancel" 278 | } 279 | 280 | rv = RecyclerView { 281 | layout_id = "rvTest" 282 | layout_width = match_parent 283 | layout_height = wrap_content 284 | top_toBottomOf = "tvTime" 285 | bottom_toTopOf = "tvOk" 286 | onItemClick = onListItemClick 287 | margin_top = 10 288 | onItemLongClick = onItemLongClickListener 289 | } 290 | 291 | EditText { 292 | layout_width = match_parent 293 | layout_height = 50 294 | textSize = 20f 295 | background_color = "#00ffff" 296 | top_toBottomOf = "rvTest" 297 | onTextChange = textWatcher { 298 | onTextChanged = { text: CharSequence?, start: Int, count: Int, after: Int -> 299 | Log.v("ttaylor", "tag=text change, FirstFragment.() text=${text}") 300 | } 301 | } 302 | } 303 | } 304 | } 305 | 306 | private val onCancelClick = { v: View -> 307 | nameLiveData.value = "new title" 308 | Unit 309 | } 310 | 311 | private val onItemLongClickListener = { v: View, i: Int, x: Float, y: Float -> 312 | adapter.myBean?.get(i)?.let { 313 | Log.v("ttaylor", "on item(${it.name}) long click ") 314 | } 315 | Unit 316 | } 317 | 318 | private val onListItemClick = { v: View, i: Int, x: Float, y: Float -> 319 | adapter.myBean?.get(i)?.let { 320 | nameLiveData.value = SpannableStringBuilder(it.name).apply { 321 | setSpan( 322 | ForegroundColorSpan(Color.RED), 323 | 0, 324 | it.name.indexOf(" "), 325 | Spannable.SPAN_INCLUSIVE_EXCLUSIVE 326 | ) 327 | val color = if (it.gender == 1) "#b300ff00" else "#b3ff00ff" 328 | setSpan( 329 | ForegroundColorSpan(Color.parseColor(color)), 330 | it.name.indexOf(" "), 331 | it.name.lastIndex + 1, 332 | Spannable.SPAN_EXCLUSIVE_INCLUSIVE 333 | ) 334 | } 335 | 336 | if (it.gender == 1) Glide.with(context).load(diamondUrl).asBitmap().into(target) 337 | else Glide.with(context).load(coinUrl).asBitmap().into(target) 338 | } 339 | v.onChildViewClick("tvStart", "tvEnd", x = x, y = y) { 340 | Log.v("ttaylor", "tag=adsf, FirstFragment.() on two child clicked") 341 | } 342 | false 343 | } 344 | 345 | private val onBackClick = { v: View -> 346 | activity?.finish() 347 | Unit 348 | } 349 | 350 | private val users = listOf( 351 | User("Taylor Swift", 10), 352 | User("Linda candy", 20, 1), 353 | User("Cindy json", 30, 1), 354 | User("Evian Mary", 40, 1) 355 | ) 356 | 357 | private var adapter = MyAdapter(users) 358 | 359 | override fun onCreateView( 360 | inflater: LayoutInflater, container: ViewGroup?, 361 | savedInstanceState: Bundle? 362 | ): View? { 363 | return rootView 364 | } 365 | 366 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 367 | super.onViewCreated(view, savedInstanceState) 368 | rv.layoutManager = LinearLayoutManager(context) 369 | rv.adapter = adapter 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.ui 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.widget.TextView 6 | 7 | class MainActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | 13 | supportFragmentManager.beginTransaction().apply { 14 | add(R.id.container, FirstFragment(), "first") 15 | commit() 16 | } 17 | 18 | findViewById(R.id.tvName).setText("taylor") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/LineFeedLayout.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | 9 | /** 10 | * a special [ViewGroup] acts like [LinearLayout], 11 | * it spreads the children from left to right until there is not enough horizontal space for them, 12 | * then the next child will be placed at a new line 13 | */ 14 | class LineFeedLayout @JvmOverloads constructor( 15 | context: Context, 16 | attrs: AttributeSet? = null, 17 | defStyleAttr: Int = 0 18 | ) : ViewGroup(context, attrs, defStyleAttr) { 19 | 20 | var horizontalGap: Int = 0 21 | var verticalGap: Int = 0 22 | 23 | /** 24 | * the height of [LineFeedLayout] depends on how much lines it has 25 | */ 26 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 27 | measureChildren(widthMeasureSpec, heightMeasureSpec) 28 | val heightMode = MeasureSpec.getMode(heightMeasureSpec) 29 | val width = MeasureSpec.getSize(widthMeasureSpec) 30 | var height = 0 31 | if (heightMode == MeasureSpec.EXACTLY) { 32 | height = MeasureSpec.getSize(heightMeasureSpec) 33 | } else { 34 | var remainWidth = width 35 | (0 until childCount).map { getChildAt(it) }.forEach { child -> 36 | val lp = child.layoutParams as MarginLayoutParams 37 | if (isNewLine(lp, child, remainWidth)) { 38 | remainWidth = width - child.measuredWidth 39 | height += (lp.topMargin + lp.bottomMargin + child.measuredHeight + verticalGap) 40 | } else { 41 | remainWidth -= child.measuredWidth 42 | if (height == 0) height = 43 | (lp.topMargin + lp.bottomMargin + child.measuredHeight + verticalGap) 44 | } 45 | remainWidth -= (lp.leftMargin + lp.rightMargin + horizontalGap) 46 | } 47 | } 48 | 49 | setMeasuredDimension(width, height) 50 | } 51 | 52 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { 53 | var left = 0 54 | var top = 0 55 | var lastBottom = 0 56 | var count = 0 57 | (0 until childCount).map { getChildAt(it) }.forEach { child -> 58 | val lp = child.layoutParams as MarginLayoutParams 59 | if (isNewLine(lp, child, r - l - left)) { 60 | left = -lp.leftMargin 61 | top = lastBottom 62 | lastBottom = 0 63 | } 64 | val childLeft = left + lp.leftMargin 65 | val childTop = top + lp.topMargin 66 | child.layout( 67 | childLeft, 68 | childTop, 69 | childLeft + child.measuredWidth, 70 | childTop + child.measuredHeight 71 | ) 72 | if (lastBottom == 0) lastBottom = child.bottom + lp.bottomMargin + verticalGap 73 | left += child.measuredWidth + lp.leftMargin + lp.rightMargin + horizontalGap 74 | count++ 75 | } 76 | } 77 | 78 | /** 79 | * place the [child] in a new line or not 80 | * 81 | * @param lp LayoutParams of [child] 82 | * @param child child view of [LineFeedLayout] 83 | * @param remainWidth the remain space of one line in [LineFeedLayout] 84 | * @param horizontalGap the horizontal gap for the children of [LineFeedLayout] 85 | */ 86 | private fun isNewLine( 87 | lp: MarginLayoutParams, 88 | child: View, 89 | remainWidth: Int, 90 | ): Boolean { 91 | val childOccupation = lp.leftMargin + child.measuredWidth + lp.rightMargin 92 | return childOccupation > remainWidth 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/OneViewGroup.kt: -------------------------------------------------------------------------------- 1 | package test.taylor.com.taylorcode.ui.one 2 | 3 | import android.content.Context 4 | import android.graphics.* 5 | import android.os.Build 6 | import android.text.Layout 7 | import android.text.StaticLayout 8 | import android.text.TextPaint 9 | import android.util.AttributeSet 10 | import android.view.GestureDetector 11 | import android.view.MotionEvent 12 | import android.view.ViewGroup 13 | import androidx.annotation.RequiresApi 14 | import androidx.appcompat.widget.AppCompatImageView 15 | import taylor.com.dsl.dp 16 | import taylor.com.dsl.parent_id 17 | import taylor.com.dsl.sp 18 | import taylor.com.dsl.toLayoutId 19 | import kotlin.math.min 20 | 21 | @RequiresApi(Build.VERSION_CODES.M) 22 | class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) { 23 | 24 | private val drawableMap = HashMap() 25 | private val drawables = mutableListOf() 26 | private var gestureDetector: GestureDetector? = null 27 | 28 | fun addDrawable(drawable: Drawable) { 29 | drawables.add(drawable) 30 | drawableMap[drawable.layoutId] = drawable 31 | } 32 | 33 | /** 34 | * find [Drawable] by id 35 | */ 36 | fun findDrawable(id: String): T? = drawableMap[id.toLayoutId()] as? T 37 | 38 | /** 39 | * set item click listener for [OneViewGroup] whick detect child [Drawable]'s click event 40 | */ 41 | fun setOnItemClickListener(onItemClickListener: (String) -> Unit) { 42 | gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { 43 | override fun onShowPress(e: MotionEvent?) { 44 | } 45 | 46 | override fun onSingleTapUp(e: MotionEvent?): Boolean { 47 | e?.let { 48 | findDrawableUnder(e.x, e.y)?.let { onItemClickListener.invoke(it.layoutIdString) } 49 | } 50 | return true 51 | } 52 | 53 | override fun onDown(e: MotionEvent?): Boolean { 54 | return true 55 | } 56 | 57 | override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { 58 | return false 59 | } 60 | 61 | override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean { 62 | return false 63 | } 64 | 65 | override fun onLongPress(e: MotionEvent?) { 66 | } 67 | }) 68 | } 69 | 70 | /** 71 | * find child [Drawable] according to coordinate 72 | */ 73 | private fun findDrawableUnder(x: Float, y: Float): Drawable? { 74 | drawables.forEach { 75 | if (it.rect.contains(x.toInt(), y.toInt())) { 76 | return it 77 | } 78 | } 79 | return null 80 | } 81 | 82 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 83 | measureChildren(widthMeasureSpec, heightMeasureSpec) 84 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 85 | drawables.forEach { it.doMeasure(widthMeasureSpec, heightMeasureSpec) } 86 | } 87 | 88 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 89 | val parentWidth = right - left 90 | val parentHeight = bottom - top 91 | drawables.forEach { 92 | val left = getChildLeft(it, parentWidth) 93 | val top = getChildTop(it, parentHeight) 94 | it.doLayout(changed, left, top, left + it.layoutMeasuredWidth, top + it.layoutMeasuredHeight) 95 | } 96 | } 97 | 98 | override fun onDraw(canvas: Canvas?) { 99 | super.onDraw(canvas) 100 | drawables.forEach { it.doDraw(canvas) } 101 | } 102 | 103 | override fun onTouchEvent(event: MotionEvent?): Boolean { 104 | return gestureDetector?.onTouchEvent(event) ?: super.onTouchEvent(event) 105 | } 106 | 107 | /** 108 | * get the top of [drawable] relative to the [OneViewGroup] 109 | */ 110 | private fun getChildTop(drawable: Drawable, parentHeight: Int): Int { 111 | val parentId = parent_id.toLayoutId() 112 | return when { 113 | drawable.topPercent != -1f -> (parentHeight * drawable.topPercent).toInt() 114 | drawable.centerVerticalOf != -1 -> { 115 | if (drawable.centerVerticalOf == parentId) { 116 | (parentHeight - drawable.layoutMeasuredHeight) / 2 117 | } else { 118 | (drawableMap[drawable.centerVerticalOf]?.let { it.layoutTop + (it.layoutBottom - it.layoutTop) / 2 } ?: 0) - drawable.layoutMeasuredHeight / 2 119 | } 120 | } 121 | drawable.topToBottomOf != -1 -> { 122 | val b = if (drawable.topToBottomOf == parentId) bottom else drawableMap[drawable.topToBottomOf]?.layoutBottom ?: 0 123 | (b + drawable.layoutTopMargin) 124 | } 125 | drawable.topToTopOf != -1 -> { 126 | val t = if (drawable.topToTopOf == parentId) top else drawableMap[drawable.topToTopOf]?.layoutTop ?: 0 127 | (t + drawable.layoutTopMargin) 128 | } 129 | drawable.bottomToTopOf != -1 -> { 130 | val t = if (drawable.bottomToTopOf == parentId) top else drawableMap[drawable.bottomToTopOf]?.layoutTop ?: 0 131 | (t - drawable.layoutBottomMargin) - drawable.layoutMeasuredHeight 132 | } 133 | drawable.bottomToBottomOf != -1 -> { 134 | val b = if (drawable.bottomToBottomOf == parentId) bottom else drawableMap[drawable.bottomToBottomOf]?.layoutBottom ?: 0 135 | (b - drawable.layoutBottomMargin) - drawable.layoutMeasuredHeight 136 | } 137 | else -> 0 138 | } 139 | } 140 | 141 | /** 142 | * get the left of [drawable] relative to the [OneViewGroup] 143 | */ 144 | private fun getChildLeft(drawable: Drawable, parentWidth: Int): Int { 145 | val parentId = parent_id.toLayoutId() 146 | return when { 147 | drawable.leftPercent != -1f -> (parentWidth * drawable.leftPercent).toInt() 148 | drawable.centerHorizontalOf != -1 -> { 149 | if (drawable.centerHorizontalOf == parentId) { 150 | (parentWidth - drawable.layoutMeasuredWidth) / 2 151 | } else { 152 | (drawableMap[drawable.centerHorizontalOf]?.let { it.layoutLeft + (it.layoutRight - it.layoutLeft) / 2 } ?: 0) - drawable.layoutMeasuredWidth / 2 153 | } 154 | } 155 | drawable.startToEndOf != -1 -> { 156 | val r = if (drawable.startToEndOf == parentId) right else drawableMap[drawable.startToEndOf]?.layoutRight ?: 0 157 | (r + drawable.layoutLeftMargin) 158 | } 159 | drawable.startToStartOf != -1 -> { 160 | val l = if (drawable.startToStartOf == parentId) left else drawableMap[drawable.startToStartOf]?.layoutLeft ?: 0 161 | (l + drawable.layoutLeftMargin) 162 | } 163 | drawable.endToStartOf != -1 -> { 164 | val l = if (drawable.endToStartOf == parentId) left else drawableMap[drawable.endToStartOf]?.layoutLeft ?: 0 165 | (l - drawable.layoutRightMargin) - drawable.layoutMeasuredWidth 166 | } 167 | drawable.endToEndOf != -1 -> { 168 | val r = if (drawable.endToEndOf == parentId) right else drawableMap[drawable.endToEndOf]?.layoutRight ?: 0 169 | (r - drawable.layoutRightMargin) - drawable.layoutMeasuredWidth 170 | } 171 | else -> 0 172 | } 173 | } 174 | 175 | /** 176 | * anything could be draw in [OneViewGroup] 177 | */ 178 | interface Drawable { 179 | /** 180 | * the measured dimension of [Drawable] 181 | */ 182 | var layoutMeasuredWidth: Int 183 | var layoutMeasuredHeight: Int 184 | 185 | /** 186 | * the frame rect of [Drawable] 187 | */ 188 | var layoutLeft: Int 189 | var layoutRight: Int 190 | var layoutTop: Int 191 | var layoutBottom: Int 192 | val rect: Rect 193 | get() = Rect(layoutLeft, layoutTop, layoutRight, layoutBottom) 194 | 195 | /** 196 | * the unique id of this [Drawable] in int 197 | */ 198 | var layoutId: Int 199 | 200 | /** 201 | * the unique id of this [Drawable] in string 202 | */ 203 | var layoutIdString: String 204 | 205 | /** 206 | * the relative position of this [Drawable] to another 207 | */ 208 | var leftPercent: Float 209 | var topPercent: Float 210 | var startToStartOf: Int 211 | var startToEndOf: Int 212 | var endToEndOf: Int 213 | var endToStartOf: Int 214 | var topToTopOf: Int 215 | var topToBottomOf: Int 216 | var bottomToTopOf: Int 217 | var bottomToBottomOf: Int 218 | var centerHorizontalOf: Int 219 | var centerVerticalOf: Int 220 | 221 | /** 222 | * dimension of [Drawable] 223 | */ 224 | var layoutWidth: Int 225 | var layoutHeight: Int 226 | 227 | /** 228 | * inner padding of [Drawable] 229 | */ 230 | var layoutPaddingStart: Int 231 | var layoutPaddingEnd: Int 232 | var layoutPaddingTop: Int 233 | var layoutPaddingBottom: Int 234 | 235 | /** 236 | * out margin of [Drawable] 237 | */ 238 | var layoutTopMargin: Int 239 | var layoutBottomMargin: Int 240 | var layoutLeftMargin: Int 241 | var layoutRightMargin: Int 242 | 243 | /** 244 | * the the frame rect of [Drawable] is set after this function 245 | */ 246 | fun setRect(left: Int, top: Int, right: Int, bottom: Int) { 247 | this.layoutLeft = left 248 | this.layoutRight = right 249 | this.layoutTop = top 250 | this.layoutBottom = bottom 251 | } 252 | 253 | /** 254 | * the measured width and height of [Drawable] is set after this function 255 | */ 256 | fun setDimension(width: Int, height: Int) { 257 | this.layoutMeasuredWidth = width 258 | this.layoutMeasuredHeight = height 259 | } 260 | 261 | fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) 262 | fun doLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) 263 | fun doDraw(canvas: Canvas?) 264 | } 265 | } 266 | 267 | 268 | ///** 269 | // * one kind of drawable shows image 270 | // */ 271 | //class Image : OneViewGroup.Drawable() { 272 | // override fun measure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 273 | // } 274 | // 275 | // override fun draw(canvas: Canvas?) { 276 | // } 277 | //} 278 | 279 | // 280 | /** 281 | * one kind of [OneViewGroup.Drawable] shows image 282 | * and it is a special [ImageView] implemented [OneViewGroup.Drawable] 283 | */ 284 | class ImageDrawable @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), OneViewGroup.Drawable { 285 | override var leftPercent: Float = -1f 286 | override var topPercent: Float = -1f 287 | override var startToStartOf: Int = -1 288 | override var startToEndOf: Int = -1 289 | override var endToEndOf: Int = -1 290 | override var endToStartOf: Int = -1 291 | override var topToTopOf: Int = -1 292 | override var topToBottomOf: Int = -1 293 | override var bottomToTopOf: Int = -1 294 | override var bottomToBottomOf: Int = -1 295 | override var centerHorizontalOf: Int = -1 296 | override var centerVerticalOf: Int = -1 297 | override var layoutWidth: Int = 0 298 | override var layoutHeight: Int = 0 299 | 300 | override var layoutMeasuredWidth: Int = 0 301 | get() = measuredWidth 302 | override var layoutMeasuredHeight: Int = 0 303 | get() = measuredHeight 304 | override var layoutLeft: Int = 0 305 | get() = left 306 | override var layoutRight: Int = 0 307 | get() = right 308 | override var layoutTop: Int = 0 309 | get() = top 310 | override var layoutBottom: Int = 0 311 | get() = bottom 312 | override var layoutId: Int = 0 313 | get() = id 314 | override var layoutIdString: String = "" 315 | override var layoutPaddingStart: Int = 0 316 | get() = paddingStart 317 | override var layoutPaddingEnd: Int = 0 318 | get() = paddingEnd 319 | override var layoutPaddingTop: Int = 0 320 | get() = paddingTop 321 | override var layoutPaddingBottom: Int = 0 322 | get() = paddingBottom 323 | override var layoutTopMargin: Int = 0 324 | get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0 325 | override var layoutBottomMargin: Int = 0 326 | get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0 327 | override var layoutLeftMargin: Int = 0 328 | get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0 329 | override var layoutRightMargin: Int = 0 330 | get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0 331 | 332 | override fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 333 | } 334 | 335 | override fun doLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 336 | layout(left, top, right, bottom) 337 | } 338 | 339 | override fun doDraw(canvas: Canvas?) { 340 | } 341 | } 342 | 343 | /** 344 | * one kind of [OneViewGroup.Drawable] shows text 345 | */ 346 | class Text : OneViewGroup.Drawable { 347 | private var textPaint: TextPaint? = null 348 | private var staticLayout: StaticLayout? = null 349 | 350 | var text: CharSequence = "" 351 | var textSize: Float = 0f 352 | var textColor: Int = Color.parseColor("#ffffff") 353 | var spaceAdd: Float = 0f 354 | var spaceMulti: Float = 1.0f 355 | var maxLines = 1 356 | var maxWidth = 0 357 | var gravity: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL 358 | var shapePaint: Paint? = null 359 | var drawableShape: Shape? = null 360 | set(value) { 361 | field = value 362 | shapePaint = Paint().apply { 363 | isAntiAlias = true 364 | style = Paint.Style.FILL 365 | color = Color.parseColor(value?.color) 366 | } 367 | } 368 | override var layoutMeasuredWidth: Int = 0 369 | override var layoutMeasuredHeight: Int = 0 370 | override var layoutLeft: Int = 0 371 | override var layoutRight: Int = 0 372 | override var layoutTop: Int = 0 373 | override var layoutBottom: Int = 0 374 | override var layoutId: Int = 0 375 | override var layoutIdString: String = "" 376 | override var leftPercent: Float = -1f 377 | override var topPercent: Float = -1f 378 | override var startToStartOf: Int = -1 379 | override var startToEndOf: Int = -1 380 | override var endToEndOf: Int = -1 381 | override var endToStartOf: Int = -1 382 | override var topToTopOf: Int = -1 383 | override var topToBottomOf: Int = -1 384 | override var bottomToTopOf: Int = -1 385 | override var bottomToBottomOf: Int = -1 386 | override var centerHorizontalOf: Int = -1 387 | override var centerVerticalOf: Int = -1 388 | override var layoutWidth: Int = 0 389 | override var layoutHeight: Int = 0 390 | override var layoutPaddingStart: Int = 0 391 | override var layoutPaddingEnd: Int = 0 392 | override var layoutPaddingTop: Int = 0 393 | override var layoutPaddingBottom: Int = 0 394 | override var layoutTopMargin: Int = 0 395 | override var layoutBottomMargin: Int = 0 396 | override var layoutLeftMargin: Int = 0 397 | override var layoutRightMargin: Int = 0 398 | 399 | @RequiresApi(Build.VERSION_CODES.M) 400 | override fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 401 | if (textPaint == null) { 402 | textPaint = TextPaint().apply { 403 | isAntiAlias = true 404 | textSize = this@Text.textSize 405 | color = textColor 406 | } 407 | } 408 | 409 | val measureWidth = if (layoutWidth != 0) layoutWidth else min(textPaint!!.measureText(text.toString()).toInt(), maxWidth) 410 | if (staticLayout == null) { 411 | staticLayout = StaticLayout.Builder.obtain(text, 0, text.length, textPaint!!, measureWidth) 412 | .setAlignment(gravity) 413 | .setLineSpacing(spaceAdd, spaceMulti) 414 | .setIncludePad(false) 415 | .setMaxLines(maxLines).build() 416 | } 417 | 418 | val measureHeight = staticLayout!!.height 419 | setDimension(measureWidth + layoutPaddingEnd + layoutPaddingStart, measureHeight + layoutPaddingTop + layoutPaddingBottom) 420 | } 421 | 422 | override fun doLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 423 | setRect(left, top, right, bottom) 424 | } 425 | 426 | override fun doDraw(canvas: Canvas?) { 427 | canvas?.save() 428 | canvas?.translate(layoutLeft.toFloat(), layoutTop.toFloat()) 429 | drawBackground(canvas) 430 | canvas?.translate(layoutPaddingStart.toFloat(), layoutPaddingTop.toFloat()) 431 | staticLayout?.draw(canvas) 432 | canvas?.restore() 433 | } 434 | 435 | private fun drawBackground(canvas: Canvas?) { 436 | if (drawableShape == null) return 437 | val _shape = drawableShape!! 438 | if (_shape.radius != 0f) { 439 | canvas?.drawRoundRect(RectF(0f, 0f, layoutMeasuredWidth.toFloat(), layoutMeasuredHeight.toFloat()), _shape.radius, _shape.radius, shapePaint!!) 440 | } else if (_shape.corners != null) { 441 | _shape.path!!.apply { 442 | addRoundRect( 443 | RectF(0f, 0f, layoutMeasuredWidth.toFloat(), layoutMeasuredHeight.toFloat()), 444 | _shape.corners!!.radii, 445 | Path.Direction.CCW 446 | ) 447 | } 448 | canvas?.drawPath(_shape.path!!, shapePaint!!) 449 | } 450 | } 451 | } 452 | // 453 | 454 | /** 455 | * a round rect shows in background 456 | */ 457 | class Shape { 458 | var color: String? = null 459 | var colors: List? = null 460 | var radius: Float = 0f 461 | internal var path: Path? = null 462 | var corners: Corners? = null 463 | set(value) { 464 | field = value 465 | path = Path() 466 | } 467 | 468 | class Corners( 469 | var leftTopRx: Float = 0f, 470 | var leftTopRy: Float = 0f, 471 | var leftBottomRx: Float = 0f, 472 | var LeftBottomRy: Float = 0f, 473 | var rightTopRx: Float = 0f, 474 | var rightTopRy: Float = 0f, 475 | var rightBottomRx: Float = 0f, 476 | var rightBottomRy: Float = 0f 477 | ) { 478 | val radii: FloatArray 479 | get() = floatArrayOf(leftTopRx, leftTopRy, rightTopRx, rightTopRy, rightBottomRx, rightBottomRy, leftBottomRx, LeftBottomRy) 480 | } 481 | } 482 | 483 | @RequiresApi(Build.VERSION_CODES.M) 484 | inline fun OneViewGroup.text(init: Text.() -> Unit) = Text().apply(init).also { addDrawable(it) } 485 | 486 | @RequiresApi(Build.VERSION_CODES.M) 487 | inline fun OneViewGroup.image(init: ImageDrawable.() -> Unit) = ImageDrawable(context).apply(init).also { 488 | addView(it) 489 | addDrawable(it) 490 | } 491 | 492 | 493 | // 494 | fun OneViewGroup.drawableShape(init: Shape.() -> Unit): Shape = Shape().apply(init) 495 | 496 | fun Shape.corners(init: Shape.Corners.() -> Unit): Shape.Corners = Shape.Corners().apply(init) 497 | 498 | val gravity_center = Layout.Alignment.ALIGN_CENTER 499 | val gravity_left = Layout.Alignment.ALIGN_NORMAL 500 | 501 | inline var OneViewGroup.Drawable.drawable_top_margin: Int 502 | get() { 503 | return 0 504 | } 505 | set(value) { 506 | layoutTopMargin = value.dp 507 | } 508 | 509 | inline var OneViewGroup.Drawable.drawable_bottom_margin: Int 510 | get() { 511 | return 0 512 | } 513 | set(value) { 514 | layoutBottomMargin = value.dp 515 | } 516 | 517 | inline var OneViewGroup.Drawable.drawable_left_margin: Int 518 | get() { 519 | return 0 520 | } 521 | set(value) { 522 | layoutLeftMargin = value.dp 523 | } 524 | 525 | inline var OneViewGroup.Drawable.drawable_right_margin: Int 526 | get() { 527 | return 0 528 | } 529 | set(value) { 530 | layoutRightMargin = value.dp 531 | } 532 | 533 | inline var OneViewGroup.Drawable.drawable_padding_start: Int 534 | get() { 535 | return 0 536 | } 537 | set(value) { 538 | layoutPaddingStart = value.dp 539 | } 540 | 541 | inline var OneViewGroup.Drawable.drawable_padding_end: Int 542 | get() { 543 | return 0 544 | } 545 | set(value) { 546 | layoutPaddingEnd = value.dp 547 | } 548 | 549 | inline var OneViewGroup.Drawable.drawable_padding_top: Int 550 | get() { 551 | return 0 552 | } 553 | set(value) { 554 | layoutPaddingTop = value.dp 555 | } 556 | 557 | inline var OneViewGroup.Drawable.drawable_padding_bottom: Int 558 | get() { 559 | return 0 560 | } 561 | set(value) { 562 | layoutPaddingBottom = value.dp 563 | } 564 | 565 | inline var OneViewGroup.Drawable.drawable_layout_id: String 566 | get() { 567 | return "" 568 | } 569 | set(value) { 570 | layoutIdString = value 571 | layoutId = value.toLayoutId() 572 | } 573 | 574 | inline var OneViewGroup.Drawable.drawable_start_to_start_of: String 575 | get() { 576 | return "" 577 | } 578 | set(value) { 579 | startToStartOf = value.toLayoutId() 580 | } 581 | 582 | inline var OneViewGroup.Drawable.drawable_start_to_end_of: String 583 | get() { 584 | return "" 585 | } 586 | set(value) { 587 | startToEndOf = value.toLayoutId() 588 | } 589 | 590 | inline var OneViewGroup.Drawable.drawable_end_to_start_of: String 591 | get() { 592 | return "" 593 | } 594 | set(value) { 595 | endToStartOf = value.toLayoutId() 596 | } 597 | 598 | inline var OneViewGroup.Drawable.drawable_end_to_end_of: String 599 | get() { 600 | return "" 601 | } 602 | set(value) { 603 | endToEndOf = value.toLayoutId() 604 | } 605 | 606 | inline var OneViewGroup.Drawable.drawable_top_to_top_of: String 607 | get() { 608 | return "" 609 | } 610 | set(value) { 611 | topToTopOf = value.toLayoutId() 612 | } 613 | 614 | inline var OneViewGroup.Drawable.drawable_top_to_bottom_of: String 615 | get() { 616 | return "" 617 | } 618 | set(value) { 619 | topToBottomOf = value.toLayoutId() 620 | } 621 | 622 | inline var OneViewGroup.Drawable.drawable_bottom_to_top_of: String 623 | get() { 624 | return "" 625 | } 626 | set(value) { 627 | bottomToTopOf = value.toLayoutId() 628 | } 629 | 630 | inline var OneViewGroup.Drawable.drawable_bottom_to_bottom_of: String 631 | get() { 632 | return "" 633 | } 634 | set(value) { 635 | bottomToBottomOf = value.toLayoutId() 636 | } 637 | 638 | inline var OneViewGroup.Drawable.drawable_center_horizontal_of: String 639 | get() { 640 | return "" 641 | } 642 | set(value) { 643 | centerHorizontalOf = value.toLayoutId() 644 | } 645 | 646 | inline var OneViewGroup.Drawable.drawable_center_vertical_of: String 647 | get() { 648 | return "" 649 | } 650 | set(value) { 651 | centerVerticalOf = value.toLayoutId() 652 | } 653 | 654 | inline var Text.drawable_gravity: Layout.Alignment 655 | get() { 656 | return Layout.Alignment.ALIGN_NORMAL 657 | } 658 | set(value) { 659 | gravity = value 660 | } 661 | 662 | inline var Text.drawable_text_size: Float 663 | get() { 664 | return 0f 665 | } 666 | set(value) { 667 | textSize = value.sp 668 | } 669 | 670 | inline var Text.drawable_max_width: Int 671 | get() { 672 | return 0 673 | } 674 | set(value) { 675 | maxWidth = value.dp 676 | } 677 | 678 | inline var Text.drawable_layout_width: Int 679 | get() { 680 | return 0 681 | } 682 | set(value) { 683 | layoutWidth = value.dp 684 | } 685 | 686 | inline var Text.drawable_layout_height: Int 687 | get() { 688 | return 0 689 | } 690 | set(value) { 691 | layoutHeight = value.dp 692 | } 693 | 694 | inline var Text.drawable_text_color: String 695 | get() { 696 | return "" 697 | } 698 | set(value) { 699 | textColor = Color.parseColor(value) 700 | } 701 | 702 | inline var Text.drawable_text: CharSequence 703 | get() { 704 | return "" 705 | } 706 | set(value) { 707 | text = value 708 | } 709 | 710 | inline var Text.drawable_max_lines: Int 711 | get() { 712 | return 1 713 | } 714 | set(value) { 715 | maxLines = value 716 | } 717 | 718 | inline var Text.drawable_shape: Shape 719 | get() { 720 | return Shape() 721 | } 722 | set(value) { 723 | drawableShape = value 724 | } 725 | 726 | 727 | inline var Shape.radius: Float 728 | get() { 729 | return 0f 730 | } 731 | set(value) { 732 | radius = value.dp 733 | } 734 | // -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/PercentLayout.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.SparseArray 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import taylor.com.dsl.parent_id 9 | import taylor.com.dsl.toLayoutId 10 | 11 | /** 12 | * the child in [PercentLayout] use [LayoutParam.leftPercent] and [LayoutParam.topPercent] to locate itself, which is a percentage of the width and height of [PercentLayout]. 13 | * 14 | * [PercentLayout] must have a specific width or height, or the children view of it will have no idea where to locate. 15 | */ 16 | class PercentLayout 17 | @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) { 18 | 19 | private val childMap = SparseArray() 20 | 21 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 22 | measureChildren(widthMeasureSpec, heightMeasureSpec) 23 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 24 | } 25 | 26 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 27 | val parentWidth = right - left 28 | val parentHeight = bottom - top 29 | (0 until childCount).map { getChildAt(it) }.forEach { child -> 30 | val lp = child.layoutParams as LayoutParam 31 | val childLeft = getChildLeft(lp, parentWidth, child) 32 | val childTop = getChildTop(lp, parentHeight, child) 33 | child.layout(childLeft, childTop, childLeft + child.measuredWidth, childTop + child.measuredHeight) 34 | } 35 | } 36 | 37 | private fun getChildTop(lp: LayoutParam, parentHeight: Int, child: View): Int { 38 | val parentId = parent_id.toLayoutId() 39 | return when { 40 | lp.topPercent != -1f -> (parentHeight * lp.topPercent).toInt() 41 | lp.centerVerticalOf != -1 -> { 42 | if (lp.centerVerticalOf == parentId) { 43 | (parentHeight - child.measuredHeight) / 2 44 | } else { 45 | (childMap.get(lp.centerVerticalOf)?.let { it.top + (it.bottom - it.top) / 2 } ?: 0) - child.measuredHeight / 2 46 | } 47 | } 48 | lp.topToBottomOf != -1 -> { 49 | val b = if (lp.topToBottomOf == parentId) bottom else childMap.get(lp.topToBottomOf)?.bottom ?: 0 50 | (b + lp.topMargin) 51 | } 52 | lp.topToTopOf != -1 -> { 53 | val t = if (lp.topToTopOf == parentId) top else childMap.get(lp.topToTopOf)?.top ?: 0 54 | (t + lp.topMargin) 55 | } 56 | lp.bottomToTopOf != -1 -> { 57 | val t = if (lp.bottomToTopOf == parentId) top else childMap.get(lp.bottomToTopOf)?.top ?: 0 58 | (t - lp.bottomMargin) - child.measuredHeight 59 | } 60 | lp.bottomToBottomOf != -1 -> { 61 | val b = if (lp.bottomToBottomOf == parentId) bottom else childMap.get(lp.bottomToBottomOf)?.bottom ?: 0 62 | (b - lp.bottomMargin) - child.measuredHeight 63 | } 64 | else -> 0 65 | } 66 | } 67 | 68 | private fun getChildLeft(lp: LayoutParam, parentWidth: Int, child: View): Int { 69 | val parentId = parent_id.toLayoutId() 70 | return when { 71 | lp.leftPercent != -1f -> (parentWidth * lp.leftPercent).toInt() 72 | lp.centerHorizontalOf != -1 -> { 73 | if (lp.centerHorizontalOf == parentId) { 74 | (parentWidth - child.measuredWidth) / 2 75 | } else { 76 | (childMap.get(lp.centerHorizontalOf)?.let { it.left + (it.right - it.left) / 2 } ?: 0) - child.measuredWidth / 2 77 | } 78 | } 79 | lp.startToEndOf != -1 -> { 80 | val r = if (lp.startToEndOf == parentId) right else childMap.get(lp.startToEndOf)?.right ?: 0 81 | (r + lp.marginStart) 82 | } 83 | lp.startToStartOf != -1 -> { 84 | val l = if (lp.startToStartOf == parentId) left else childMap.get(lp.startToStartOf)?.left ?: 0 85 | (l + lp.marginStart) 86 | } 87 | lp.endToStartOf != -1 -> { 88 | val l = if (lp.endToStartOf == parentId) left else childMap.get(lp.endToStartOf)?.left ?: 0 89 | (l - lp.marginEnd) - child.measuredWidth 90 | } 91 | lp.endToEndOf != -1 -> { 92 | val r = if (lp.endToEndOf == parentId) right else childMap.get(lp.endToEndOf)?.right ?: 0 93 | (r - lp.marginEnd) - child.measuredWidth 94 | } 95 | else -> 0 96 | } 97 | } 98 | 99 | override fun onViewAdded(child: View?) { 100 | super.onViewAdded(child) 101 | child?.let { childMap.put(it.id, it) } 102 | } 103 | 104 | override fun onViewRemoved(child: View?) { 105 | super.onViewRemoved(child) 106 | child?.let { childMap.remove(it.id) } 107 | } 108 | 109 | class LayoutParam(source: ViewGroup.LayoutParams?) : MarginLayoutParams(source) { 110 | var leftPercent: Float = -1f 111 | var topPercent: Float = -1f 112 | var startToStartOf: Int = -1 113 | var startToEndOf: Int = -1 114 | var endToEndOf: Int = -1 115 | var endToStartOf: Int = -1 116 | var topToTopOf: Int = -1 117 | var topToBottomOf: Int = -1 118 | var bottomToTopOf: Int = -1 119 | var bottomToBottomOf: Int = -1 120 | var centerHorizontalOf: Int = -1 121 | var centerVerticalOf: Int = -1 122 | } 123 | 124 | 125 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.views 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.* 6 | import android.util.AttributeSet 7 | import android.view.View 8 | 9 | /** 10 | * a linear progress bar 11 | */ 12 | class ProgressBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 13 | View(context, attrs, defStyleAttr) { 14 | var progressBackgroundColor: Int = Color.parseColor("#00ff00") 15 | set(value) { 16 | field = value 17 | backgroundPaint.color = value 18 | } 19 | var progressForegroundColor: Int = Color.parseColor("#ff00ff") 20 | set(value) { 21 | field = value 22 | foreGroundPaint.color = value 23 | } 24 | 25 | var colors = intArrayOf() 26 | 27 | var backgroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 28 | color = progressBackgroundColor 29 | style = Paint.Style.FILL 30 | } 31 | 32 | var foreGroundPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 33 | color = progressForegroundColor 34 | style = Paint.Style.FILL 35 | } 36 | 37 | var foregroundRectF = RectF() 38 | var backgroundRectF = RectF() 39 | 40 | var rx: Float = 0f 41 | var ry: Float = 0f 42 | 43 | var progress: Float = 0f 44 | 45 | @SuppressLint("DrawAllocation") 46 | override fun onDraw(canvas: Canvas?) { 47 | foreGroundPaint.shader = 48 | if (colors.isEmpty()) null else LinearGradient(0f, 0f, width * progress, height.toFloat(), colors, null, Shader.TileMode.CLAMP) 49 | backgroundRectF.set(0f, 0f, width.toFloat(), height.toFloat()) 50 | canvas?.drawRoundRect(backgroundRectF, rx, ry, backgroundPaint) 51 | foregroundRectF.set(0f, 0f, width * progress, height.toFloat()) 52 | canvas?.drawRoundRect(foregroundRectF, rx, ry, foreGroundPaint) 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/Selector.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 7 | import android.widget.FrameLayout 8 | 9 | open class Selector @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 10 | FrameLayout(context, attrs, defStyleAttr) { 11 | /** 12 | * the unique identifier for a [Selector] 13 | */ 14 | var tag: String = "default tag" 15 | 16 | /** 17 | * the identifier for the [SelectorGroup] this [Selector] belongs to 18 | */ 19 | var groupTag: String = "default group tag" 20 | 21 | /** 22 | * the [SelectorGroup] this [Selector] belongs to 23 | */ 24 | var group: SelectorGroup? = null 25 | 26 | /** 27 | * the layout view for this [Selector] 28 | */ 29 | var contentView: View? = null 30 | set(value) { 31 | field = value 32 | value?.let { 33 | addView(it, LayoutParams(MATCH_PARENT, MATCH_PARENT)) 34 | setOnClickListener { 35 | group?.onSelectorClick(this) 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * it will be invoked when the selection state of this [Selector] has changed, 42 | * override it if you want customized effect of selected or unselected 43 | */ 44 | var onStateChange: ((Selector, Boolean) -> Unit)? = null 45 | 46 | init { 47 | contentView?.let { 48 | addView(it, LayoutParams(MATCH_PARENT, MATCH_PARENT)) 49 | setOnClickListener { 50 | group?.onSelectorClick(this) 51 | } 52 | } 53 | } 54 | 55 | override fun setSelected(selected: Boolean) { 56 | val isPreSelected = isSelected 57 | super.setSelected(selected) 58 | if (isPreSelected != selected) { 59 | onStateChange?.invoke(this, selected) 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/java/taylor/com/views/SelectorGroup.kt: -------------------------------------------------------------------------------- 1 | package taylor.com.views 2 | 3 | 4 | class SelectorGroup { 5 | 6 | companion object { 7 | /** 8 | * single choice mode, previous [Selector] will be unselected if a new one is selected 9 | */ 10 | var MODE_SINGLE = { selectorGroup: SelectorGroup, selector: Selector -> 11 | selectorGroup.run { 12 | find(selector.groupTag)?.let { setSelected(it, false) } 13 | setSelected(selector, true) 14 | } 15 | } 16 | 17 | /** 18 | * multiple choice mode, several [Selector] could be selected in one [SelectorGroup] 19 | */ 20 | var MODE_MULTIPLE = { selectorGroup: SelectorGroup, selector: Selector -> 21 | selectorGroup.setSelected(selector, !selector.isSelected) 22 | } 23 | } 24 | 25 | /** 26 | * the selected [Selector]s in this [SelectorGroup] 27 | */ 28 | private var selectors = mutableListOf() 29 | 30 | /** 31 | * the choice mode of this [SelectorGroup], there are two default choice mode, which is [singleMode] and [multipleMode] 32 | */ 33 | var choiceMode: ((SelectorGroup, Selector) -> Unit)? = null 34 | 35 | 36 | var selectChangeListener: ((List, Boolean) -> Unit)? = null 37 | 38 | fun onSelectorClick(selector: Selector) { 39 | choiceMode?.invoke(this, selector) 40 | } 41 | 42 | fun find(groupTag: String) = selectors.find { it.groupTag == groupTag } 43 | 44 | fun setSelected(selector: Selector, select: Boolean) { 45 | if (select) selectors.add(selector) else selectors.remove(selector) 46 | selector.isSelected = select 47 | if (select) { 48 | selectChangeListener?.invoke(selectors, select) 49 | } else { 50 | selectChangeListener?.invoke(listOf(selector), select) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_orange_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/diamond_tag.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/drawable/diamond_tag.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_black.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/drawable/ic_back_black.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_member_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/drawable/ic_member_more.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/tag_checked_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/user_portrait_gender_female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/drawable/user_portrait_gender_female.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wisdomtl/Layout_DSL/8e48f4a011e91ad475a3889b2000ffbc9b4d8232/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Layout_dsl 3 | Settings 4 | 5 | First Fragment 6 | Second Fragment 7 | Next 8 | Previous 9 | 10 | Hello first fragment 11 | Hello second fragment. Arg: %1$s 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 21 | 22 |