Skip to content

Commit b20f690

Browse files
committed
v1.4 Support for layout inspector in xml files
1 parent 3d6df7e commit b20f690

File tree

7 files changed

+267
-2
lines changed

7 files changed

+267
-2
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.maf.custom.views.customgradientbutton
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
8+
import com.maf.custom.views.customgradientbutton.databinding.BottomSheetLayoutBinding
9+
10+
class BottomSheet : BottomSheetDialogFragment() {
11+
12+
lateinit var binding: BottomSheetLayoutBinding
13+
14+
override fun onCreateView(
15+
inflater: LayoutInflater,
16+
container: ViewGroup?,
17+
savedInstanceState: Bundle?
18+
): View {
19+
binding = BottomSheetLayoutBinding.inflate(inflater)
20+
return binding.root
21+
}
22+
23+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
24+
super.onViewCreated(view, savedInstanceState)
25+
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
package com.maf.custom.views.customgradientbutton
22

33
import android.os.Bundle
4+
import android.view.LayoutInflater
45
import androidx.activity.viewModels
56
import androidx.appcompat.app.AppCompatActivity
7+
import androidx.compose.ui.ExperimentalComposeUiApi
8+
import androidx.compose.ui.platform.ComposeView
9+
import androidx.compose.ui.platform.createLifecycleAwareWindowRecomposer
10+
import androidx.lifecycle.ViewTreeLifecycleOwner
11+
import com.google.android.material.bottomsheet.BottomSheetDialog
612
import com.maf.custom.views.customgradientbutton.databinding.ActivityMainBinding
13+
import com.maf.custom.views.customgradientbutton.databinding.BottomSheetLayoutBinding
714
import dagger.hilt.android.AndroidEntryPoint
15+
import kotlinx.coroutines.Dispatchers
816

917
@AndroidEntryPoint
1018
class MainActivity : AppCompatActivity() {
1119

1220
private lateinit var binding: ActivityMainBinding
1321
private val mainViewModel: MainViewModel by viewModels()
1422

23+
@OptIn(ExperimentalComposeUiApi::class)
1524
override fun onCreate(savedInstanceState: Bundle?) {
1625
super.onCreate(savedInstanceState)
1726
binding = ActivityMainBinding.inflate(layoutInflater)
27+
ViewTreeLifecycleOwner.set(window.decorView, this)
1828
setContentView(binding.root)
1929

2030
binding.main = mainViewModel
2131
binding.lifecycleOwner = this
32+
33+
binding.customButtonHv.setOnDebounceClickListener {
34+
BottomSheet().show(this.supportFragmentManager, "")
35+
}
36+
37+
2238
}
2339
}

app/src/main/res/layout/activity_main.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
app:rippleColor="#2196F3"
3838
app:buttonBorderWidth="2"
3939
app:buttonTextSize="18"
40+
android:layout_marginHorizontal="10dp"
4041
app:disabledButtonBackground="#26B5064D"
4142
app:fontWidth="semiBold"
4243
app:iconsArrangement="spaceBetween"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<layout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
xmlns:tools="http://schemas.android.com/tools">
5+
6+
<data>
7+
8+
</data>
9+
10+
<LinearLayout
11+
android:layout_width="match_parent"
12+
android:layout_height="match_parent"
13+
android:background="@color/white"
14+
android:orientation="vertical"
15+
tools:context=".MainActivity">
16+
17+
<androidx.appcompat.widget.AppCompatTextView
18+
android:id="@+id/text"
19+
android:layout_width="wrap_content"
20+
android:layout_height="wrap_content"
21+
android:layout_gravity="center_horizontal"
22+
android:layout_marginTop="50dp"
23+
android:text="Basic"
24+
android:textColor="#fff"
25+
android:textSize="25sp" />
26+
27+
<com.maf.custom.views.gradient_button.CustomGradientButton
28+
android:id="@+id/custom_button"
29+
android:layout_width="match_parent"
30+
android:layout_height="wrap_content"
31+
android:layout_marginTop="20dp"
32+
app:buttonBackground="#B5064D"
33+
app:rippleColor="#2196F3"
34+
app:buttonBorderWidth="2"
35+
app:buttonTextSize="18"
36+
app:disabledButtonBackground="#26B5064D"
37+
app:fontWidth="semiBold"
38+
app:iconsArrangement="spaceBetween"
39+
app:innerHorizontalPadding="20"
40+
app:innerVerticalPadding="15"
41+
app:isEnabled="true"
42+
app:layout_constraintBottom_toBottomOf="parent"
43+
app:layout_constraintEnd_toEndOf="parent"
44+
app:layout_constraintStart_toStartOf="parent"
45+
app:layout_constraintTop_toTopOf="parent"
46+
app:roundedBoarder="50"
47+
app:textColor="#fff"
48+
app:textTitle="Enabled" />
49+
50+
<com.maf.custom.views.gradient_button.CustomGradientButton
51+
android:id="@+id/custom_button_disabled"
52+
android:layout_width="match_parent"
53+
android:layout_height="wrap_content"
54+
android:layout_marginHorizontal="10dp"
55+
android:layout_marginTop="20dp"
56+
app:buttonBackground="#B5064D"
57+
app:buttonBorderWidth="2"
58+
app:buttonTextSize="18"
59+
app:disabledButtonBackground="#80B5064D"
60+
app:disabledButtonBackgroundAlpha="0.5"
61+
app:disabledTextAlpha="0.9"
62+
app:font="@font/fjalla_one_regular"
63+
app:fontWidth="semiBold"
64+
app:iconsArrangement="spaceBetween"
65+
app:innerHorizontalPadding="20"
66+
app:innerVerticalPadding="15"
67+
app:isEnabled="false"
68+
app:layout_constraintBottom_toBottomOf="parent"
69+
app:layout_constraintEnd_toEndOf="parent"
70+
app:layout_constraintStart_toStartOf="parent"
71+
app:layout_constraintTop_toTopOf="parent"
72+
app:roundedBoarder="50"
73+
app:textColor="#fff"
74+
app:textTitle="Disabled" />
75+
76+
</LinearLayout>
77+
</layout>

gradient-button/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ afterEvaluate {
7474

7575
groupId = 'com.maf.custom.views'
7676
artifactId = 'gradient-button'
77-
version = '1.3'
77+
version = '1.4'
7878
}
7979
}
8080
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.maf.custom.views.gradient_button
2+
3+
import android.os.Bundle
4+
import android.view.View
5+
import androidx.compose.runtime.MonotonicFrameClock
6+
import androidx.compose.runtime.PausableMonotonicFrameClock
7+
import androidx.compose.runtime.Recomposer
8+
import androidx.compose.ui.InternalComposeUiApi
9+
import androidx.compose.ui.platform.AbstractComposeView
10+
import androidx.compose.ui.platform.AndroidUiDispatcher
11+
import androidx.compose.ui.platform.compositionContext
12+
import androidx.lifecycle.*
13+
import androidx.savedstate.SavedStateRegistry
14+
import androidx.savedstate.SavedStateRegistryController
15+
import androidx.savedstate.SavedStateRegistryOwner
16+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
17+
import kotlinx.coroutines.*
18+
import kotlinx.coroutines.android.asCoroutineDispatcher
19+
20+
private class ComposeLayoutPreviewHelper(val view: AbstractComposeView) {
21+
22+
private val fakeSavedStateRegistryOwner = object : SavedStateRegistryOwner {
23+
private val lifecycle = LifecycleRegistry(this)
24+
private val controller = SavedStateRegistryController.create(this).apply {
25+
performRestore(Bundle())
26+
}
27+
28+
init {
29+
// Starts the recomposition.
30+
lifecycle.currentState = Lifecycle.State.RESUMED
31+
}
32+
33+
override val savedStateRegistry: SavedStateRegistry
34+
get() = controller.savedStateRegistry
35+
36+
override fun getLifecycle(): Lifecycle = lifecycle
37+
}
38+
39+
private val fakeViewModelStoreOwner = object : ViewModelStoreOwner {
40+
private val viewModelStore = ViewModelStore()
41+
42+
override fun getViewModelStore() = viewModelStore
43+
}
44+
45+
init {
46+
val stateRegistryOwner = fakeSavedStateRegistryOwner
47+
val viewModelStoreOwner = fakeViewModelStoreOwner
48+
ViewTreeLifecycleOwner.set(view, stateRegistryOwner)
49+
view.setViewTreeSavedStateRegistryOwner(stateRegistryOwner)
50+
ViewTreeViewModelStoreOwner.set(view, viewModelStoreOwner)
51+
}
52+
53+
@OptIn(DelicateCoroutinesApi::class, InternalComposeUiApi::class)
54+
fun createAndInstallWindowRecomposer(
55+
rootView: View = view,
56+
lifecycleOwner: LifecycleOwner = fakeSavedStateRegistryOwner
57+
): Recomposer {
58+
val newRecomposer = createViewTreeRecomposer(lifecycleOwner = lifecycleOwner)
59+
rootView.compositionContext = newRecomposer
60+
61+
// If the Recomposer shuts down, unregister it so that a future request for a window
62+
// recomposer will consult the factory for a new one.
63+
val unsetJob = GlobalScope.launch(
64+
rootView.handler.asCoroutineDispatcher("windowRecomposer cleanup").immediate
65+
) {
66+
try {
67+
newRecomposer.join()
68+
} finally {
69+
// Unset if the view is detached. (See below for the attach state change listener.)
70+
// Since this is in a finally in this coroutine, even if this job is cancelled we
71+
// will resume on the window's UI thread and perform this manipulation there.
72+
val viewTagRecomposer = rootView.compositionContext
73+
if (viewTagRecomposer === newRecomposer) {
74+
rootView.compositionContext = null
75+
}
76+
}
77+
}
78+
79+
// If the root view is detached, cancel the await for recomposer shutdown above.
80+
// This will also unset the tag reference to this recomposer during its cleanup.
81+
rootView.addOnAttachStateChangeListener(
82+
object : View.OnAttachStateChangeListener {
83+
override fun onViewAttachedToWindow(v: View) {}
84+
override fun onViewDetachedFromWindow(v: View) {
85+
v.removeOnAttachStateChangeListener(this)
86+
// cancel the job to clean up the view tags.
87+
// this will happen immediately since unsetJob is on an immediate dispatcher
88+
// for this view's UI thread instead of waiting for the recomposer to join.
89+
// NOTE: This does NOT cancel the returned recomposer itself, as it may be
90+
// a shared-instance recomposer that should remain running/is reused elsewhere.
91+
unsetJob.cancel()
92+
}
93+
}
94+
)
95+
return newRecomposer
96+
}
97+
98+
private fun createViewTreeRecomposer(
99+
lifecycleOwner: LifecycleOwner
100+
): Recomposer {
101+
val currentThreadContext = AndroidUiDispatcher.CurrentThread
102+
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
103+
PausableMonotonicFrameClock(it).apply { pause() }
104+
}
105+
val contextWithClock = currentThreadContext + (pausableClock ?: Dispatchers.Unconfined)
106+
val recomposer = Recomposer(contextWithClock)
107+
val runRecomposeScope = CoroutineScope(contextWithClock)
108+
109+
lifecycleOwner.lifecycle.addObserver(
110+
LifecycleEventObserver { _, event ->
111+
when (event) {
112+
Lifecycle.Event.ON_CREATE ->
113+
// Undispatched launch since we've configured this scope
114+
// to be on the UI thread
115+
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
116+
recomposer.runRecomposeAndApplyChanges()
117+
}
118+
Lifecycle.Event.ON_START -> pausableClock?.resume()
119+
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
120+
Lifecycle.Event.ON_DESTROY -> {
121+
recomposer.cancel()
122+
}
123+
Lifecycle.Event.ON_RESUME,
124+
Lifecycle.Event.ON_PAUSE,
125+
Lifecycle.Event.ON_ANY -> {
126+
// Do nothing.
127+
}
128+
}
129+
}
130+
)
131+
132+
return recomposer
133+
}
134+
}
135+
136+
fun AbstractComposeView.setupEditMode() {
137+
ComposeLayoutPreviewHelper(this).createAndInstallWindowRecomposer()
138+
}

gradient-button/src/main/java/com/maf/custom/views/gradient_button/CustomGradientButton.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import androidx.compose.ui.unit.TextUnit
3737
import androidx.compose.ui.unit.dp
3838
import androidx.compose.ui.unit.sp
3939
import androidx.core.content.withStyledAttributes
40+
import androidx.lifecycle.ViewTreeLifecycleOwner
41+
import androidx.lifecycle.ViewTreeViewModelStoreOwner
42+
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
43+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
4044

4145
class CustomGradientButton @JvmOverloads constructor(
4246
context: Context,
@@ -106,7 +110,9 @@ class CustomGradientButton @JvmOverloads constructor(
106110
}
107111

108112
override fun onAttachedToWindow() {
109-
setParentCompositionContext(null)
113+
if (isInEditMode) {
114+
setupEditMode()
115+
}
110116
super.onAttachedToWindow()
111117
}
112118

0 commit comments

Comments
 (0)