Is there a way to achieve a "rotating" border effe...
# compose
s
Is there a way to achieve a "rotating" border effect, where there are many colors and those colors are moving around, by knowing the colors and a generic
Shape
?
The first approach was to do
Copy code
val degrees ... animation from 0f to 360f
...
clip(shape)
 .padding(borderWidth)
 .drawWithContent {
  rotate(degrees = degrees) {
   drawCircle(
    brush = gradient,
    radius = size.width,
    blendMode = BlendMode.SrcIn,
   )
  }
  drawContent()
 },
Which works, however this adds a padding to the content equivalent to the borderWidth which is what I am trying to avoid here. The second idea was to use a
Copy code
val stroke = Stroke(4.dp.toPx())
drawWithContent {
 drawContent()
 drawPath(
  Path().apply { addOutline(shape.createOutline(size, layoutDirection, this@drawWithContent)) },
  Brush.sweepGradient(colors),
  style = stroke,
 )
}
But here I do not see a way for me to "rotate" the colors. I've tried to look into possibilities like: • Adding a PathEffect to the stroke but I don't think that's the use of this parameter. • Changing the order of the colors in the sweepGradient brush itself, but then it happens too drastically, I'd optimally like this to rotate smoothly. The third idea was to do something like:
Copy code
drawWithContent {
 drawContent()
 rotate(degrees) {
  drawCircle()
 }
 drawSmallerOutline() // An outline which by hopefully using some sort of BlendMode would cancel out the circle right above, but only the inner part of it, and leave the outer `borderWidth` size as-is. Effectively making it look like a normal border.
}
But I have yet again not found a way here to achieve such a way of one path cancelling out the other, without also affect the content which is drawn behind. best I could do is the inner outline cancelling out the content inside and leaving the border in-tact. But that also cleared out all of the normal content which was not rendered at all.
For visuals, think of something like this where the colors are rotating around the border, But instead of hard stops in colors I tried with a sweepGradient because I want the gradient effect instead.
s
I would try implementing it as overlapping colorful paths, with the color offset controlled by trimming the path. This approach would enable animation of the length of each color segment
s
I would optimally want to keep the length of each color segment the same throughout the animation, and also have all of the segments stay the same length compared to each other. Perhaps what I could animate there is their placement in relation to one another, or is that what you are suggesting against here? Did you mean that there would be X amount of paths, where X is the number of colors, all of which would be on top of each other, and all of them would span the entire border length. But each of those lines would have a start/stop along this path which would be animated throughout time. And it could be made to look seamless by making them just stop at the position which the next one starts? Just trying to understand what you mean first in order to perhaps start looking into what APIs exist to make this happen
s
Did you mean that there would be X amount of paths, where X is the number of colors, all of which would be on top of each other, and all of them would span the entire border length. But each of those lines would have a start/stop along this path which would be animated throughout time. And it could be made to look seamless by making them just stop at the position which the next one starts?
yes
image.png
s
Any hints as to how to begin with this? Perhaps even any previous thread I could read myself in here? Is the idea to pass my path into a
PathMeasure()
and try to get the split paths from it from in there using
.getSegment(...)
? Perhaps using
Path.divide()
somehow? This is completely uncharted territory for me so I am just looking at the names and seeing what might sound fitting 😄
s
I would just use completely different paths and animate only trim start and trim end. I wouldn’t subdivide the path into segments
Unfortunately, I don’t remember specific APIs or threads to read. However, Romain Guy gave a good presentation on graphics and paths.
s
Copy code
fun drawPath(
 path: Path,
 brush: Brush,
 @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
 style: DrawStyle = Fill,
 colorFilter: ColorFilter? = null,
 blendMode: BlendMode = DefaultBlendMode
)
drawPath itself doesn't seem to give any options to provide custom start and end positions for trimming. Which API did you have in mind here when you meant to animate the trim values? Edit: Ah, perhaps in
Brush.linearGradient()
, giving that a try
old but gold
Also

https://www.youtube.com/watch?v=gtQsC1bav5M

Using
PathEffect
and
chainPathEffect
as explained in the above two articles may be an easy approach
s
The ribbon animates its placement but the colors stay static. I have the opposite scenario where the path is always drawn, but my colors move. I don't think this is exactly applicable here but I will try to take a closer look. I don't think the PathEffect is what I want here, as that one doesn't affect the color really, it adds stops and other stuff like that. Unless I am misunderstanding this part so badly as well 😅
s
Ah, I was thinking that you'd animate the Float
intervals
to achieve your effect
Sinasamaki achieves this by drawing the path multiple times with different phases on a
dashPathEffect
Copy code
onDrawWithContent {
	drawPath(  
	    path = ribbonPath,  
	    color = color,  
	    style = Stroke(  
	        width = stroke.toPx(),
	        pathEffect = PathEffect.dashPathEffect(  
				intervals = distanceArray.toFloatArray(),  
			),
	    )  
	)
	
	drawContent()
	
	drawPath(  
	    path = ribbonPath,  
	    color = color,  
	    style = Stroke(  
	        width = stroke.toPx(),   
	        pathEffect = PathEffect.dashPathEffect(  
				intervals = distanceArray.toFloatArray(),  
				phase = distanceArray.first()  
			),  
	    )  
	)
}
ColorBorderTest.kt
It can likely be optimized a bit further, but there's a proof of concept
r
You could also do it with a sweep gradient
1
Anyway, dashes work but it's a bit cumbersome. I would just create a
Rect
inside a
Path
and then use
getSegment
s
I think SweepGradient would work great for paths which are close to circles, but not necessarily an arbitrary path
r
@Seri Yes it won't work for everything but it'll work for a rectangle
💯 1
but it can be tricky to setup properly as well
s
Yeah I have a generic Shape here unfortunately, not a rectangle shape
r
Then
PathMeasure
+
getSegment
are your friends
👀 1
👍 1
(
divide()
is something different, it extracts multiple contours inside a
Path
as separate
Path
objects)
(for instance if you do
Path().apply { addRect(…); addRect(…) }.divide()
you get a list of 2
Path
, each containing a rectangle)
s
I saw the description for
getSegment
and couldn't make sense of it. Does it modify the
destination
parameter?
r
Yes it does
What
getSegment
does is take a chunk of your source path (from start to end distance) and saves that in the destination
For example if your source is a Path containing a line from x=0 to x=100
If you pass start=20, end=60, destination will now hold a line from x=20 to x=60
You can use this API to animate the drawing of any path by using start=0 and animated end
s
I gave it a shot and it looks like you'd need to handle looping segments yourself, since getSegment doesn't have a mode for closed path loops where
startD > stopD
(here is the getSegment example, the transitions on looping segments are not yet handled)
l
I think there are two separate problems here: 1. Drawing an 'inner' border, similar to
border()
, in userspace 2. Applying a gradient / shader / etc to that border 1. is very easy for a rectangle, it can be complicated for advanced shapes though 🙂 This is a known 'issue' / something we would like to expose an API for. 2. There are a few different ways you could achieve this once you have the proper inner path, if you need each segment to be exactly the same length that's more complicated, but if you just want to apply a radial gradient for example that's not too bad
s
Technically it's fine for it not to be "inner", it can also be just on the line of the border and have half of it go outside of the box and half inside. I just wanted to avoid adding padding to the real content. When you say it'd not be too bad with a radial gradient, did you mean with the first approach that I mentioned in the original message, or something else which I haven't tried yet?
l
Yeah the first approach - it's not too difficult to implement and it gets most of the effect.
Or just drawing the border with a provided brush for the stroke, etc
r
Note: you want a sweep gradient, not a radial gradient. But as it was mentioned earlier, this only works for certain shapes, not all shapes
l
ah yeah, thanks for clarifying 🙂 We implemented something similar recently using a runtime shader, for more flexibility for different shapes
r
Yeah, it just doesn't scale well to arbitrary shapes (unless you have the tangent data or the shapes can be fully described as distance fields or whatever)
s
Bit late here, but I've had this exact use-case, and solved it with a custom Brush (a sweep gradient with a
startOffset
parameter). Submitted it as a feature request a while ago: https://issuetracker.google.com/issues/303944825 Both skiko and Android implementations are in this gist: https://gist.github.com/Skaldebane/8e042b76023fbe20a7d70b59a9938f90
🤗 1
☝️ 1
s
My friend, you are anything but late. You are, I would say, more on time than you could ever be. This works exactly as I wanted it to work, thank you so much!
e
Arriving late here but I implemented this with a sweep gradient + rotation. My constraints were a bit special (building a design system) and while my initial implementation was like the custom brush, I really didn’t like the recompositions or constantly recreating the shader while animating, so I opted for drawing the border in a offscreen graphics layer and applying rotation to it there before drawing it around the content. The main caveat was that I needed to copy the platform border implementation (
borderDrawLambda
) to get it to look like and be a drop in replacement for the platform border (I imagine thats what you meant @Louis Pullen-Freilich [G]in point number 1) so I wouldnt necessarily recommend this. Just sharing for information purposes, the entire gist of the approach is: