androidandroid-imageviewandroid-canvasandroid-custom-viewandroid-paint

Can not set image resource of a custom imageview in Android


I have created a custom view project based on the lessons I have learned in this and this codelab. In my project, I tried to draw not just on a view but on a custom ImageView. Therefore, I have created a custom ImageView and did all the steps as in the mentioned official codelabs. Here is my custom ImageView class:

// Stroke width for the the paint.
private const val STROKE_WIDTH = 12f

class MyImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    private var path = Path()
    private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)

    private lateinit var extraCanvas : Canvas
    private lateinit var extraBitmap : Bitmap

    private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

    private var currentX = 0f
    private var currentY = 0f

    private var motionTouchEventX = 0f
    private var motionTouchEventY = 0f

    // Set up the paint with which to draw.
    private val paint = Paint().apply {
        color = drawColor
        // Smooths out edges of what is drawn without affecting shape.
        isAntiAlias = true
        // Dithering affects how colors with higher-precision than the device are down-sampled.
        isDither = true
        style = Paint.Style.STROKE // default: FILL
        strokeJoin = Paint.Join.ROUND // default: MITER
        strokeCap = Paint.Cap.ROUND // default: BUTT
        strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
    }

    init{
        init()
    }

    private fun init(){


        setOnTouchListener(OnTouchListener { _, event ->
            event?.let {
                motionTouchEventX = it.x
                motionTouchEventY = it.y

                when(it.action){
                    MotionEvent.ACTION_DOWN -> touchStart()
                    MotionEvent.ACTION_MOVE -> touchMove()
                    MotionEvent.ACTION_UP -> touchUp()
                }
                return@OnTouchListener true
            }
            false
        })
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.drawBitmap(extraBitmap, 0f, 0f, null)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        if (::extraBitmap.isInitialized) extraBitmap.recycle()
        extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        extraCanvas = Canvas(extraBitmap)
    }


    private fun touchStart() {
        path.reset()
        path.moveTo(motionTouchEventX, motionTouchEventY)
        currentX = motionTouchEventX
        currentY = motionTouchEventY
    }

    private fun touchMove() {
        val dx = Math.abs(motionTouchEventX - currentX)
        val dy = Math.abs(motionTouchEventY - currentY)
        if (dx >= touchTolerance || dy >= touchTolerance) {
            // QuadTo() adds a quadratic bezier from the last point,
            // approaching control point (x1,y1), and ending at (x2,y2).
            path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
            currentX = motionTouchEventX
            currentY = motionTouchEventY
            // Draw the path in the extra bitmap to save it.
            extraCanvas.drawPath(path, paint)
        }
        // Invalidate() is inside the touchMove() under ACTION_MOVE because there are many other
        // types of motion events passed into this listener, and we don't want to invalidate the
        // view for those.
        invalidate()
    }

    private fun touchUp() {
        // Reset the path so it doesn't get drawn again.
        path.reset()
    }
}

This is how my XML layout of the MainActivity looks like (in the codelab there was no one, they just used the custom view programmatically):

<layout 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">


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <com.celik.abdullah.drawingonimageview.MyImageView
            android:id="@+id/myImageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And here is my MainActivity.kt class:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.myImageView.setImageResource(R.drawable.ic_launcher_foreground)
    }
}

My problem is: the image resource is not shown on which I want to draw. The drawing part works like in the codelabs. But the R.drawable.ic_launcher_foreground drawable which I wanted to use just for testing purposes is not shown on the screen. WHY ?

I hope somebody can help.


Solution

  • In the custom view, the canvas is only being used to draw the extraBitmap which is being updated by extraCanvas so MyImageView is only handling the drawing of extraBitmap.

    setImageResource belongs to ImageView class which is internally handling the conversion of resource to drawable and drawing it on the canvas inside it's onDraw() but onDraw is being overridden by the custom view which is not handling the drawing of received bitmap so the solution is call super.onDraw as:

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        // ^^^^^^^^^^^^^^^^ add this to execute imageview's onDraw for image handling
        canvas?.drawBitmap(extraBitmap, 0f, 0f, null)
    }
    

    Alternately, you can override setImageResource and add the code to draw received resource via setImageResource method on the canvas inside onDraw of MyImageView.