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!
Say I have a Custom View built from scratch that looks like this:
class CustomTextView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
textSize = 48f
color = Color.BLUE
strokeWidth = 3f
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawText("Text from Custom view", width / 2f, height / 2f, paint)
}
}
This is very simple drawing Text on Canvas. And in a fragment layout, I add a TextView and my CustomText view like the following:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text from Text View" />
<com.example.testing.views.CustomTextView
android:layout_width="250dp"
android:layout_height="32dp"
android:layout_marginTop="10dp" />
</LinearLayout
My espresso test file looks like:
#RunWith(AndroidJUnit4::class)
class MyFragmentTest {
private lateinit var scenario: FragmentScenario<MyFragment>
#Before
fun setup() {
scenario = launchFragmentInContainer(themeResId = R.style.Theme_Testing)
scenario.moveToState(Lifecycle.State.STARTED)
}
#Test
fun testNormalTextView() { // -> PASSED
onView(withText("Text from Text View")).check(matches(isDisplayed()))
}
#Test
fun testCustomTextView() { // -> FAILED NoMatchingView Exception
onView(withText("Text from Custom View")).check(matches(isDisplayed()))
}
}
When I run the tests on my physical device, it passes only testNormalTextView but it fails on testCustomTextView. How do I make these Espresso test pass with Custom Views?
From the official docs, withText() viewMatcher works with Textviews.
Returns a matcher that matches TextView based on its text property value.
In your case your custom view is extending View class.
Following are two ways which i will suggest.
Make your custom view extend TextView. [If your requirement is to access only the view with the specific text regardless of it's id]
Use withId() viewMatcher instead of withText(), passing id of your customview given in xml layout. You need to give id to your custom view in xml. [If you want to check view with specific id, not with the text it holds]
In your xml
<com.example.testing.views.CustomTextView
android:id="#+id/my_custom_view"
android:layout_width="250dp"
android:layout_height="32dp"
android:layout_marginTop="10dp" />
In your testFunction
#Test
fun testCustomTextView() {
onView(withId(R.id.my_custom_view)).check(matches(isDisplayed()))
}
Update:
For recyclerview, you can use onData() instead of onView() passing matcher in argument.
You can find further info about testing adapterViews here
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 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
}
}
I know we can draw a vector drawable like this on canvas:
vectorDrawable.setBounds(left, top, right, bottom);
vectorDrawable.draw(canvas);
but Is it possible to use animatedVectorDrawables in custom view?
Yes, it is possible, and looks like the snippet you have would work. Be sure to set bounds and call the start() method on the drawable to start the animation.
class MyCustomView : View {
lateinit var animVectDrawable: AnimatedVectorDrawable
fun startAnim() {
animVectDrawable.setBounds(left, top, right, bottom)
animVectDrawable.start()
shouldDrawAnim = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (shouldDrawAnim) {
animVectDrawable.draw(canvas)
}
}
}