Skip to content

Commit 9d93191

Browse files
authored
Add some extension functions (re-open) (#11)
* Add view extension functions (#2, #3) * Visibility * Keyobard * Dimension * Add debounce click extension functions to prevent multiple click events * Add ViewGroup extension function * Visibility * Add EditText extension functions * Text changed TextWatcher * Validator watcher Show/hide password * Add resource getter extension functions * Color * Drawable * Add TextView extension functions * Set text along with existence/visibility setting * Add doc comment to extension functions * Add extension function doc and update README to add link to extension doc
2 parents 7f21bb4 + 6c01bde commit 9d93191

7 files changed

Lines changed: 371 additions & 0 deletions

File tree

EXTENSIONS.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Useful Extension Functions
2+
## View
3+
```kotlin
4+
// Prevent multiple/duplicate click
5+
View.debounceClick() { ... }
6+
7+
// Visibility
8+
View.visible()
9+
View.invisible()
10+
View.gone()
11+
View.showIf { ... }
12+
View.hideIf { ... }
13+
View.goneIf { ... }
14+
15+
// Keyboard
16+
View.showKeyboard()
17+
View.hideKeyboard()
18+
19+
// Dimension
20+
View.px2dp()
21+
View.dp2px()
22+
View.px2sp()
23+
View.sp2px()
24+
25+
// Resources
26+
View.getColor(R.color.colorPrimary)
27+
View.getDrawable(R.drawable.ic_launcher)
28+
```
29+
30+
## EditText
31+
```kotlin
32+
EditText.textChanged { s -> ... }
33+
EditText.textChanged().doOnNext { s -> ... }.subscribe()
34+
EditText.validate(errorMessage = "The name should not be empty") { !it.isNullOrBlank() }
35+
EditText.validateEmail { ... }
36+
EditText.validateEmail("It's not valid email")
37+
EditText.showPassword()
38+
EditText.hidePassword()
39+
```
40+
41+
## TextView
42+
```kotlin
43+
// Set view gone while text is null or empty.
44+
TextView.setTextWithExistence("1234")
45+
46+
// Set view invislble while text is null or empty.
47+
TextView.setTextWithVisibility("5678")
48+
```
49+
50+
## String
51+
```kotlin
52+
String?.isValidEmail(): Boolean
53+
```
54+

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ fun Project.importCommonDependencies() {
147147
* [Timber](https://github.com/JakeWharton/timber), for logging.
148148
* [Epoxy](https://github.com/airbnb/epoxy), for RecyclerView complex view layout.
149149

150+
## Useful Extensions
151+
* See [Extension Functions.](./EXTENSIONS.md)
152+
150153
## How to Update
151154
Keep this repository as one of your project tracked remote.
152155

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.enginebai.base.extensions
2+
3+
import android.text.Editable
4+
import android.text.InputFilter
5+
import android.text.TextWatcher
6+
import android.text.method.HideReturnsTransformationMethod
7+
import android.text.method.PasswordTransformationMethod
8+
import android.widget.EditText
9+
import io.reactivex.Observable
10+
import io.reactivex.Single
11+
import io.reactivex.subjects.PublishSubject
12+
13+
/**
14+
* Invoke the listener when afterTextChanged() method is called.
15+
*
16+
* @return TextWatcher remember to remove when no longer use it anymore.
17+
*/
18+
fun EditText.textChanged(listener: (String) -> Unit): TextWatcher {
19+
val textWatcher = object : TextWatcher {
20+
override fun afterTextChanged(s: Editable?) {
21+
listener.invoke(s?.toString() ?: "")
22+
}
23+
24+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
25+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
26+
}
27+
this.addTextChangedListener(textWatcher)
28+
return textWatcher
29+
}
30+
31+
fun EditText.textChanged(): Observable<out CharSequence> {
32+
val source: PublishSubject<String> = PublishSubject.create()
33+
val textWatcher = TextChangeWatcher(source)
34+
addTextChangedListener(textWatcher)
35+
return source.doOnDispose {
36+
textWatcher.unsubscribe()
37+
removeTextChangedListener(textWatcher)
38+
}
39+
}
40+
41+
private class TextChangeWatcher(private var publisher: PublishSubject<String>? = null) :
42+
TextWatcher {
43+
44+
override fun afterTextChanged(s: Editable?) {
45+
publisher?.onNext(s?.toString() ?: "")
46+
}
47+
48+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
49+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
50+
51+
fun unsubscribe() {
52+
publisher = null
53+
}
54+
}
55+
56+
/**
57+
* Validate the input text and display error when it's invalid.
58+
* @param [errorMessage] error message to display
59+
* @param [validator] the predicate to validate input.
60+
* @return TextWatcher remember to remove when no longer use it anymore.
61+
*/
62+
fun EditText.validate(errorMessage: String, validator: (String) -> Boolean): TextWatcher {
63+
fun showErrorIfInvalid(s: String) {
64+
this.error = if (validator(s)) null else errorMessage
65+
}
66+
// validate original text
67+
showErrorIfInvalid(this.text.toString())
68+
// validate changed text
69+
return this.textChanged { showErrorIfInvalid(it) }
70+
}
71+
72+
/**
73+
* Validate if input is valid email, and invoke either valid or invalid listener.
74+
* @return TextWatcher remember to remove when no longer use it anymore.
75+
*/
76+
fun EditText.validateEmail(validListener: (String) -> Unit, invalidListener: (String) -> Unit = {}): TextWatcher {
77+
return this.textChanged {
78+
if (it.isValidEmail()) validListener(it)
79+
else invalidListener(it)
80+
}
81+
}
82+
83+
/**
84+
* Validate if input is valid email, and display error if invalid. * @return TextWatcher remember to remove when no longer use it anymore.
85+
* @return TextWatcher remember to remove when no longer use it anymore.
86+
*/
87+
fun EditText.validateEmail(errorMessage: String): TextWatcher {
88+
return validate(errorMessage) { it.isValidEmail() }
89+
}
90+
91+
/**
92+
* Validate if input is valid email and return result as stream.
93+
*/
94+
fun EditText.validateEmail(): Single<Boolean> {
95+
return Single.fromObservable(this.textChanged()).flatMap {
96+
Single.just(it.toString().isValidEmail())
97+
}
98+
}
99+
100+
fun EditText.showPassword() {
101+
transformationMethod = HideReturnsTransformationMethod.getInstance()
102+
selectAll()
103+
}
104+
105+
fun EditText.hidePassword() {
106+
transformationMethod = PasswordTransformationMethod.getInstance()
107+
selectAll()
108+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.enginebai.base.extensions
2+
3+
import android.util.Patterns
4+
import androidx.core.util.PatternsCompat
5+
6+
fun String?.isValidEmail(): Boolean {
7+
return PatternsCompat.EMAIL_ADDRESS.matcher(this ?: "").matches()
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.enginebai.base.extensions
2+
3+
import android.view.View
4+
import android.widget.TextView
5+
6+
/**
7+
* Set view visible when text is not null or blank, else set view gone.
8+
*/
9+
fun TextView.setTextWithExistence(s: String?) {
10+
s?.let { this.text = it }
11+
setExistence(!s.isNullOrBlank())
12+
}
13+
14+
/**
15+
* Set view visible when text is not null or blank, else set view invisible.
16+
*/
17+
fun TextView.setTextWithVisibility(s: String?) {
18+
s?.let { this.text = it }
19+
setVisible(!s.isNullOrBlank())
20+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.enginebai.base.extensions
2+
3+
import android.content.Context
4+
import android.graphics.drawable.Drawable
5+
import android.view.View
6+
import android.view.inputmethod.InputMethodManager
7+
import androidx.annotation.ColorRes
8+
import androidx.annotation.DrawableRes
9+
import androidx.core.content.ContextCompat
10+
11+
/**
12+
* Prevent multiple click in a short period of time. Default interval is 1500 milli-second.
13+
* @param intervalInMillis: The time interval to trigger next click events.
14+
* @param listener: The click listener
15+
*/
16+
inline fun View.debounceClick(intervalInMillis: Int = 1500, crossinline listener: (view: View) -> Unit) {
17+
var lastClick = 0L
18+
setOnClickListener {
19+
val diff = System.currentTimeMillis() - lastClick
20+
lastClick = System.currentTimeMillis()
21+
if (diff > intervalInMillis) {
22+
listener(it)
23+
}
24+
}
25+
}
26+
27+
//
28+
// visibility
29+
//
30+
/**
31+
* Set visible or invisible of view.
32+
*/
33+
fun View.setVisible(visible: Boolean) {
34+
visibility = if (visible) View.VISIBLE else View.INVISIBLE
35+
}
36+
37+
/**
38+
* Set visible or gone of view.
39+
*/
40+
fun View.setExistence(exist: Boolean) {
41+
visibility = if (exist) View.VISIBLE else View.GONE
42+
}
43+
44+
/**
45+
* Set view visible when matching the given [predicate]
46+
*/
47+
inline fun View.showIf(predicate: () -> Boolean): View {
48+
if (visibility != View.VISIBLE && predicate()) {
49+
visibility = View.VISIBLE
50+
}
51+
return this
52+
}
53+
54+
/**
55+
* Set view invisible when matching the given [predicate]
56+
*/
57+
inline fun View.hideIf(predicate: () -> Boolean): View {
58+
if (visibility != View.INVISIBLE && predicate()) {
59+
visibility = View.INVISIBLE
60+
}
61+
return this
62+
}
63+
64+
/**
65+
* Set view gone when matching the given [predicate]
66+
*/
67+
inline fun View.goneIf(predicate: () -> Boolean): View {
68+
if (visibility != View.GONE && predicate()) {
69+
visibility = View.GONE
70+
}
71+
return this
72+
}
73+
74+
fun View.gone() {
75+
setExistence(false)
76+
}
77+
78+
fun View.invisible() {
79+
setVisible(false)
80+
}
81+
82+
fun View.visible() {
83+
visibility = View.VISIBLE
84+
}
85+
86+
fun View.toggleVisible() {
87+
visibility = if (visibility == View.VISIBLE) View.INVISIBLE else View.VISIBLE
88+
}
89+
90+
fun View.toggleExistence() {
91+
visibility = if (visibility == View.VISIBLE) View.GONE else View.VISIBLE
92+
}
93+
94+
val View.isGone: Boolean get() = (visibility == View.GONE)
95+
val View.isInvisible: Boolean get() = (visibility == View.INVISIBLE)
96+
val View.isVisible: Boolean get() = (visibility == View.VISIBLE)
97+
98+
//
99+
// keyboard
100+
//
101+
fun View.showKeyboard() {
102+
val inputMethodManager =
103+
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
104+
inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_FORCED)
105+
}
106+
107+
fun View.hideKeyboard() {
108+
val inputMethodManager =
109+
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
110+
inputMethodManager.hideSoftInputFromWindow(this.windowToken, 0)
111+
}
112+
113+
//
114+
// dimension conversion
115+
//
116+
fun View.px2dp(px: Float): Int {
117+
return (px / resources.displayMetrics.density + 0.5f).toInt()
118+
}
119+
120+
fun View.dp2px(dp: Float): Int {
121+
return (dp * resources.displayMetrics.density + 0.5f).toInt()
122+
}
123+
124+
fun View.px2sp(px: Float): Int {
125+
return (px / resources.displayMetrics.scaledDensity + 0.5f).toInt()
126+
}
127+
128+
fun View.sp2px(sp: Float): Int {
129+
return (sp * resources.displayMetrics.scaledDensity + 0.5f).toInt()
130+
}
131+
132+
//
133+
// resources
134+
//
135+
fun View.getColor(@ColorRes resId: Int) = ContextCompat.getColor(context, resId)
136+
fun View.getDrawable(@DrawableRes resId: Int) = ContextCompat.getDrawable(context, resId)
137+
fun View.getDrawableWithIntrinsicSize(@DrawableRes resId: Int): Drawable? {
138+
return getDrawable(resId)?.apply {
139+
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
140+
}
141+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.enginebai.base.extensions
2+
3+
import android.view.View
4+
import android.view.ViewGroup
5+
6+
fun ViewGroup.setVisible(visible: Boolean) {
7+
visibility = if (visible) View.VISIBLE else View.INVISIBLE
8+
}
9+
10+
fun ViewGroup.setExistence(exist: Boolean) {
11+
visibility = if (exist) View.VISIBLE else View.GONE
12+
}
13+
14+
fun ViewGroup.gone() {
15+
setExistence(false)
16+
}
17+
18+
fun ViewGroup.invisible() {
19+
setVisible(false)
20+
}
21+
22+
fun ViewGroup.visible() {
23+
visibility = View.VISIBLE
24+
}
25+
26+
fun ViewGroup.toggleVisible() {
27+
visibility = if (visibility == View.VISIBLE) View.INVISIBLE else View.VISIBLE
28+
}
29+
30+
fun ViewGroup.toggleExistence() {
31+
visibility = if (visibility == View.VISIBLE) View.GONE else View.VISIBLE
32+
}
33+
34+
val ViewGroup.isGone: Boolean get() = (visibility == View.GONE)
35+
val ViewGroup.isInvisible: Boolean get() = (visibility == View.INVISIBLE)
36+
val ViewGroup.isVisible: Boolean get() = (visibility == View.VISIBLE)
37+

0 commit comments

Comments
 (0)