This question already has an answer here:
Curved text Jetpack compose
(1 answer)
Closed 7 months ago.
I'm beginner in jetpack compose, especially in Canvas.
I want to create this with Canvas:
But I don't know how.
I could create curved Text but with fillMaxSize and it's not useful for me. Because I want to use this picture as a button in a page and I have some buttons like this one in one page. So I want to have this Canvas with wrapcontent size.
So how can I create that?
The answer mentioned in the comments above is mine. I did some adjustments in order to give you some direction. The code is listed below:
#Composable
fun ButtonWithCurvedText(
#DrawableRes icon: Int,
text: String,
iconSize: Dp,
onClick: () -> Unit,
) {
val density = LocalDensity.current
val imgSize = iconSize
val imageSizePx = with(density) { imgSize.toPx() }
val labelSize = 12.sp
val textToIconPadding = 4.dp
val textToIconPaddingPx = with(density) { textToIconPadding.toPx() }
val boxSize = imgSize + (textToIconPadding * 2) + with(density) { labelSize.roundToPx().toDp() }
val boxSizePx = with(density) { boxSize.toPx() }
val imageInset = ((boxSize - imgSize) / 2)
val imageInsetPx = with(density) { imageInset.toPx() }
val arcTop = imageInsetPx - (textToIconPaddingPx / 2f)
val arcLeft = imageInsetPx - (textToIconPaddingPx / 2f)
val arcBottom = boxSizePx - imageInsetPx + (textToIconPaddingPx / 2f)
val arcRight = boxSizePx - imageInsetPx + (textToIconPaddingPx / 2f)
val vectorPainter = rememberVectorPainter(
ImageVector.vectorResource(icon)
)
Canvas(
modifier = Modifier
.size(boxSize)
.background(Color.Gray)
.clickable {
onClick()
}
) {
drawCircle(Color.LightGray, radius = imageSizePx / 2f)
with(vectorPainter) {
inset(
horizontal = imageInsetPx,
vertical = imageInsetPx
) {
draw(Size(imageSizePx, imageSizePx))
}
}
drawIntoCanvas {
val path = Path().apply {
addArc(arcLeft, arcTop, arcRight, arcBottom, 270f, 180f)
}
it.nativeCanvas.drawTextOnPath(
text,
path,
0f,
0f,
Paint().apply {
textSize = labelSize.toPx()
textAlign = Paint.Align.CENTER
}
)
}
}
}
As you mentioned, you want something responsive. You just need to make some constants used here as parameters. The only "size" param required here is the icon size.
You can use like this:
Row() {
ButtonWithCurvedText(R.drawable.ic_share, "Share", 48.dp, {})
ButtonWithCurvedText(R.drawable.ic_cancel, "Cancel", 72.dp, {})
ButtonWithCurvedText(R.drawable.ic_check, "Check", 96.dp, {})
}
The result is like below:
Related
I am trying to split some words on two lines to create a sentence. When there is no more space on the first line, the words should automatically go to the second line, but no matter what I have tried so far, only the first line is used, while the second line remains empty all the time.
Here is a screen capture.
MainActivity:
class MainActivity : AppCompatActivity(), RemoveAnswerListener {
private var binding: ActivityMainBinding? = null
var listAnswers = mutableListOf<Answer>()
private lateinit var actualAnswer: List<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
actualAnswer = listOf(
"What",
"did",
"you",
"want",
"to",
"ask",
"me",
"yesterday?"
)
val answerOption = listOf(
"me",
"yesterday?",
"did",
"to",
"want",
"you",
"ask",
"What"
)
answerOption.forEach {
val key = TextView(binding?.answerBox?.context)
with(key){
binding?.answerBox?.addView(this)
setBackgroundColor(Color.WHITE)
text = it
textSize = 18F
setPadding(40, 20, 40, 20)
val margin = key.layoutParams as FlexboxLayout.LayoutParams
margin.setMargins(30, 30, 30, 30)
layoutParams = margin
setOnClickListener {
moveToAnswer(it)
}
}
}
}
private fun moveToAnswer(view: View) {
if(listAnswers.size < actualAnswer.size){
view.setOnClickListener(null)
listAnswers.add(Answer(view, view.x, view.y, (view as TextView).text.toString(), this#MainActivity))
val topPosition = binding?.lineFirst?.y?.minus(120F)
// val topPosition1 = binding?.lineSecond?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
// var leftPosition1 = binding?.lineSecond?.x
if (listAnswers.size > 0) {
var allWidth = 0F
listAnswers.forEach {
allWidth += (it.view?.width)?.toFloat()!! + 20F
}
allWidth -= (view.width).toFloat()
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}else{
// leftPosition1 = binding?.lineSecond?.x?.plus(allWidth)
}
}
val completeMove = object: Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
}
}
if (leftPosition != null) {
if (topPosition != null) {
view.animate()
.setListener(completeMove)
.x(leftPosition)
.y(topPosition)
}
}
}
}
}
And this is the data class "Answer":
data class Answer(var view: View? = null,
var actualPositionX: Float = 0F,
var actualPositionY: Float = 0F,
var text: String = "",
var removeListener: RemoveAnswerListener? = null
){
init {
view?.setOnClickListener {
it.animate()
.x(actualPositionX)
.y(actualPositionY)
removeListener?.onRemove(this)
}
}
}
interface RemoveAnswerListener{
fun onRemove(answer: Answer)
}
You don't seem to be offsetting the View to the second line anywhere? You just initialise its position to the start of the first line:
val topPosition = binding?.lineFirst?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
And then you adjust the x position if there's room for it, otherwise it stays at the start
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}
(unless I'm misunderstanding things - I don't know what lineFirst is, a containing view for the first line I guess)
So you're not moving the view down, or to lineSecond or whatever - and you're not adjusting the x position based on the contents of the second line either.
Honestly if I were you, I'd look into the Flow helper for ConstraintLayout - it works like flexbox and basically moves elements below as they fill up the horizontal space, so it automatically does what you're doing here. Here's a guide on it that should give you the idea. Might save you a lot of hassle!
I want to make a shape filling the circle according to given percentage. Since i suck at math I'm unable to achieve this with path Api of jetpack compose.
My custom class is looking like this:
class ProgressPie(val progress: Float) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val path = Path().apply {
addArc(
Rect(0f, 0f, size.width , size.height ),
-90f,
360f * progress
)
close()
}
return Outline.Generic(path)
}
}
and when the progress parameter changed the result should be looking like this.
You can use Canvas or Modifier.draWithContent to draw a pie chart.
Column(modifier=Modifier.padding(10.dp)) {
var progress by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.background(Color.White)
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind {
drawCircle(Color.LightGray, style = Stroke(1.dp.toPx()))
drawArc(
Color.Blue,
startAngle = -90f,
sweepAngle = progress,
useCenter = true
)
}
)
Slider(
value = progress,
onValueChange = {
progress = it
},
valueRange = 0f..360f
)
}
You can use shape with remember to create new shape on progress change but the issue with Path and arc is it's not drawn from center.
val shape = remember(progress) {
GenericShape { size: Size, _: LayoutDirection ->
if( progress == 360f){
addOval(
Rect(0f, 0f, size.width, size.height)
)
}else {
moveTo(size.width/2f, size.height/2f)
arcTo(
Rect(0f, 0f, size.width, size.height),
-90f,
progress,
forceMoveTo = false
)
}
close()
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.clip(shape)
.background(Color.Blue)
.aspectRatio(1f)
)
I need to have a component that scrolls vertically and horizontally.
Here is what I did so far:
#Composable
fun Screen() {
val scope = rememberCoroutineScope()
val scrollOffset = remember {
mutableStateOf(value = 0F)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(10) {
SimpleRow(
onScrollOffsetChange = {
scrollOffset.value = it
}
)
}
}
}
#Composable
private fun SimpleRow(
onScrollOffsetChange: (Float) -> Unit,
) {
val lazyRowState = rememberLazyListState()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyRowState
) {
item {
Text(
text = "firstText"
)
}
for (i in 1..30) {
item {
Text(text = "qwerty")
}
}
}
}
When anyone of these SimpleRow is scrolled I want to scroll all of the SimpleRows together.
And I do not know how many SimpleRows I have because they came from our server.
Is there any kind of composable or trick that can do it for me?
You can use multiple rows inside a column that scrolls horizontally and vertically, like this:
#Composable
fun MainContent() {
val scrollStateHorizontal = rememberScrollState()
val scrollStateVertical = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollStateVertical)
.horizontalScroll(state = scrollStateHorizontal)
) {
repeat(40){ c ->
Row {
repeat(40){ r ->
Item("col: $c | row: $r")
}
}
}
}
}
#Composable
fun Item(text: String) {
Text(
modifier = Modifier
.padding(5.dp)
.background(Color.Gray),
text = text,
color = Color.White
)
}
I found that function to drag and drop component like in my code simple image
but now i need to found the x and y axis for that image on screen
like for example when i move it to new position put the new position in variable
#Composable
fun ImgLatterMoving(painter: Painter,contentDescription: String) {
val offsetX = remember { mutableStateOf(0F) }
val offsetY = remember { mutableStateOf(0F) }
Box(
modifier = Modifier
.offset {
IntOffset(
x = offsetX.value.roundToInt(),
y = offsetY.value.roundToInt()
)
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX.value += dragAmount.x
offsetY.value += dragAmount.y
}
}
.size(100.dp)
) {
Image(painter = painter,
contentDescription = contentDescription,
modifier = Modifier.clip(RoundedCornerShape(10.dp))
)
}
}
How can I create a scrolling row that scrolls automatically at a fixed speed that loops around the content of a list of images?
I have a lazy row of images as defined below, but haven't found a good way to loop it (like a carousel).
var images: List<String> = listOf()
repeat(8) {
images = images.plus("https://place-puppy.com/300x300")
}
val state = rememberLazyListState()
LazyRow(
modifier = modifier.fillMaxWidth(),
state = state
) {
items(count = images.size) { i ->
val image = images.get(i)
Column(
modifier = Modifier
.width(40.dp)
.aspectRatio(1f)
) {
Image(
painter = rememberImagePainter(image),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
}
}
}
firstly, create an infinite auto-scrolling effect that will be running as long as the composable is active & displayed:
LazyRow() {
....
}
LaunchedEffect(Unit) {
autoScroll(lazyListState)
}
private tailrec suspend fun autoScroll(lazyListState: LazyListState) {
lazyListState.scroll(MutatePriority.PreventUserInput) {
scrollBy(SCROLL_DX)
}
delay(DELAY_BETWEEN_SCROLL_MS)
autoScroll(lazyListState)
}
private const val DELAY_BETWEEN_SCROLL_MS = 8L
private const val SCROLL_DX = 1f
Secondly, update positions of items in the list accordingly:
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
modifier = modifier,
) {
items(images) {
...
if (it == itemsListState.last()) {
val currentList = images
val secondPart = currentList.subList(0, lazyListState.firstVisibleItemIndex)
val firstPart = currentList.subList(lazyListState.firstVisibleItemIndex, currentList.size)
rememberCoroutineScope().launch {
lazyListState.scrollToItem(0, maxOf(0, lazyListState.firstVisibleItemScrollOffset - SCROLL_DX.toInt()))
}
images = firstPart + secondPart
}
}
}
That should give you the looping behavior.
Credits: https://proandroiddev.com/infinite-auto-scrolling-lists-with-recyclerview-lazylists-in-compose-1c3b44448c8