Why did Canvas draw in Coroutine background thread draw in weird position? - android-custom-view

I have a simple customView that draw a single line as below.
class ViewCustom #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
private val strokePaint = Paint()
.apply { color = Color.BLACK}
.apply { strokeWidth = 6f }
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawLine(
0f, 0f,
width.toFloat(), height.toFloat(),
strokePaint
)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec),
resolveSize(desiredHeight, heightMeasureSpec))
}
}
It works fine, draw the diagonal line end to end.
However, if I add a coroutine thread around it
class ViewCustom #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
private val treeDrawing by lazy {
TreeDrawingCanvasCustom()
}
private val strokePaint = Paint()
.apply { color = Color.BLACK}
.apply { strokeWidth = 6f }
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
CoroutineScope(Dispatchers.Default).launch { // <-- add coroutine
canvas.drawLine(
0f, 0f,
width.toFloat(), height.toFloat(),
strokePaint
)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec),
resolveSize(desiredHeight, heightMeasureSpec))
}
}
It still draws, but at a strange location as shown in the diagram below. I have checked that the height and width value is still the same with or without the coroutine.
Why is it behaving as such?

Just found out the reason is that there's only one canvas shared across all drawings. When the onDraw is on the background thread, there's no guarantee others are also manipulating the main thread in drawing the canvas, hence such concurrent drawing will cause the drawing coordinate to be misaligned from its original coordinate.
In the image diagram case below, the drawing is on the Caption area, is because that happens concurrently between the Text Caption drawing, hence it is drawn on the Text Caption area.

Related

How to make Jetpack Compose Image animating infinite

I have an animated-vector drawable.
I want this animated vector to be animated in loop while this image is showing.
Cannot find a good solution for this.
val image = animatedVectorResource(R.drawable.no_devices_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = image.painterFor(atEnd),
"image",
Modifier.width(150.dp).clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Fit)
When I tap on the image it is animating but then stops. This is kind of an infinite progress.
Leaving my solution here (using compose 1.2.0-alpha07).
Add the dependencies in your build.gradle
dependencies {
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.animation:animation-graphics:$compose_version"
}
And do the following:
#ExperimentalAnimationGraphicsApi
#Composable
fun AnimatedVectorDrawableAnim() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.avd_anim)
var atEnd by remember { mutableStateOf(false) }
// This state is necessary to control start/stop animation
var isRunning by remember { mutableStateOf(true) }
// The coroutine scope is necessary to launch the coroutine
// in response to the click event
val scope = rememberCoroutineScope()
// This function is called when the component is first launched
// and lately when the button is pressed
suspend fun runAnimation() {
while (isRunning) {
delay(1000) // set here your delay between animations
atEnd = !atEnd
}
}
// This is necessary just if you want to run the animation when the
// component is displayed. Otherwise, you can remove it.
LaunchedEffect(image) {
runAnimation()
}
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
null,
Modifier
.size(150.dp)
.clickable {
isRunning = !isRunning // start/stop animation
if (isRunning) // run the animation if isRunning is true.
scope.launch {
runAnimation()
}
},
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(Color.Red)
)
}
Case you need to repeat the animation from the start, the only way I find was create two vector drawables like ic_start and ic_end using the information declared in the animated vector drawable and do the following:
// this is the vector resource of the start point of the animation
val painter = rememberVectorPainter(
image = ImageVector.vectorResource(R.drawable.ic_start)
)
val animatedPainter = rememberAnimatedVectorPainter(
animatedImageVector = AnimatedImageVector.animatedVectorResource(R.drawable.avd_anim),
atEnd = !atEnd
)
Image(
painter = if (atEnd) painter else animatedPainter,
...
)
So, when the animated vector is at the end position, the static image is drawn. After the delay, the animation is played again. If you need a continuous repetition, set the delay as the same duration of the animation.
Here's the result:
Infinite loop without static images solution:
Best working with animated vector drawable that is a loop.
First add the dependencies in your build.gradle
dependencies {
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.animation:animation-graphics:$compose_version"
}
Animated loop loader
#OptIn(ExperimentalAnimationGraphicsApi::class)
#Composable
fun Loader() {
val image = AnimatedImageVector.animatedVectorResource(id = R.drawable.loader_cycle)
var atEnd by remember {
mutableStateOf(false)
}
val painterFirst = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
val painterSecond = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = !atEnd)
val isRunning by remember { mutableStateOf(true) }
suspend fun runAnimation() {
while (isRunning) {
atEnd = !atEnd
delay(image.totalDuration.toLong())
}
}
LaunchedEffect(image) {
runAnimation()
}
Image(
painter = if (atEnd) painterFirst else painterSecond,
contentDescription = null,
)
}
Right now I don't see any option to restart painter to start all over again - you need to rewind it. So in this solution we create two of those painter. One of them is starting with !atEnd. Each time atEnd is changed both of them do their work but we display only one animating from start to end. The other one is silently rewinding the animation.
According to https://developer.android.com/jetpack/compose/resources#animated-vector-drawables
val image = AnimatedImageVector.animatedVectorResource(R.drawable.animated_vector)
val atEnd by remember { mutableStateOf(false) }
Icon(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = null
)
To make the animation loop infinitely:
val image = AnimatedImageVector.animatedVectorResource(id = vectorResId)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd),
contentDescription = null,
)
LaunchedEffect(Unit) {
while (true) {
delay(animDuration)
atEnd = !atEnd
}
}
I tried to implement a loading spinner using this approach, but encountered some issues.
I had an issue with the proposed solution, where the animation reverses back to the beginning. To prevent this, as #nglauber suggested, I tried switching to a static vector when the animation ends. Unfortunately, that wasn't smooth as the animation would have to wait for the delay before restarting.
This was the workaround that I used.
#OptIn(ExperimentalAnimationGraphicsApi::class)
#Composable
fun rememberAnimatedVectorPainterCompat(image: AnimatedImageVector, atEnd: Boolean): Painter {
val animatedPainter = rememberAnimatedVectorPainter(image, atEnd)
val animatedPainter2 = rememberAnimatedVectorPainter(image, !atEnd)
return if (atEnd) {
animatedPainter
} else {
animatedPainter2
}
}

Transparent view

I would like to create a View (stage, window) with partially transparent background. I have an image containing alpha channel
I used this kind of scenes in JavaFx, where I had to set the scene fill to null and the root node background color to transparent. I tried the same with TornadoFX:
class NextRoundView : View("Következő kör") {
override val root = vbox {
style {
backgroundColor = multi(Color.TRANSPARENT)
backgroundImage = multi(URI.create("/common/rope-bg-500x300.png"))
backgroundRepeat = multi(BackgroundRepeat.NO_REPEAT
to BackgroundRepeat.NO_REPEAT)
}
prefWidth = 500.0
prefHeight = 300.0
spacing = 20.0
padding = insets(50, 20)
text("A text") {
font = Font.font(40.0)
alignment = Pos.CENTER
}
button("OK")
{
font = Font.font(20.0)
action {
close()
}
}
sceneProperty().addListener{ _,_,n ->
n.fill = null
}
}
}
I'm calling the view like this:
NextRoundView().apply {
openModal(stageStyle = StageStyle.TRANSPARENT, block = true)
}
However, the stage is still has background:
What have I missed?
You've made a couple of mistakes that causes this. First of all, you must never manually instantiate UICompoenents (View, Fragment). Doing so will make them miss important life cycle callbacks. One important callback is onDock, which is the perfect place to manipulate the assigned scene. Changing these two issues and also cleaning up some syntax leads to this code, which successfully makes the background transparent:
class MyApp : App(MyView::class)
class MyView : View() {
override val root = stackpane {
button("open").action {
find<NextRoundView>().openModal(stageStyle = StageStyle.TRANSPARENT, block = true)
}
}
}
class NextRoundView : View("Következő kör") {
override val root = vbox {
style {
backgroundColor += Color.TRANSPARENT
backgroundImage += URI.create("/common/rope-bg-500x300.png")
backgroundRepeat += BackgroundRepeat.NO_REPEAT to BackgroundRepeat.NO_REPEAT
}
prefWidth = 500.0
prefHeight = 300.0
spacing = 20.0
padding = insets(50, 20)
text("A text") {
font = Font.font(40.0)
alignment = Pos.CENTER
}
button("OK") {
font = Font.font(20.0)
action {
close()
}
}
}
override fun onDock() {
currentStage?.scene?.fill = null
}
}
Here is a screenshot of the app with the changes implemented:

Flutter: How would one save a Canvas/CustomPainter to an image file?

I am trying to collect a signature from the user and save it to an image. I have made it far enough that I can draw on the screen, but now I'd like to click a button to save to an image and store in my database.
This is what I have so far:
import 'package:flutter/material.dart';
class SignaturePadPage extends StatefulWidget {
SignaturePadPage({Key key}) : super(key: key);
#override
_SignaturePadPage createState() => new _SignaturePadPage();
}
class _SignaturePadPage extends State<SignaturePadPage> {
List<Offset> _points = <Offset>[];
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(painter: new SignaturePainter(_points)),
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null)
canvas.drawLine(points[i], points[i + 1], paint);
}
}
bool shouldRepaint(SignaturePainter other) => other.points != points;
}
Not sure where to go from there...
You can capture the output of a CustomPainter with PictureRecorder. Pass your PictureRecorder instance to the constructor for your Canvas. The Picture returned by PictureRecorder.endRecording can then be converted to an Image with Picture.toImage. Finally, extract the image bytes using Image.toByteData.
Here's an example: https://github.com/rxlabz/flutter_canvas_to_image
Add the rendered method in your widget
ui.Image get rendered {
// [CustomPainter] has its own #canvas to pass our
// [ui.PictureRecorder] object must be passed to [Canvas]#contructor
// to capture the Image. This way we can pass #recorder to [Canvas]#contructor
// using #painter[SignaturePainter] we can call [SignaturePainter]#paint
// with the our newly created #canvas
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
SignaturePainter painter = SignaturePainter(points: _points);
var size = context.size;
painter.paint(canvas, size);
return recorder.endRecording()
.toImage(size.width.floor(), size.height.floor());
}
Then using state fetch the rendered image
var image = signatureKey.currentState.rendered
Now, you can produce png Image using toByteData(format: ui.ImageByteFormat.png) and store using asInt8List()
var pngBytes = await image.toByteData(format: ui.ImageByteFormat.png);
File('your-path/filename.png')
.writeAsBytesSync(pngBytes.buffer.asInt8List());
For complete example, on how to export canvas as png check out this example
https://github.com/vemarav/signature
The existing solutions worked for me, but the images I captured with PictureRecorder were always blurry vs. what was rendering on-screen. I eventually realized I could use some elementary Canvas tricks to pull this off. Basically, after you create the PictureRecorder's Canvas, set its size to multiple times your desired scale (here I have it set to 4x). Then just canvas.scale it. Boom - your generated images are no longer blurry vs. what appears on screens with modern resolutions!
You may want to crank the _overSampleScale value higher for printed or images that may be blown up/expanded, or lower if you're using this a ton and want to improve image preview loading performance. Using it on-screen, you'll need to constrain your Image.memory Widget with a Container of the actual width and height, as with the other solutions. Ideally this number would be the ratio between Flutter's DPI in its fake "pixels" (i.e. what PictureRecorder captures) and the actual DPI of the screen.
static const double _overSampleScale = 4;
Future<ui.Image> get renderedScoreImage async {
final recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
final size = Size(widget.width * _overSampleScale, widget.height * _overSampleScale);
final painter = SignaturePainter(points: _points);
canvas.save();
canvas.scale(_overSampleScale);
painter.paint(canvas, size);
canvas.restore();
final data = recorder.endRecording()
.toImage(size.width.floor(), size.height.floor());
return data;
}
Given all the data that you need to paint your custom painter, this is all you need to do (in this example, "points" were needed for my customer painter,of course this will change based on your usecase):
Future<void> _handleSavePressed() async {
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
var painter = MyCustomPainter(points: points);
var size = _containerKey.currentContext.size;
painter.paint(canvas, size);
ui.Image renderedImage = await recorder
.endRecording()
.toImage(size.width.floor(), size.height.floor());
var pngBytes =
await renderedImage.toByteData(format: ui.ImageByteFormat.png);
Directory saveDir = await getApplicationDocumentsDirectory();
String path = '${saveDir.path}/custom_image.jpg';
File saveFile = File(path);
if (!saveFile.existsSync()) {
saveFile.createSync(recursive: true);
}
saveFile.writeAsBytesSync(pngBytes.buffer.asUint8List(), flush: true);
await GallerySaver.saveImage(path, albumName: 'iDream');
print('Image was saved!');
}
Answer based on https://gist.github.com/OPY-bbt/a5418127d8444393a2ef25ad2d966dc0
Follow the complete class to draw a PNG image using Flutter > 3.0.0
import 'dart:typed_data';
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class BitmapUtils {
Future<Uint8List> generateImagePngAsBytes(String text) async {
ByteData? image = await generateSquareWithText(text);
return image!.buffer.asUint8List();
}
Future<ByteData?> generateSquareWithText(String text) async {
final recorder = PictureRecorder();
final canvas = Canvas(
recorder, Rect.fromPoints(Offset(0.0, 0.0), Offset(200.0, 200.0)));
final stroke = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke;
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, 200.0, 200.0), stroke);
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: Colors.black,
fontSize: 30,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center);
textPainter.layout();
// Draw the text centered around the point (50, 100) for instance
final offset =
Offset(50 - (textPainter.width / 2), 100 - (textPainter.height / 2));
textPainter.paint(canvas, offset);
final picture = recorder.endRecording();
ui.Image img = await picture.toImage(200, 200);
final ByteData? pngBytes =
await img.toByteData(format: ImageByteFormat.png);
return pngBytes;
}
}

My AsyncTask does not update UI smoothly (animation)

I want to make a TextView appear little by little, like animation. The problem is, the animation is not smooth. It gets stuck for a little while sometimes and then resumes. Sometimes even worse, it goes back... I mean, the TextView gets bigger and bigger but suddenly gets smaller then bigger again. Could anyone help me?
private class UnfoldTask extends AsyncTask<Integer, Integer, Integer> {
View view;
public UnfoldTask(View v) {
this.view = v;
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = 0;
view.setLayoutParams(pa);
}
#Override
protected Integer doInBackground(Integer... maxHeight) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
while (pa.height < maxHeight[0]) {
pa.height += (int) (24 * getResources().getDisplayMetrics().density + 0.5f);
sleep(100);
publishProgress(pa.height);
}
return maxHeight[0];
}
private void sleep(int i) {
try {
Thread.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
#Override
protected void onProgressUpdate(Integer... values) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = values[0];
view.setLayoutParams(pa);
}
#Override
protected void onPostExecute(Integer result) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = result;
view.setLayoutParams(pa);
}
}
You should be using a scale animation for this. Here's an example:
ScaleAnimation animation = new ScaleAnimation(1, 2, 1, 2, centerX, centerY); // Scales from normal size (1) to double size (2). centerX/Y is the center of your text view. Change this to set the pivot point of your animation.
animation.setDuration(1000);
myTextView.startAnimation(animation);
You can use droidQuery to simplify this:
//this will set the height of myView to 0px.
$.with(myView).height(0);
//when you are ready to animate to height (in pixels):
$.with(myView).animate("{height:" + height + "}", new AnimationOptions());
Check the documentation if you want to get fancy - such as adding duration, and event callbacks. If you are still noticing non-smooth animation, consider adding the application attribute to your AndroidManifest:
android:hardwareAccelerated="true"

SlimDX Handling Window Resizing

I'm trying to handle the program window being resized, and the (I think inefficient) code I've flung together below seems to do the trick.
Is there a better way to do this, preferably one that does not create a stutter when resizing the window and which does not constantly use 12-17% of a CPU? I also suspect MessagePump.Run may somehow run before form.Resize finishes setting up the device again, and throw an error.
Thanks!
using System;
using System.Drawing;
using System.Windows.Forms;
using SlimDX;
using SlimDX.Direct3D9;
using SlimDX.Windows;
namespace SlimDX_1
{
struct Vertex
{
public Vector4 Position;
public int Color;
}
static class Program
{
private static VertexBuffer vertices;
private static Device device;
private static RenderForm form;
private static PresentParameters present;
private static VertexDeclaration vertexDecl;
private static VertexElement[] vertexElems;
private static bool wasMinimized = false;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
form = new RenderForm("Tutorial 1: Basic Window");
init();
form.Resize += (o, e) =>
{
if (form.WindowState == FormWindowState.Minimized)
{
foreach (var item in ObjectTable.Objects)
{
item.Dispose();
}
wasMinimized = true;
}
else
{
foreach (var item in ObjectTable.Objects)
{
item.Dispose();
}
init();
device.SetRenderState(RenderState.FillMode, FillMode.Wireframe);
device.SetRenderState(RenderState.CullMode, Cull.None);
present.BackBufferHeight = form.ClientSize.Height;
present.BackBufferWidth = form.ClientSize.Width;
device.Reset(present);
}
};
MessagePump.Run(form, () =>
{
if (form.WindowState == FormWindowState.Minimized)
{
return;
}
device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0f, 0);
device.BeginScene();
device.SetStreamSource(0, vertices, 0, 20); // 20 is the size of each vertex
device.VertexDeclaration = vertexDecl;
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
device.EndScene();
device.Present();
});
foreach (var item in ObjectTable.Objects)
{
item.Dispose();
}
}
private static void init()
{
present = new PresentParameters();
//present.EnableAutoDepthStencil = false;
//present.BackBufferCount = 1;
//present.SwapEffect = SwapEffect.Discard;
present.Windowed = true;
present.BackBufferHeight = form.ClientSize.Height;
present.BackBufferWidth = form.ClientSize.Width;
//present.BackBufferFormat = Format.Unknown;
device = new Device(new Direct3D(), 0, DeviceType.Hardware, form.Handle, CreateFlags.HardwareVertexProcessing, present);
vertices = new VertexBuffer(device, 3 * 20, Usage.WriteOnly, VertexFormat.None, Pool.Managed);
vertices.Lock(0, 0, LockFlags.None).WriteRange(new Vertex[]
{
new Vertex() { Color = Color.Red.ToArgb(), Position = new Vector4(400.0f, 100.0f, 0.5f, 1.0f) },
new Vertex() { Color = Color.Blue.ToArgb(), Position = new Vector4(650.0f, 500.0f, 0.5f, 1.0f) },
new Vertex() { Color = Color.Green.ToArgb(), Position = new Vector4(150.0f, 500.0f, 0.5f, 1.0f) }
});
vertices.Unlock();
// specifies the layout of the vertexes
vertexElems = new VertexElement[]
{
new VertexElement(0, 0, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.PositionTransformed, 0),
new VertexElement(0, 16, DeclarationType.Color, DeclarationMethod.Default, DeclarationUsage.Color, 0),
VertexElement.VertexDeclarationEnd
};
vertexDecl = new VertexDeclaration(device, vertexElems);
}
}
}
You're going way above and beyond what you need to do when the window is resized. You're releasing every single DirectX object you've created, including the graphics device, and then recreating everything. This is going to take a comparatively long time, which is why you're seeing performance issues.
In fact, none of your objects need to be released. Simply call the Reset() function on the device to recreate the backbuffer to match the new window size. Check out some of the native Direct3D9 tutorials on window resizing to see how in general how the process works.

Resources