RecyclerView 中元素之间可见的白色细缝
Visible thin white slits between elements in RecyclerView
我正在使用 Android Studio 使用 Kotlin 创建一个像素艺术编辑器应用程序。并且 - 为此 - 我决定创建一个带有网格布局适配器的 RecyclerView,其中包含一个名为 Pixel 的自定义视图。
每当按下像素时,颜色变为黑色。
代码如下:
Canvas 片段:
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
}
}
回收适配器:
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
}
和 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)
}
}
像素:
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>
现在,我明白这可能不是最好的方法,但这不是重点。
重点是,当我 运行 应用程序时,我在每个像素之间看到这些可见的白色细缝:
有时只有一列有问题:
事实上,大多数时候是一栏有效而另一栏无效:
无论网格大小如何,我仍然看到这种明显的烦恼。
现在,我不确定这是否是我的 EMU 的渲染问题 - 但似乎并非如此。
这不是 EMU 问题,我的朋友安装了 APK 并发送了他的 phone 的屏幕截图,它仍然可见:
(朋友的照片phone。)
这不会直接回答您的问题,但您可以通过以下方式编写显示像素艺术的单个视图 class。 Canvas 如果您只绘制矩形,则不是很吓人。
此 class 不会强制自己为正方形,但您可以使用布局约束来做到这一点。如果它是 ConstraintLayout 中的视图,您可以为此使用 app:layout_constraintDimensionRatio="w,1:1"
,或者任何与您的水平和垂直像素数比率相匹配的比率(如果没有填充)。
绘图确实会创建集合副本,但如果性能有问题,您可以将其更改为使用 MutableSet。或者另一种策略是使用布尔值(或 Int 颜色)的二维数组,这样您甚至不需要像素 class.
如果您要支持颜色,您可以将颜色 属性 添加到像素 class,然后您将更改 [=] 循环中每个像素的颜色12=].
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)
}
}
正如你们在评论中提到的,名为 Pixel 的自定义视图 class 包含确保宽度和高度相同的代码:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
我认为正如你们指出的那样,删除这段代码对我来说解决了问题。
由于删除了onMeasure
功能,classPixel
是多余的,所以我将其切换为常规View
class未来。
现在看起来是这样,如你所见,看不到任何缝隙:
如果有人遇到类似这样的小众问题,我建议使用 setMeasuredDimensions 函数删除 'onMeasure()'(如果你有一个与我的类似的函数),RecyclerView 会自动确保宽度和高度是等于所以它是多余的,是许多问题的根源。
如果有人想为代码做出贡献,正如我看到你们中的一些人所要求的,这里是 link:
https://github.com/realtomjoney/PyxlMoose
我想我现在会坚持使用 RecyclerView,因为我不同意 Canvas 更容易的观点,实际上它似乎与我看到的代码相反。不过还是谢谢了。
我正在使用 Android Studio 使用 Kotlin 创建一个像素艺术编辑器应用程序。并且 - 为此 - 我决定创建一个带有网格布局适配器的 RecyclerView,其中包含一个名为 Pixel 的自定义视图。
每当按下像素时,颜色变为黑色。
代码如下:
Canvas 片段:
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
}
}
回收适配器:
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
}
和 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)
}
}
像素:
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>
现在,我明白这可能不是最好的方法,但这不是重点。
重点是,当我 运行 应用程序时,我在每个像素之间看到这些可见的白色细缝:
有时只有一列有问题:
事实上,大多数时候是一栏有效而另一栏无效:
无论网格大小如何,我仍然看到这种明显的烦恼。
现在,我不确定这是否是我的 EMU 的渲染问题 - 但似乎并非如此。
这不是 EMU 问题,我的朋友安装了 APK 并发送了他的 phone 的屏幕截图,它仍然可见:
(朋友的照片phone。)
这不会直接回答您的问题,但您可以通过以下方式编写显示像素艺术的单个视图 class。 Canvas 如果您只绘制矩形,则不是很吓人。
此 class 不会强制自己为正方形,但您可以使用布局约束来做到这一点。如果它是 ConstraintLayout 中的视图,您可以为此使用 app:layout_constraintDimensionRatio="w,1:1"
,或者任何与您的水平和垂直像素数比率相匹配的比率(如果没有填充)。
绘图确实会创建集合副本,但如果性能有问题,您可以将其更改为使用 MutableSet。或者另一种策略是使用布尔值(或 Int 颜色)的二维数组,这样您甚至不需要像素 class.
如果您要支持颜色,您可以将颜色 属性 添加到像素 class,然后您将更改 [=] 循环中每个像素的颜色12=].
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)
}
}
正如你们在评论中提到的,名为 Pixel 的自定义视图 class 包含确保宽度和高度相同的代码:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
我认为正如你们指出的那样,删除这段代码对我来说解决了问题。
由于删除了onMeasure
功能,classPixel
是多余的,所以我将其切换为常规View
class未来。
现在看起来是这样,如你所见,看不到任何缝隙:
如果有人遇到类似这样的小众问题,我建议使用 setMeasuredDimensions 函数删除 'onMeasure()'(如果你有一个与我的类似的函数),RecyclerView 会自动确保宽度和高度是等于所以它是多余的,是许多问题的根源。
如果有人想为代码做出贡献,正如我看到你们中的一些人所要求的,这里是 link:
https://github.com/realtomjoney/PyxlMoose
我想我现在会坚持使用 RecyclerView,因为我不同意 Canvas 更容易的观点,实际上它似乎与我看到的代码相反。不过还是谢谢了。