Calling Invalidate from MainActivity doesn't execute OnDraw - android

So i tried out kotlin in android and wanted to begin with painting a circle on a view, where i get a touch input. It worked fine, when i had the onTouchEvent in the View-Class, but i wanted to put the logic outside of the graphic part, so i made a method in the view class, which changes the coordinates of the circle and calls invalidate(). Now i put the onTouchEvent in to the MainActivity and called the method with the new coordinates. The methods gets called as i can see by system.out.println(), but invalidate() doesn't calls on Draw() so the circle doesn't change. I think there is a simple solution for that problem, but i can't find it. So i would be very happy to get some help.
Here's my code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(GameView(this))
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
GameView(this).updateCircle(event!!.x,event!!.y)
return true
}
}
MainActivity.kt
class GameView(context: Context): View(context) {
val paint : Paint
var circleX : Float
var circleY : Float
init {
paint = Paint();
paint.isFilterBitmap = true;
paint.isAntiAlias = true;
paint.color = Color.YELLOW;
circleX = 100f;
circleY = 100f;
}
override fun onDraw(canvas: Canvas?) {
System.out.println("Gets Called")
super.onDraw(canvas)
canvas?.drawColor(Color.WHITE);
canvas?.drawCircle(circleX,circleY,50f,paint)
}
fun drawGrid(height: Int,width: Int){
}
fun updateCircle(x:Float,y:Float){
circleX = x
circleY = y
invalidate()
System.out.println(circleX)
}
}
GameView.kt

Related

Visible thin white slits between elements in RecyclerView

I'm creating a pixel art editor application with Android Studio using Kotlin. And - for this - I've decided to create a RecyclerView with a grid layout adapter which contains a custom View called a Pixel.
Whenever a Pixel is pressed, the colour turns black.
Here is the code:
Canvas Fragment:
package com.realtomjoney.pyxlmoose
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import com.realtomjoney.pyxlmoose.databinding.FragmentCanvasBinding
class CanvasFragment : Fragment() {
private var _binding: FragmentCanvasBinding? = null
private val binding get() = _binding!!
private lateinit var caller: CanvasFragmentListener
companion object {
fun newInstance(): CanvasFragment {
return CanvasFragment()
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is CanvasFragmentListener) {
caller = context
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCanvasBinding.inflate(inflater, container, false)
setUpRecyclerView()
return binding.root
}
private fun setUpRecyclerView() {
val context = activity as Context
binding.canvasRecyclerView.layoutManager = GridLayoutManager(context, 25)
val pixels = caller.initPixels()
binding.canvasRecyclerView.adapter = CanvasRecyclerAdapter(pixels, caller)
binding.canvasRecyclerView.suppressLayout(true)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Recycler Adapter:
class CanvasRecyclerAdapter(private val pixels: List<Pixel>,
private val caller: CanvasFragmentListener) :
RecyclerView.Adapter<RecyclerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val currentPixel = pixels[position]
holder.tileParent.addView(currentPixel)
holder.tileParent.setOnClickListener {
caller.onPixelTapped(currentPixel)
}
}
override fun getItemCount() = pixels.size
}
And ViewHolder:
class RecyclerViewHolder(inflater: LayoutInflater, parent: ViewGroup)
: RecyclerView.ViewHolder(inflater.inflate(R.layout.pixel_layout, parent, false)) {
val tileParent: SquareFrameLayout = itemView.findViewById(R.id.pixelParent)
}
Canvas Activity:
class CanvasActivity : AppCompatActivity(), CanvasFragmentListener {
private lateinit var binding: ActivityCanvasBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setBindings()
setUpFragment()
}
private fun setUpFragment() {
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentHost, CanvasFragment.newInstance()).commit()
}
private fun setBindings() {
binding = ActivityCanvasBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun initPixels(): List<Pixel> {
val list = mutableListOf<Pixel>()
for (i in 1..625) {
list.add(Pixel(this))
}
return list.toList();
}
override fun onPixelTapped(pixel: Pixel) {
pixel.setBackgroundColor(Color.BLACK)
}
}
Pixel:
class Pixel : View {
constructor(context: Context) : super(context)
constructor(context: Context, attributes: AttributeSet) : super(context, attributes)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CanvasFragment"
android:id="#+id/fragmentHost">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/canvasRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Now, I understand this may not be the best approach for this, but that is besides the point.
The point is that when I run the app I get these visible thin white slits between each pixel:
Sometimes only one column has the issue:
In fact most of the time it's one column that does and another that doesn't:
Regardless of the grid size, I still see this visible annoyance.
Now, I am not sure if it's a rendering issue with my EMU - but it doesn't seem to be the case.
This is NOT an EMU issue, my friend installed the APK and sent a screenshot of his phone and it was still visible:
(Picture of friend's phone.)
This doesn't directly answer your question, but here's how you could write a single View class that displays pixel art. Canvas is not very intimidating if you are only drawing rectangles.
This class doesn't enforce itself to be square, but you can do that using your layout constraints. If it's a view in a ConstraintLayout, you could use app:layout_constraintDimensionRatio="w,1:1" for this, or whatever ratio matches your ratio of horizontal and vertical pixel counts (if there isn't padding).
Drawing does create Set copies, but you could change it to using a MutableSet if performance is a problem. Or an alternate strategy could be to use a 2D array of Booleans (or Int colors) so you don't even need a Pixel class.
If you were going to support color, you could add a color property to the Pixel class and then you would change the color of the paint for each pixel inside the loop in onDraw.
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
data class Pixel(val x: Int, val y: Int)
class PixelArtView(context: Context, attrs: AttributeSet) : View(context, attrs) {
var pixels: Set<Pixel> = emptySet()
set(value) {
if (field != value) invalidate()
field = value
}
var horizontalPixels: Int = 10
set(value) {
field = value
invalidate()
}
var verticalPixels: Int = 10
set(value) {
field = value
invalidate()
}
private val pixelWidth: Float
get() = (width - paddingLeft - paddingRight).toFloat() / horizontalPixels
private val pixelHeight: Float
get() = (height - paddingTop - paddingBottom).toFloat() / verticalPixels
var isInteractive = true
private var isErasing = false
private val paint = Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
}
init {
// So we can see something in the layout editor
if (isInEditMode) pixels = List(10) { Pixel(it, it) }.toSet()
}
override fun onDraw(canvas: Canvas) {
val pixelWidth = pixelWidth
val pixelHeight = pixelHeight
for (pixel in pixels) {
val left = paddingLeft + pixel.x * pixelWidth
val top = paddingTop + pixel.y * pixelHeight
canvas.drawRect(left, top, left + pixelWidth, top + pixelHeight, paint)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (isInteractive) {
val touchDown = event.actionMasked == MotionEvent.ACTION_DOWN
val touchMove = event.actionMasked == MotionEvent.ACTION_MOVE
if (touchDown || touchMove) {
val pixel = Pixel(
((event.x - paddingLeft) / pixelWidth).toInt().coerceIn(0, horizontalPixels - 1),
((event.y - paddingTop) / pixelHeight).toInt().coerceIn(0, verticalPixels - 1)
)
if (touchDown) {
isErasing = pixel in pixels
}
pixels = if (isErasing) pixels - pixel else pixels + pixel
return true
}
}
return super.dispatchTouchEvent(event)
}
}
As you guys had mentioned in the comments, the custom View class called Pixel contains the code which makes sure the width and height are the same:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
I think as you guys pointed out, removing this code fixed the problem for me.
Since the onMeasure function is removed, the class Pixel is redundant, so I will switch it to a regular View class in the future.
Right now it looks like so, as you can see, no slits are visible:
If anyone is facing a similar niche problem like this, I would recommend removing the 'onMeasure()' with the setMeasuredDimensions function (if you have one similar to mine), the RecyclerView automatically makes sure the width and height are equal so it's redundant and is the root of many problems.
If anyone wants to contribute to the code, as I had seen some of you request, here is the link:
https://github.com/realtomjoney/PyxlMoose
I think I will be sticking with RecyclerView for now, as I disagree with the notion that Canvas is easier, it actually seems to be the opposite of the case from the code I've seen. But thanks anyways.

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.
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.
You can achieve it also by using setBackgroundResource(..):
binding.myImageView.setBackgroundResource(R.drawable.ic_launcher_foreground)
So your MainActivity would look like:
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.myImageView.setBackgroundResource(R.drawable.ic_launcher_foreground)
}
}
Using a purple draw color the result would be something like this after drawing a bit and using ic_launcher_foreground as background image:
Don't forget, as suggested by #Pavneet_Singh:
call super on the onDraw method you override (as in
super.onDraw(canvas))

How to dismiss Bottom Sheet fragment when click outside in Kotlin?

I make bottom sheet fragment like this:
val bottomSheet = PictureBottomSheetFragment(fragment)
bottomSheet.isCancelable = true
bottomSheet.setListener(pictureListener)
bottomSheet.show(ac.supportFragmentManager, "PictureBottomSheetFragment")
But its not dismiss when I touch outside. and dismiss or isCancelable not working.
try this
behavior.setState(BottomSheetBehavior.STATE_HIDDEN));
You can override method and indicate, for example, in onViewCreated what you need:
class ModalDialogSuccsesDataPatient : ModalDialog() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false //or true
}
}
Let's try to design reusable functions to solve this problem and similar ones if the need arises.
We can create extension functions on View that tell whether a point on the screen is contained within the View or not.
fun View.containsPoint(rawX: Int, rawY: Int): Boolean {
val rect = Rect()
this.getGlobalVisibleRect(rect)
return rect.contains(rawX, rawY)
}
fun View.doesNotContainPoint(rawX: Int, rawY: Int) = !containsPoint(rawX, rawY)
Now we can override the dispatchTouchEvent(event: MotionEvent) method of Activity to know where exactly the user clicked on the screen.
private const val SCROLL_THRESHOLD = 10F // To filter out scroll gestures from clicks
private var downX = 0F
private var downY = 0F
private var isClick = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
isClick = true
}
MotionEvent.ACTION_MOVE -> {
val xThreshCrossed = abs(downX - event.x) > SCROLL_THRESHOLD
val yThreshCrossed = abs(downY - event.y) > SCROLL_THRESHOLD
if (isClick and (xThreshCrossed or yThreshCrossed)) {
isClick = false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (isClick) onScreenClick(event.rawX, event.rawY)
}
else -> { }
}
return super.dispatchTouchEvent(event)
}
private fun onScreenClick(rawX: Float, rawY: Float) { }
Now, you can simply use the above-defined functions to achieve the required result
private fun onScreenClick(rawX: Float, rawY: Float) {
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}
What more? If you have a BaseActivity which is extended by all your Activities then you can add the click detection code to it. You can make the onScreenClick an protected open method so that it can be overridden by the sub-classes.
protected open fun onScreenClick(rawX: Float, rawY: Float) { }
Usage:
override fun onScreenClick(rawX: Float, rawY: Float) {
super.onScreenClick(rawX, rawY)
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}

Why is MotionEvent.getx() not the same result in onTouchEvent and onDraw?

private lateinit var lastEvent: MotionEvent
override fun onDraw(canvas: Canvas?) {
...
Log.d("demo","ondraw lastEvent.x="+lastEvent.x+" lastEvent.y="+lastEvent.y)
Log.d("demo","ondraw lastEvent.rawX="+lastEvent.rawX+" lastEvent.rawY="+lastEvent.rawY)
...
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event.action) {
MotionEvent.ACTION_DOWN -> {
return true
}
MotionEvent.ACTION_MOVE -> {
lastEvent = event
Log.d("demo","onTouchEvent lastEvent.x="+lastEvent.x+" lastEvent.y="+lastEvent.y)
}
else -> {
}
}
...
}
Why the lastEvent.x get different results。This problem is bothering me.
Place help me, thanks.
Most probably, the method onDraw() is not being called as soon as you touch the view.
Add a call to invalidate() at the end of the method onTouchEvent() so that it will invoke the method onDraw() internally and draw at the same location.

Can't change variable in Event

Try to detect swipe gesture and use code from android docs with liitle changes.
I try to show toast with Y-axis data. I was declared variable in the beginning of MyGestureListener class and try to change it, when onFling method called. I want to show toast with Y-axis data, but always see default string "Def_Nothing". If I add Log.d — I watch, that onFling method working and I get correct data of X and Y axis.
I think don't understood some fundamental of object oriented programming and neen little explanation of it.
class MainActivity : AppCompatActivity() {
private lateinit var mDetector: GestureDetectorCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mDetector = GestureDetectorCompat(this, MyGestureListener())
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
this.mDetector.onTouchEvent(event)
val myGestureListener = MyGestureListener()
Toast.makeText(this, myGestureListener.currentGesture, Toast.LENGTH_LONG).show()
return super.onTouchEvent(event)
}
class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
var currentGesture: String = "Def_Nothing"
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
currentGesture = (e2!!.y - e1!!.y).toString()
return true
}
}
}
You have to keep a reference to your gesture listener, you cant just create a new one every time. See the example code below.
class MainActivity : AppCompatActivity() {
private lateinit var mDetector: GestureDetectorCompat
private lateinit var mGestureListener: MyGestureListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mGestureListener = MyGestureListener()
mDetector = GestureDetectorCompat(this, mGestureListener)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
this.mDetector.onTouchEvent(event)
Toast.makeText(this, mGestureListener.currentGesture, Toast.LENGTH_LONG).show()
return super.onTouchEvent(event)
}
class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
...
}
}

Categories

Resources