Home > database >  Visible thin white slits between elements in RecyclerView
Visible thin white slits between elements in RecyclerView

Time:10-23

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:

enter image description here

Sometimes only one column has the issue:

enter image description here

In fact most of the time it's one column that does and another that doesn't:

enter image description here

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.)

enter image description here

CodePudding user response:

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:

enter image description here

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.

CodePudding user response:

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)
    }
}
  • Related