class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset()
mDrawPath.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate()
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
So, I'm taking a course about Android Development and the instructor is building a drawing app -- and I can't understand how to code works, because he's not explaining the 'why'; so, he types a lot of code without explaining why he uses that variable or why is he overring those functions; therefore, I don't understand how to code work. Could you help me in understanding how this code work?
class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
// init block will called first when instance will be created so we
// are calling method setUpDrawing() as it is initialising everything
// that required to draw like color , brush size , brush behaviour
// (round , stroke etc .. ) . in simple manner , we can say painter is
// collecting all required tools before starting to paint
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
// this method is going to be called by system when size is going to be
// changed so we are here creating blank board on which we are going to
// draw
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
// this method will be called by system when user is going to touch screen
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
// this event will be fired when user put finger on screen
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset() // reseting path before we set inital point
mDrawPath.moveTo(touchX!!, touchY!!)// set point from where drawing will be started
}
// this event will be fired when user start to move it's fingure . this will be fired continually until user pickup fingure
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
// this event will be fired when user will pick up fingure from screen
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate() / /refreshing layout to reflect drawing changes
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
This code is a custom view class in Android for drawing. The class extends the View class and implements touch event handling to allow a user to draw on the screen.
The class defines instance variables to store the paint, path, bitmap and canvas used for drawing. It also has variables for brush size and color.
In the setUpDrawing method, the paint object for drawing and the path object for tracking the user's touch are set up.
In onSizeChanged, the bitmap object is created and its canvas object is initialized.
In onDraw, the canvas is drawn on the screen and the path is drawn on the canvas if it's not empty.
The onTouchEvent method handles touch events (DOWN, MOVE, UP) and updates the path accordingly.
The FingerPath inner class extends the Path class and contains the color and brush size of the path.
Have you read through this section of the docs? Custom View Components
It explains the basics of creating a custom view, which on a basic level means overriding onDraw (where you draw the contents of the view) and maybe onSizeChanged (where you're told how big the view is - maybe you want to set the thickness of your paint brushes, work out where the centre of the view is etc).
onTouchEvent is a little more advanced, that allows you to handle the user poking at your view - the link up there gets into it, but you'll have a better understanding if you read the section on touch gestures. Broadly, you can get info about interaction events through onTouchEvent and work out things like whether the user touched the view, moved a finger, lifted it or moved it out of bounds etc. You'd use this info to, say, draw stuff under the user's finger for a painting app.
A Canvas is a drawing interface for a Bitmap. Instead of having to work out what pixels to change to what colour to draw a line or whatever, you do all your drawing through a Canvas that wraps that Bitmap. This lets you abstract the process and do high-level stuff like "draw a line", "draw an arc", "draw this entire path", "render this text" and so on. The Canvas works out how to make that happen in the Bitmap's pixel array.
The drawing happens with Paints, which you can think of as an object that says how the drawing happens. What does drawing a line look like? The Paint you use defines things like stroke width, transparency, colour etc - it's like the tool options in a paint program. It also allows you to set things like text size (if you're using it to render text) and do fancy things with blending over the pixels it's being drawn on, create shaders etc. It can also do things like control tiling if you're using a bitmap as the source image.
Generally you'll want to create these Paints at startup and reuse them during your View's lifespan, so it's typical to have a few of them - the one for painting the background, the one for drawing curves, the one for rendering text etc.
Usually in onDraw you just draw the view in its current state. The method takes a Canvas parameter - that's how you draw to the actual bitmap rendered on the UI. So if you had a knob, you could draw the circle, draw the position indicator at the appropriate location, add any text, and you're done!
For a drawing app it's a little more complicated - that "state" you need to remember and draw is the sum of all the drawing operations that have happened. You can get fancy with this (to create levels of undo) but the simplest approach is to create your own Bitmap to store the current drawing. You wrap that in its own Canvas (so you can draw on it) and then all the user interaction draws stuff to that Canvas, so you're painting on the bitmap. Then, in onDraw, you basically just need to paint the contents of that bitmap onto the system-provided Canvas so it gets drawn on-screen - just showing what you have, basically.
The code you have here is a little weird - you have that internal bitmap (called mCanvasBitmap) wrapped in a Canvas (called canvas) but you never actually draw to it. I feel like this line is meant to be rendering the Path you build up with the touch gestures:
override fun onDraw(canvas: Canvas) {
...
if (!mDrawPath.isEmpty) {
...
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
but in onDraw's scope canvas is referring to that parameter being passed in, i.e. the Canvas for drawing to the screen, not the one wrapping mCanvasBitmap. So that internal bitmap always stays empty, and then you just draw the most recent path (this is a good example of watching how you name things - why not mCanvas to be consistent?). It never "builds up" an image by drawing paths onto your internal bitmap. I can't tell what the exact intent is from the code though, I just thought I'd point it out!
Related
I am working on implementing an eraser tool for a paint style app using Kotlin. I have the following function which so far just changes whatever i touch to black.
fun setEraser(){
mDrawPaint!!.alpha = 0
mDrawPaint!!.maskFilter = null
mDrawPaint!!.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
mDrawPaint!!.isAntiAlias = true
}
I have the following private variables which I am also making use of when the function setEraser is called:
private var mCanvasBitMap: Bitmap? = null
private var mDrawPaint: Paint? = null
private var mCanvasPaint: Paint? = null
private var color = Color.BLACK
private var canvas: Canvas? = null
I have found examples for Java but havent been able to make use of them:
Android: Painting app with eraser not working
Implementing an eraser in an Android drawing app - black trail and then transparent
Any help would be greatly appreciated! I am quite stuck after lots of googling and reading :/
I'm making android app with Kotlin in android studio now, and want to draw on already drawn custom view.
This app is simple app about shows graphical information with received data with serial port. Actually I'm new on Kotlin and rookie on Java/Android. This is code that has problem.
This is XML of custom view, (it is in the contraintlayout)
<!--activity_main.xml-->
<androidx.constraintlayout.widget.ConstraintLayout ... >
<com.(...).DrawUI
android:id="#+id/DrawUI" ...
/>
Trying to call drawing function 'DrawRxData(rxData)' on this thread,
//BluetoothController.kt
class BluetoothClient(private val activity: MainActivity,
private val socket:BluetoothSocket): Thread() {
override fun run() {
...
if(inputStream.available() > 0) {
...
inputStream.read(buffer)
val rxData = BluetoothRxParser(buffer)
DrawRxData(rxData)
...
}
...
}
}
And this is DrawUI that wrote above on XML
I want to draw something on this View with calling function in BluetoothController.kt!
//DrawUI.kt
class DrawUI(context: Context, attrs: AttributeSet?) : View(context, attrs) {
init {
// initializing Paint()s
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// measuring dimension and determine drawing sector
}
override fun onDraw(canvas: Canvas) {
// drawing background
}
}
I tried use Canvas on DrawUI class but it didn't work properly.
Also tried making Bitmap but it occurs error that width and height are zero. How can I fix this problem?
EDIT
This way is how I solved this problem, maybe there is another and simple solution..
Created ImageView that has same size with DrawUI on XML
<ImageView
android:id="#+id/DrawRx" ...
/>
Get View and cast to ImageView, then create bitmap and canvas.. DrawRxData() is in BluetoothClient class.
private fun DrawRxData (rxData: RxData) {
val v : View = activity.findViewById(R.id.DrawRx)
val iv : ImageView = v as ImageView
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.setColor(Color.BLACK)
canvas.drawCircle(50F, 50F, 10F, paint)
Attach bitmap to ImageView but for thread, it looks like Dispatcher in C#..
Reference
activity.runOnUiThread(java.lang.Runnable {
iv.setImageBitmap(bitmap)
})
Actually just overlapped exist view, please answer if there is efficient way!
I read that is bad practice to call invalidate() inside the onDraw method so i want to be able to call invalidate() from my Ball class to the onDraw method that is on the Game class.
In another post i read that creating an object of the Game class and then calling that object like gameClass.invalidate() is the best way to do it and im trying to figure out how to do that.
Im having trouble passing the context and attrs.
Here is my CustomView class:
class Game(context: Context?, attrs: AttributeSet?) : View(context,
attrs){
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval((width / 2) - 50,
ball1.posy.toFloat() - 50f,
(width / 2) + 50,
ball1.posy.toFloat() + 50f,
circleColor)
}
ball1.update()
}
Here is the class where i need to call invalidate:
is there a better way to call invalidate() here
val gameClass = Game( how do i pass the context and attrs here? )
class Ball(var posx: Double, var posy:Double,var velocity: Double){
//how do i pass the context and attrs here?
val gameClass = Game(...)
fun update(){
posy += 10
gameClass.invalidate()
}
}
here is my xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<view android:layout_width="0dp"
android:layout_height="0dp"
class="com.example.myapplication.Game"
id="#+id/view4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
Okay first things first. It maybe a typo in your question but according to your code Game class is instantiating a Ball object and Ball class is instantiating a Game object. This is called circular dependency and you don't want this because it will result in StackOverFlow error (Ironic isn't it ?). So remove the Game object from Ball class.
Secondly, since you are calling ball1.update() inside onDraw() so calling invalidate() inside ball1.update() is no different than calling it after ball1.update(). In both cases you are calling invalidate() inside onDraw().
The purpose of invalidate() is to tell the view to redraw itself whenever we change any data related to the view. Since you are changing the data related to view inside onDraw() by calling ball1.update() so calling invalidate() right after it would be the logical step to take. Like this and it will work in your case.
class Game(context: Context, attributes: AttributeSet): View(context,attributes) {
private val paint :Paint = Paint(ANTI_ALIAS_FLAG)
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
init{
paint.color = Color.CYAN
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval(
(width / 2) - 50f,
ball1.posy.toFloat() - 50f,
(width / 2) + 50f,
ball1.posy.toFloat() + 50f,
paint)
ball1.update()
invalidate()
}
}
}
But onDraw() is not the best place to call invalidate(). There are many performance related issues so it is better to leave onDraw() alone.
Read here:
https://developer.android.com/training/custom-views/optimizing-view
https://www.youtube.com/watch?v=zK2i7ivzK7M
I'm assuming you want to animate your view. So it will be better to read the docs on animation.
But anyway let's continue. If you want to avoid calling invalidate() from onDraw() but still want to achieve the same result. Your best bet would be to create another function inside Game class which will start the process of updating your Ball object and calling invalidate() continuously. I'm naming this method as startAnimation()
This is how we can do this. Since your view is running on the UI Thread we need to get a Handler to it and tell the UI to run a Runnable continuously by using Handler.post(). That Runnable will contain the code that will update your Ball object and call invalidate(). Luckily the View class itself contains post() method which is the same as Handler.post() so we can safely call this method inside our Game class because Game inherits View.
Here is the code :
class Game(context: Context, attributes: AttributeSet): View(context,attributes) {
private lateinit var runnable : Runnable // reference to the runnable object
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
private val paint :Paint = Paint(ANTI_ALIAS_FLAG)
//This is the constructor of the class
init{
paint.color = Color.CYAN
//Here we initialize our Runnable and put the code that we want to run once we call startAnimation()
runnable = Runnable {
ball1.update()
invalidate()
//Calling post() inside here will loop the above code and you will see a smooth animation
post(runnable)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval(
(width / 2) - 50f,
ball1.posy.toFloat() - 50f,
(width / 2) + 50f,
ball1.posy.toFloat() + 50f,
paint)
}
}
//This is the new function I am talking about
fun startAnimation()
{
post(runnable)
}
fun stopAnimation()
{
removeCallbacks(runnable)
}
}
And we start the animation by calling startAnimation() from MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val game = findViewById<Game>(R.id.view)
game.startAnimation()
}
}
To stop the animation just call stopAnimation()
Read more about processes, threads and handler here :
https://developer.android.com/guide/components/processes-and-threads
https://developer.android.com/reference/android/os/Handler
I have my own custom view that extends RecyclerView and I use it to render a document with multiple pages. Each item in the adapter is a page.
I want to add the ability to zoom and pan the entire document (not each page individually).
I have tried following the Android guide here but I still can't get both zoom and pan to work properly.
My current approach is this:
Have a GestureDetector to detect pan gestures
Have a ScaleGestureDetector to detect pinch gestures
When each of these detectors register events, save the information regarding the current scale factor and the pan, call invalidate() and modify the Canvas in the drawChild method appropriately.
It is the last step that I'm unsure how to implement properly.
I'm using this data class to keep track of the view's current pan and zoom:
private data class PanAndZoom(
var scaleFactor: Float,
var focusX: Float,
var focusY: Float,
var panX: Float ,
var panY: Float
)
// field in my custom view initialised like this:
private val panAndZoom = PanAndZoom(1f, 0f, 0f, 0f, 0f)
and here is how I update the values when gestures are received:
private val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
panAndZoom.scaleFactor *= detector.scaleFactor
panAndZoom.focusX = detector.focusX
panAndZoom.focusY = detector.focusY
invalidate()
return true
}
}
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
panAndZoom.panX -= distanceX
panAndZoom.panY -= distanceY
invalidate()
return true
}
}
I override two methods in my custom view, one to pass touch events to my gesture detectors and one to draw child view:
override fun onTouchEvent(e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
scaleGestureDetector.onTouchEvent(e)
return super.onTouchEvent(e)
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
val save = canvas.save()
canvas.scale(panAndZoom.scaleFactor, panAndZoom.scaleFactor, panAndZoom.focusX, panAndZoom.focusY)
canvas.translate(panAndZoom.panX, panAndZoom.panY)
val result = super.drawChild(canvas, child, drawingTime)
canvas.restoreToCount(save)
return result
}
However this doesn't give the expected result, namely:
I am able to overscroll outside the RV altogether (I realise I'm not restricting my pan values, but how do I do that)?
Pan seems to be going very fast when zoomed in (how do I scale my pan values so that they respect the scale factor)?
If I am on page 2 of the document, and I zoom in, then pan to page 3, the layout manager doesn't realise that the view was actually scrolled (meaning, calling findFirstVisibleItem() will return the second page when it isn't actually visible.
Most likely related to the point above: I can pan past a page, and because the layout manager doesn't realise that another view is now displayed it doesn't render it (because it still thinks that view is hidden from view and so it optimises by not rendering it).
I have two packages with one that is the primary package with the MainActivity. The other package gets imported into the MainActivity. The other package deals with drawing on a canvas. However, any changes that I am making to the imported package are not showing up on the actual app. For example, when i change the size of the rectangle being drawn, nothing is actually changing. Does anyone know why this is?
Sorry if this is a trivial question, I just started learning android dev. Also, I got the code off of github, and i'm trying to learn from it. If there is any information that I left out that is crucial please ask. Thank
The code is from: https://github.com/husaynhakeem/android-face-detector
The specific part that I had tried to change is the ANCHOR_RADIUS, ID_OFFSET, as well as the xOffset, yOffset values in the FaceBoundsOverlay. Each variable affects some boundary that is supposed to be used to draw the rectangle and center dot. I pasted the drawAnchor method down below. In that method, when I changed the ANCHOR_RADIUS to 50f, I expected the dot to become larger but nothing was changed.
The FaceBoundsOverlay is in the package called facedetector while the MainActivity is in the package called facedetectorapp
class FaceBoundsOverlay #JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(ctx, attrs, defStyleAttr) {
.....
private fun drawAnchor(canvas: Canvas, centerX: Float, centerY:
Float) { private fun drawAnchor(canvas: Canvas, centerX: Float,
centerY: Float) {
canvas.drawCircle(
centerX,
centerY,
ANCHOR_RADIUS,
anchorPaint)
}
companion object {
private const val ANCHOR_RADIUS = 10f
// private const val ANCHOR_RADIUS = 50f
private const val ID_OFFSET = 50f
}
}