In an Android app I have an object that is created through a custom view. It basically has a rectangular shape. I want to apply a 3D transformation that makes it look like this:
In the draw() function I have tried various things like this:
override fun draw(canvas: Canvas?) {
super.draw(canvas)
val matrix = Matrix()
var pivX = myWidth / 2.0f
var pivY = myHeight /1.0f
matrix.setSkew(1f, 0f, pivX, pivY);
canvas?.withMatrix(matrix) {
drawPath(path(-150F), paint())
drawPath(path(150F), paint())
}
}
I couldn't make it work though. Where's my mistake? What am I missing?
Related
I have built a custom view class called progressCircle which is a snapchat-like, circular progress bar - this is inside a constraint layout, over a circular button.
This view has parameter angle which when called from onViewCreated(), will work perfectly if run
progressCircle.angle = 100f
However, I am trying to animate this onClick. If I run this same code, onClick, the progressCircle will not show up?! After trial and error, I found that updating the background colour here made the view visible & it was updated. IE;
button.setOnClickListener {
progressCircle.setBackgroundColor(android.R.color.transparent)
progressCircle.angle = 270f
}
Whats going on here, and how can I fix this so I can animate it properly...
Edit:
class ProgressCircle(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint: Paint
private val rect: RectF
private val fillPaint: Paint
private val fillRect: RectF
var angle: Float
var startAngle: Float = 0f
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircle)
angle = typedArray.getFloat(R.styleable.ProgressCircle_angle, 0f)
typedArray.recycle()
val offsetAngle = 0f
val color = getColor(context, R.color.outputON)
val strokeWidth = 15f
val circleSize = 276f
paint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.STROKE)
setStrokeWidth(strokeWidth)
setColor(color)
}
rect = RectF(
strokeWidth,
strokeWidth,
(circleSize - strokeWidth),
(circleSize - strokeWidth)
)
fillPaint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.FILL)
setColor(getColor(context, R.color.flat_blue_1))
}
val offsetFill = strokeWidth
fillRect = RectF(
offsetFill,
offsetFill,
(circleSize - offsetFill),
(circleSize - offsetFill)
)
//Initial Angle (optional, it can be zero)
angle = offsetAngle
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (getColor(context, R.color.flat_blue_1) > 0) {
canvas.drawArc(rect, 0f, 360f, false, fillPaint)
}
canvas.drawArc(rect, startAngle, angle, false, paint)
}
}
TIA
By default, a View only redraws itself when something has changed - i.e., when the view is "invalidated" as per the View documentation:
Drawing is handled by walking the tree and recording the drawing commands of any View that needs to update. After this, the drawing commands of the entire tree are issued to screen, clipped to the newly damaged area.
If you want your custom view to redraw itself when the angle property is changed, you need to call invalidate():
var angle: Float
set(value) {
// Do the default behavior of setting the value
field = value
// Then call invalidate() to force a redraw
invalidate()
}
}
Good day!
I faced such a problem that I need to draw a circle under the touch of my finger.
I tried to use the following code:
private fun pressFinger() {
mFirst.setOnTouchListener(View.OnTouchListener { v, event ->
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
drawCircle(x, y)
}
}
return#OnTouchListener true
})
}
fun drawCircle(x: Float, y: Float) {
val width: Int = x.toInt()
val height: Int = y.toInt()
val bitmap = (mFirst.getDrawable() as BitmapDrawable).bitmap
val workingBitmap: Bitmap = Bitmap.createBitmap(bitmap)
val mutableBitmap = workingBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvasBitmap = Canvas(mutableBitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.style = Paint.Style.FILL
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
canvasBitmap.drawCircle(width.toFloat(), height.toFloat(), 180F, paint)
mFirst.setImageBitmap(mutableBitmap)
drawUtils.draw()
}
The problem is that the circle is not drawn under the finger, but on the side.
I also want to note that the image placed inside the ImageView has a larger size and I use scaleType = "cropCenter".
It turns out that I need to get the position of the finger exactly on the image placed in the ImageView, and not just on the screen.
If someone knows the correct answer to the question, I would be grateful.
This is really bad code. Why are you editing the background image in the view? The easier thing would be to save the coordinates in the onTouch function, then invalidate the view. Then in the view's onDraw, you draw a circle at that location. This way you avoid all the scaling issues, and you don't have to make 2 COPIES!!! of the bitmap which is horribly inefficient.
canvasBitmap.drawCircle(width.toFloat() - 180F / 2, height.toFloat() - 180F / 2, 180F, paint)
Have a try.
So I'm trying to draw a rectangle in kotlin and I have a class that does this but I need the size of the canvas to do this so in my main view I call the classes setCanvas() function in my onDraw() function, something like this.
main view
var rectangle = Rectangle()
override fun onDraw(canvas:Canvas) {
super.onDraw(canvas)
rectangle.setNewCanvas(canvas)
rectangle.show()
rectangle.update()
invalidate()
}
class
class Rectangle {
private var canvas: Canvas? = null
var x = 0
var speed = 10
fun setNewCanvas(newCanvas: Canvas)
if(canvas == null) {
canvas = newCanvas
x = canvas?.width!!
}
}
fun show() {
canvas?.drawRect(x.toFloat(), 0F, x.toFloat() - 100, 1000F, Paint(Color.RED))
}
fun update() {
x -= speed
}
}
I'm trying to get the rectangle to move across the screen but when I check if canvas is null in setNewCanvas() it moves but the rectangle is drawn above the canvas in the banner where the apps project name is. If I don't check if canvas is null then the value of x gets reassigned to the width with every call so the rectangle never moves.
I think you should not encapsulate the function of modifying coordinates into Rectangle, and you cannot set canvas repeatedly.
var x = 0
var speed = 10
fun update() {
x -= speed
}
override fun onDraw(canvas:Canvas) {
canvas.drawRect(x.toFloat(), 0F, x.toFloat() - 100, 1000F, Paint(Color.RED))
rectangle.update()
invalidate()
}
I have a 3x3 android.graphics.Matrix, and i want to apply all transformation to a 4x4 OpenGL Matrix for 2D transformations only. So far I have manage to apply rotation and scaling I am using the example from the android team HERE to render a triangle. I used this class for generating the android.graphics.Matrix, from finger gestures made by the user for scale, move and translate transformations.
After that I attach the MatrixGestureDetector on the onTouchEvent from the View. In the MyGLSurfaceView class:
class MyGLSurfaceView : GLSurfaceView {
...
private val matrixGestureDetector = MatrixGestureDetector()
override fun onTouchEvent(e: MotionEvent): Boolean {
matrixGestureDetector.onTouchEvent(e)
requestRender()
return true
}
}
Then I used it to convert the android.graphics.Matrix to a OpenGL Matrix in the onDrawFrame method in
MyGLRenderer class
...
lateinit var matrixGestureDetector: MatrixGestureDetector
override fun onDrawFrame(unused: GL10) {
...
// get graphics matrix values
val m = FloatArray(9)
matrixGestureDetector.matrix.getValues(m)
// set rotation and scaling from graphics matrix to form new 4x4 OpenGL matrix
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
0f, 0f, 0f, 1f
)
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, openGLMatrix, 0)
// draw shape, where scaling and rotation work
mTriangle.draw(scratch)
}
To apply the translation I have to add the m[2] and m[5] from the android.graphics.Matrix values and change the openGLMatrix to:
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
m[2], m[5], 0f, 1f
)
Now the problem is that the OpenGL viewbox size is formed by coordinates in range [-1,1], look at the image below:
But the translation X and Y values from the android.graphics.Matrix are not in that range, to do that I changed it to:
val scaleX: Float = m[android.graphics.Matrix.MSCALE_X]
val skewY: Float = m[android.graphics.Matrix.MSKEW_Y]
val translateX = m[android.graphics.Matrix.MTRANS_X]
val translateY = m[android.graphics.Matrix.MTRANS_Y]
val ratio = width.toFloat() / height
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
-ratio * (translateX / width * 2), -(translateY / height * 2), 0f, 1f
)
Now translation work, but scale and rotation are not done on the pivot point(center point of rotation between the two fingers). How to apply all the transformation and is there a example code for 2D transformations for finger gestures that I can find anywhere?
Well, I figure out that there is miscalculations in the conversion from Graphic coordinate system to a OpenGL coordinate system for the translation. Here is the code for getting the accurate translation in a OpenGL coordinate system, set as separate functions:
fun normalizeTranslateX(x: Float): Float {
val translateX = if (x < width / 2f) {
-1f + (x / (width / 2f))
} else {
(x - (width / 2f)) / (width / 2f)
}
return -translateX * OpenGLRenderer.NEAR * ratio
}
fun normalizeTranslateY(y: Float): Float {
val translateY = if (y < height / 2f) {
1f - (y / (height / 2f))
} else {
-(y - (height / 2f)) / (height / 2f)
}
return translateY * OpenGLRenderer.NEAR
}
I have also updated the whole finger gesture transformation class, for generating the OpenGL matrix, with the applied transformations from the finger gestures here is the class OpenGLFingerGestureTransformations.
To get the OpenGL matrix first create your own OpenGLMatrixGestureDetector object, using the same way as creating MatrixGestureDetector:
class MyGLSurfaceView : GLSurfaceView {
...
private val matrixGestureDetector = OpenGLMatrixGestureDetector()
override fun onTouchEvent(e: MotionEvent): Boolean {
matrixGestureDetector.onTouchEvent(e)
requestRender()
return true
}
}
Then in the MyGLRenderer class just generate the matrix with the method transform()
...
lateinit var matrixGestureDetector: OpenGLMatrixGestureDetector
private val transformedMatrixOpenGL: FloatArray = FloatArray(16)
override fun onDrawFrame(unused: GL10) {
...
// get OpenGL matrix with the applied transformations, from finger gestures
matrixGestureDetector.transform(mMVPMatrix, transformedMatrixOpenGL)
// draw shapes with apply transformations from finger gestures
mTriangle.draw(transformedMatrixOpenGL)
}
I have uploaded the full source code HERE.
Here is the final result:
I have been updating some old code in an app that has a lot of re-allocations in onDraw that complain with message:
Avoid object allocations during draw/layout operations (preallocate and reuse instead).
So I have got all of it updated properly with no more warning, except one. LinearGradient. It seems there is no method to set values on an instance of the object. And the properties are not public so you can't do linLayout.x = value;
This is my code and it complains with the warning described above (underlines LinearGradient):
myPaintGradient.setShader(new LinearGradient(deviation,6,halfwidth,LinearGradientSize,barColorGreen, barColorRed, android.graphics.Shader.TileMode.CLAMP));
I have just solved the same problem, albeit with a RadialGradient.
If you want to update location data of a shader on each draw call you should pre-allocate the shader (as the linter hints at) and you should also pre-allocate a Matrix. The only functionality a shader instance exposes is getLocalMatrix and setLocalMatrix.
To do what you want here, you will make use of setLocalMatrix, passing in a Matrix instance which you have performed some appropriate transforms on. In your case, I think a simple translation transform will do the trick.
As I touched on earlier, you will need to pre-allocate a Matrix and you will modify this pre-allocated one on each draw loop, before passing it into the shader.setLocalMatrix.
Here is an example of how I did this to update the centerX and centerY of a RadialGradient shader. The use case for this was a user can drag a circle around and I needed to keep the radial gradient centered on the circle.
The code below shows you a full working solution as a custom view which you could copy and paste into your code base, and use in some XML.
The interesting bit is in the onDraw override, where I am doing the matrix transforms and updating the shader:
class MoveableGradientCircle #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// Struct for all the data which needs pre-allocating
private data class Circle(
val radius: Float,
val centerX: Float,
val centerY: Float,
val paint: Paint,
val shaderMatrix: Matrix
)
// Pre-allocate the data
private var circle: Circle = Circle(
radius = 10f,
centerX = x,
centerY = y,
paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL_AND_STROKE
shader = RadialGradient(
centerX = 0f,
centerY = 0f,
radius = 10f,
startColor = Color.RED,
edgeColor = Color.GREEN,
tileMode = Shader.TileMode.CLAMP
)
},
shaderMatrix = Matrix()
)
// Setup a touch listener to update the circles x/y positions on user touch location
init {
setOnTouchListener { view, event ->
view.performClick()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
MotionEvent.ACTION_MOVE -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
MotionEvent.ACTION_UP -> {
circle = circle.copy(
centerX = event.x,
centerY = event.y
)
invalidate()
true
}
else -> false
}
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// No need to keep re-allocating shader
// Instead, we update the pre-allocated matrix
// In this case, we'll just translate it to the circles current center x,y
// which happens to be the users touch location
circle.shaderMatrix.reset()
circle.shaderMatrix.setTranslate(
circle.centerX,
circle.centerY
)
// Update the matrix on the shader
circle.paint.shader.setLocalMatrix(circle.shaderMatrix)
// Draw the arc with the updated paint
canvas?.drawArc(
circle.centerX - circle.radius,
circle.centerY - circle.radius,
circle.centerX + circle.radius,
circle.centerY + circle.radius,
0f,
360f,
true,
circle.paint
)
}
}
I hope this helps you or someone else with the same problem in the future!