TextClock is a widget for Android XML layouts which keeps and displays time on it's own. You just have to add the format and timezone.
Right now I don't see an equivalent in Jetpack Compose.
Should I implement it by my own with a Text composable and some time formatting libraries? Should I inflate a TextClock and make use of the backwards compatibility? Or is there a ready to use component?
I started with using the original TextView by adding it via the AndroidView composable. It does work but I would still appreciate an answer without "legacy" code.
#Composable
private fun TextClock(
modifier: Modifier = Modifier,
format12Hour: String? = null,
format24Hour: String? = null,
timeZone: String? = null,
) {
AndroidView(
factory = { context ->
TextClock(context).apply {
format12Hour?.let { this.format12Hour = it }
format24Hour?.let { this.format24Hour = it }
timeZone?.let { this.timeZone = it }
}
},
modifier = modifier
)
}
Here is my solution, which made styling possible.
#Composable
fun ClockText() {
val currentTimeMillis = remember {
mutableStateOf(System.currentTimeMillis())
}
LaunchedEffect(key1 = currentTimeMillis) {
while (true) {
delay(250)
currentTimeMillis.value = System.currentTimeMillis()
}
}
Box() {
Text(
text = DateUtils.formatDateTime(LocalContext.current, currentTimeMillis.value, DateUtils.FORMAT_SHOW_TIME),
modifier = Modifier.padding(8.dp, 8.dp),
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.subtitle2
)
}
}
https://gist.github.com/dino-su/c8edf1c206dd974b282326f3b9641ccc
TextClock class exteds TextView class and TextView class extends View class.
However Jetpack Compose is not based on 'Views'.
So I tried to find ways to use Views in Compose and I cound find this link.
https://developer.android.com/jetpack/compose/interop/interop-apis#views-in-compose
The link explains about AndroidView composable.
My sample code is as below and Merry Christmas!
#Composable
fun MyTextClock() {
AndroidView(factory = { context ->
TextClock(context).apply {
format12Hour?.let {
this.format12Hour = "a hh: mm: ss"
}
timeZone?.let { this.timeZone = it }
textSize.let { this.textSize = 24f }
}
})
}
#Preview(showBackground = true)
#Composable
fun MyTextClockPreview() {
MyTextClock()
}
Related
we've encountered a problem with Jetpack Compose button actions leaking. We provide a class with resources (e.g strings, colors, and BUTTON ACTIONS):
data class CameraOnBoardingComposeViewResources(val title: String, val buttonTitle: String, val buttonAction: () -> Unit)
This data class is created in the fragment like so:
val resources = CameraOnBoardingComposeViewResources(
"Title", "ButtonTitle"
) { navigate() }
And in our composable we use those resources like so:
#Composable
fun composeOnBoardingView(resources: CameraOnBoardingComposeViewResources) {
Surface(
shape = RoundedCornerShape(4.dp),
color = colorResource(R.color.idenfyMainColorV2),
modifier = Modifier
.height(42.dp)
.padding(start = 16.dp, end = 16.dp)
.clickable(onClick = resources.buttonAction)
.fillMaxWidth()
)
}
The problem is that buttonAction leaks. How can we avoid this leak, should we somehow dispose of it, or do we need to change this architecture a bit?
EDIT After CommonsWare
Ok, I’ve located the problem, and it is indeed inside the composeOnBoardingView composable. It’s this code snippet:
val currentInstructionDescription: MutableState<String> = remember {
mutableStateOf("")
}
val progress = remember { mutableStateOf(0.0f) }
val handler = Handler(Looper.getMainLooper())
exoPlayer?.apply {
val runnable = object : Runnable {
override fun run() {
progress.value =
exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
currentInstructionDescription.value = InstructionsDescriptionBuilder.build(
context,
(resources.documentCenterImageUiViewModel as OnBoardingCenterImageUIViewModel.DocumentResource).step,
exoPlayer.currentPosition.toInt() / 1000
)
handler.postDelayed(this, 1000)
}
}
handler.post(runnable)
}
I tried disposing the handler like this:
val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
onDispose {
handler.removeCallbacksAndMessages(null)
}
}
But that did not solve the problem. Any ideas?
EDIT
The issue is solved thanks CommonsWare! We switched to the LaunchedEffect and coroutines for an every-second in-composition timer, rather than Handler
I have a string that contains html, how can I display this in a Jetpack compose Text?
In a TextView I would use a Spanned and do something like:
TextView.setText(Html.fromHtml("<p>something", HtmlCompat.FROM_HTML_MODE_LEGACY)
How can I do this with Text from Jetpack compose?
Same answer as Yhondri, but using HtmlCompat if you are targeting api >24:
#Composable
fun Html(text: String) {
AndroidView(factory = { context ->
TextView(context).apply {
setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY))
}
})
}
I have done it this way instead of using TextView in AndroidView and it seems to work quite well for me. The below composable also wraps up the text and expands when you click on it.
#Composable
fun ExpandingText(
description: String,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.body2,
expandable: Boolean = true,
collapsedMaxLines: Int = 3,
expandedMaxLines: Int = Int.MAX_VALUE,
) {
val text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(description, Html.FROM_HTML_MODE_LEGACY)
} else {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
var canTextExpand by remember(text) { mutableStateOf(true) }
var expanded by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
Text(
text = text.toString(),
style = textStyle,
overflow = TextOverflow.Ellipsis,
maxLines = if (expanded) expandedMaxLines else collapsedMaxLines,
modifier = Modifier
.clickable(
enabled = expandable && canTextExpand,
onClick = { expanded = !expanded },
indication = rememberRipple(bounded = true),
interactionSource = interactionSource,
)
.animateContentSize(animationSpec = spring())
.then(modifier),
onTextLayout = {
if (!expanded) {
canTextExpand = it.hasVisualOverflow
}
}
)
}
Unfortunately, Jetpack compose does NOT support HTML yet...
So, what you could do is:
Option 1: Create your own HTML parser
Jetpack compose supports basic styling such as Bold, color, font etc.. So what you can do is loop through the original HTML text and apply text style manually.
Option 2: Integrate the old TextView into your Jetpack compose.
Please read: Adopting Compose in your app
Thanks.
You can integrate the old TextView into your Jetpack compose like follows:
AndroidView(factory = { context ->
TextView(context).apply {
text = Html.fromHtml(your_html)
}
})
More info: https://foso.github.io/Jetpack-Compose-Playground/viewinterop/androidview/
you can use the code below:
#Composable
private fun TextHtml() {
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Gray600)) {
append("normal text")
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold,color = Gray700)) {
append("bold text ")
}
})
}
use withStyle to apply the html tags and use append() inside it to add the string
when I use CompositionLocal, I have got the data from the parent and modify it, but I found it would not trigger the child recomposition.
I have successfully change the data, which can be proved through that when I add an extra state in the child composable then change it to trigger recomposition I can get the new data.
Is anybody can give me help?
Append
code like below
data class GlobalState(var count: Int = 0)
val LocalAppState = compositionLocalOf { GlobalState() }
#Composable
fun App() {
CompositionLocalProvider(LocalAppState provides GlobalState()) {
CountPage(globalState = LocalAppState.current)
}
}
#Composable
fun CountPage(globalState: GlobalState) {
// use it request recomposition worked
// val recomposeScope = currentRecomposeScope
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.clickable {
globalState.count++
// recomposeScope.invalidate()
}) {
Text("count ${globalState.count}")
}
}
I found a workaround is using currentRecomposable to force recomposition, maybe there is a better way and pls tell me.
The composition local is a red herring here. Since GlobalScope is not observable composition is not notified that it changed. The easiest change is to modify the definition of GlobalState to,
class GlobalState(count: Int) {
var count by mutableStateOf(count)
}
This will automatically notify compose that the value of count has changed.
I am not sure why you are using compositionLocalOf in this way.
Using the State hoisting pattern you can use two parameters in to the composable:
value: T: the current value to display.
onValueChange: (T) -> Unit: an event that requests the value to change where T is the proposed new value.
In your case:
data class GlobalState(var count: Int = 0)
#Composable
fun App() {
var counter by remember { mutableStateOf(GlobalState(0)) }
CountPage(
globalState = counter,
onUpdateCount = {
counter = counter.copy(count = counter.count +1)
}
)
}
#Composable
fun CountPage(globalState: GlobalState, onUpdateCount: () -> Unit) {
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.clickable (
onClick = onUpdateCount
)) {
Text("count ${globalState.count}")
}
}
You can declare your data as a MutableState and either provide separately the getter and the setter or just provide the MutableState object directly.
internal val LocalTest = compositionLocalOf<Boolean> { error("lalalalalala") }
internal val LocalSetTest = compositionLocalOf<(Boolean) -> Unit> { error("lalalalalala") }
#Composable
fun TestProvider(content: #Composable() () -> Unit) {
val (test, setTest) = remember { mutableStateOf(false) }
CompositionLocalProvider(
LocalTest provides test,
LocalSetTest provides setTest,
) {
content()
}
}
Inside a child component you can do:
#Composable
fun Child() {
val test = LocalTest.current
val setTest = LocalSetTest.current
Column {
Button(onClick = { setTest(!test) }) {
Text(test.toString())
}
}
}
Is there any way to use android:autoLink feature on JetPack Compose Text?
I know, that it is maybe not "declarative way" for using this feature in one simple tag/modifier, but maybe there is some easy way for this?
For styling text I can use this way
val apiString = AnnotatedString.Builder("API provided by")
apiString.pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
apiString.append("https://example.com")
Text(text = apiString.toAnnotatedString())
But, how can I manage clicks here? And would be great, if I programatically say what behaviour I expect from the system (email, phone, web, etc). Like it. works with TextView.
Thank you
We can achieve Linkify kind of TextView in Android Compose like this example below,
#Composable
fun LinkifySample() {
val uriHandler = UriHandlerAmbient.current
val layoutResult = remember {
mutableStateOf<TextLayoutResult?>(null)
}
val text = "API provided by"
val annotatedString = annotatedString {
pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
append(text)
addStringAnnotation(
tag = "URL",
annotation = "https://example.com",
start = 0,
end = text.length
)
}
Text(
fontSize = 16.sp,
text = annotatedString, modifier = Modifier.tapGestureFilter { offsetPosition ->
layoutResult.value?.let {
val position = it.getOffsetForPosition(offsetPosition)
annotatedString.getStringAnnotations(position, position).firstOrNull()
?.let { result ->
if (result.tag == "URL") {
uriHandler.openUri(result.item)
}
}
}
},
onTextLayout = { layoutResult.value = it }
)
}
In the above example, we can see we give the text and also we use addStringAnnotation to set the tag. And using tapGestureFilter, we can get the clicked annotation.
Finally using UriHandlerAmbient.current we can open the link like email, phone, or web.
Reference : https://www.hellsoft.se/rendering-markdown-with-jetpack-compose/
The most important part of jetpack compose is the compatibility with native android components.
Create a component that use TextView and use it:
#Composable
fun DefaultLinkifyText(modifier: Modifier = Modifier, text: String?) {
val context = LocalContext.current
val customLinkifyTextView = remember {
TextView(context)
}
AndroidView(modifier = modifier, factory = { customLinkifyTextView }) { textView ->
textView.text = text ?: ""
LinkifyCompat.addLinks(textView, Linkify.ALL)
Linkify.addLinks(textView, Patterns.PHONE,"tel:",
Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter)
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
How to use:
DefaultLinkifyText(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = "6999999 and https://stackoverflow.com/ works fine"
)
Let's suppose I'm using some library that's intended to provide some UI Widgets.
Let's say this library provides a Button Widget called FancyButton.
In the other hand, I have a new project created with Android Studio 4 that allows me to create a new project with an Empty Compose Activity.
The question is:
How should I add this FancyButton to the view stack? Is it possible? Or with Jetpack Compose I can only use components that had been developed specifically for Jetpack Compose. In this case, AFAIK I could only use Android standars components (Text, MaterialTheme, etc).
If I try to use something like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting("Android")
FancyButton(context, "Some text")
}
}
}
then I get this error:
e: Supertypes of the following classes cannot be resolved. Please make sure you have the required dependencies in the classpath.
Currently (as of 0.1.0-dev04), there is not a good solution to this. In the future, you'll be able to simply call it as FancyButton("Some text") (no context needed).
You can see a demo of what it will look like in the Compose code here.
Update in alpha 06
It is possible to import Android View instances in a composable.
Use ContextAmbient.current as the context parameter for the View.
Column(modifier = Modifier.padding(16.dp)) {
// CustomView using Object
MyCustomView(context = ContextAmbient.current)
// If the state updates
AndroidView(viewBlock = ::CustomView, modifier = modifier) { customView ->
// Modify the custom view
}
// Using xml resource
AndroidView(resId = R.layout.view_demo)
}
You can wrap your custom view within the AndroidView composable:
#Composable
fun RegularTextView() {
AndroidView(
factory = { context ->
TextView(context).apply {
text = "RegularTextView"
textSize = 34.dp.value
}
},
)
}
And here is how to update your custom view during a recomposition, by using the update parameter:
#Composable
fun RegularTextView() {
var string by remember {
mutableStateOf("RegularTextView")
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AndroidView(
factory = { context ->
TextView(context).apply {
textSize = 34.dp.value
}
},
update = { textView ->
textView.text = string
}
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
string = "Button clicked"
},
) {
Text(text = "Update text")
}
}
}
#Composable
fun ButtonType1(text: String, onClick: () -> Unit)
{
Button (
modifier=Modifier.fillMaxWidth().height(50.dp),
onClick = onClick,
shape = RoundedCornerShape(5.dp),
border = BorderStroke(3.dp, colorResource(id = R.color.colorPrimaryDark)),
colors = ButtonDefaults.buttonColors(contentColor = Color.White, backgroundColor = colorResource(id = R.color.colorPrimaryDark))
)
{
Text(text = text , color = colorResource(id = R.color.white),
fontFamily = montserrat,
fontWeight = FontWeight.Normal,
fontSize = 15.sp
)
}
}