Visible thin white slits between elements in RecyclerView - android

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.

Related

MotionLayout not recyclable as a child of RecyclerView

i try to implement programmatically version of MotionLayout by extending it. And i have a base activity ayout using RecyclerView.
However, when i add my motion layout as an item of the RecyclerView, the view is not recycled when i try to scrolling up and down.
And it works well when i use as a normal view (act as single view).
Here is the preview:
class SimpleMotionLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {
private val motionScene = MotionScene(this)
private var _simpleTransition: MotionScene.Transition? = null
private lateinit var squareView: View
init {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
initDefaultConstraint(this)
setMotion()
}
fun setMotion() {
_simpleTransition = createPlaceholderTransition(motionScene)
setDebugMode(DEBUG_SHOW_PATH)
/**
* The order matters here.
* [MotionScene.addTransition] adds the transition to the scene while
* [MotionScene.setTransition] sets the transition to be the current transition.
*/
motionScene.addTransition(_simpleTransition)
motionScene.setTransition(_simpleTransition)
scene = motionScene
setTransition(_simpleTransition!!.id)
animateView()
}
fun setSquareColor(color: Int) {
squareView.setBackgroundColor(color)
}
fun initDefaultConstraint(motionLayout: ConstraintLayout) {
// View
squareView = View(context).apply {
id = R.id.default_button
setBackgroundColor(Color.BLACK)
}
motionLayout.addView(
squareView,
LayoutParams(
fromDp(context, 52),
fromDp(context, 52)
)
)
val set = ConstraintSet()
set.clone(motionLayout)
// Setup constraint set to TOP, LEFT to the Parent
set.connect(
squareView.id,
TOP,
PARENT_ID,
TOP
)
set.connect(
squareView.id,
START,
PARENT_ID,
START
)
set.applyTo(motionLayout)
}
private fun setToEnd() {
val endSet = getConstraintSet(_simpleTransition?.endConstraintSetId ?: return)
endSet.clear(R.id.default_button, START)
endSet.connect(
R.id.default_button,
END,
PARENT_ID,
END
)
}
fun animateView() {
setToEnd()
_simpleTransition?.setOnSwipe(
OnSwipe().apply {
dragDirection = DRAG_END
touchAnchorId = R.id.default_button
touchAnchorSide = SIDE_START
onTouchUp = ON_UP_AUTOCOMPLETE_TO_START
setMaxAcceleration(500)
}
)
setTransition(_simpleTransition!!.id)
}
// Placeholder transition??
fun createPlaceholderTransition(motionScene: MotionScene): MotionScene.Transition? {
val startSetId = View.generateViewId()
val startSet = ConstraintSet()
startSet.clone(this)
val endSetId = View.generateViewId()
val endSet = ConstraintSet()
endSet.clone(this)
val transitionId = View.generateViewId()
return TransitionBuilder.buildTransition(
motionScene,
transitionId,
startSetId, startSet,
endSetId, endSet
)
}
/**
* Get px from dp
*/
private fun fromDp(context: Context, inDp: Int): Int {
val scale = context.resources.displayMetrics.density
return (inDp * scale).toInt()
}
}
Below is my adapter:
class SimpleMotionLayoutAdapter : RecyclerView.Adapter<SimpleMotionLayoutAdapter.ViewHolder>() {
val items = mutableListOf<Int>() // colors
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun setColor(color: Int) {
(view as SimpleMotionLayout).setSquareColor(color)
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = SimpleMotionLayout(parent.context)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setColor(items[position])
}
override fun getItemCount(): Int = items.size
companion object {
const val TYPE_NORMAL = 0
const val TYPE_EXCEPTIONAL = 1
}
}
Am i missing implementation?
Thank you
In general you need to cache and restore the state of the MotionLayout when it gets Recycled.
Right now in onBindViewHolder you only set the Color.
Remember RecyclerView keeps a only a screens worth (+ about 3) of ViewHolders and reuses them using onBindViewHolder
At minimum you need to set the Progress of the MotionLayout.
Due to differences in timing you may need to set the progress in an onPost

How to make a Recycler View scroll slow and smooth when the user takes hands off the screen?

This is how the current implementation when I scroll fast.
With in second the whole items in the recycler view are showed till the end.. What I am trying to achieve is something like the one shown below (live demo: recycler views in play store),
even if we scroll fast it shows 1 or 2 items so it feels natural. How can I make the recycler view behave this way?
The code:
Adapter
class CustomAdapter(private val mList: List<ItemsViewModel>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.card_view_design, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
}
override fun getItemCount(): Int {
return mList.size
}
class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
val imageView: ImageView = itemView.findViewById(R.id.imageview)
val textView: TextView = itemView.findViewById(R.id.textView)
}
}
Implementation
val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)
recyclerview.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
val data = ArrayList<ItemsViewModel>()
val adapter = CustomAdapter(data)
recyclerview.adapter = adapter
When a RecyclerView is scrolled horizontally, the scrollHorizontallyBy() is called several times with dx values that can make the scrolling behave smooth and real according to the fling (push) value of the finger.
The same for vertical RecyclerView, scrollVerticallyBy() gets called with different dy values.
So, you can manipulate this dx value to have the desired behavior; in your case you need to get smaller values of dx.
You'd think of a fixed dx value, but that won't be that natural; instead you can divide it by a fixed amount to scale down the same dx values.
Here I'm using an arbitrary value that you can manipulate it as you need.
So, what you need is to use a custom LinearLayoutManager & override scrollHorizontallyBy() as you use a horizontal recyclerView:
class MyLinearLayoutManager(context: Context?, orientation: Int, reverseLayout: Boolean) :
LinearLayoutManager(context, orientation, reverseLayout) {
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?
): Int {
var newDx = (dx / 1.2).toInt() // 1.2 is an arbitrary value
if (newDx == 0 && dx != 0) { // To have no 0 values (Optionally)
newDx = 1
}
return super.scrollHorizontallyBy(newDx, recycler, state)
}
}

RecyclerView's ViewHolders becoming unclickable

Prehisrory
I have a list of stocks (some objects) from RoomDB. Each one of them have "symbol", "name",
"price" and what's most important, "isFavourite" fields.
I made a ViewPager2 with two Fragments containing RecyclerView (actually there are just two instances of one class StocksFragment - one for all stocks, one for only favourite stocks). Each stock in RecyclerView is connected to the repository through Obsrver (data changes => stock's ViewHolder changes). Also each ViewHolder has own checkBox that changes "isFavourite" Stock field through StockListViewModel that calls StockRepository, that works directly with roomDB (with kotlin coroutines - sth like
fun getStocks(): LiveData<List<Stock>> = runBlocking{ stockDao.getStocks() })
Problem
When i click the same checkBox several times in a relatively small amount of time, all RecyclerView's ViewHolders become unclickable (neither the delete button nor the checkbox works). But i still can scroll RecyclerView How can i fix that?
I think i am doing something very inefficient but i dont know what.
Here is my StocksFragment code:
package com.nikitakrapo.android.happystocks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class StocksFragment(var stockListType: StockListType) : Fragment() {
private val stockListViewModel: StockListViewModel by lazy{
ViewModelProvider(this).get(StockListViewModel::class.java)
}
private lateinit var stocksRecyclerView: RecyclerView
private var adapter: StocksAdapter? = StocksAdapter()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_stocks, container, false)
stocksRecyclerView = view.findViewById(R.id.recycler_view)
stocksRecyclerView.layoutManager = LinearLayoutManager(context)
stocksRecyclerView.setHasFixedSize(true)
stocksRecyclerView.adapter = adapter
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var stocks = if (stockListType == StockListType.favouriteStocksList)
stockListViewModel.favStockListLiveData
else stockListViewModel.stockListLiveData
stocks.observe(
viewLifecycleOwner,
{ stocks ->
stocks?.let {
adapter?.setStocks(stocks)
}
}
)
}
private inner class StockHolder(view: View) : RecyclerView.ViewHolder(view){
private lateinit var stock: Stock
val symbolTextView: TextView = itemView.findViewById(R.id.stock_symbol)
private val nameTextView: TextView = itemView.findViewById(R.id.stock_name)
private val priceTextView: TextView = itemView.findViewById(R.id.stock_price)
private val stockImageView: ImageView = itemView.findViewById(R.id.stock_image)
val stockDeleteButton: Button = itemView.findViewById(R.id.stock_delete)
val favouriteCheckBox: CheckBox = itemView.findViewById(R.id.is_favourite)
fun bind(stock: Stock, holder: StockHolder) {
this.stock = stock
symbolTextView.text = this.stock.symbol
nameTextView.text = this.stock.name
priceTextView.text = "$" + this.stock.priceUSD.toString()
favouriteCheckBox.isChecked = this.stock.isFavourite
holder.stockDeleteButton.setOnClickListener {
stockListViewModel.deleteStock(stock)
}
holder.favouriteCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
stockListViewModel.updateFavourite(holder.symbolTextView.text.toString(), isChecked)
}
}
}
private inner class StocksAdapter
: RecyclerView.Adapter<StockHolder>() {
private var stockList: List<Stock> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
: StockHolder {
val view = layoutInflater.inflate(R.layout.list_item_stock, parent, false)
return StockHolder(view)
}
override fun onBindViewHolder(holder: StockHolder, position: Int) {
val stock = stockList[position]
holder.bind(stock, holder)
}
public fun setStocks(stockList: List<Stock>){
this.stockList = stockList
notifyDataSetChanged()
}
override fun getItemCount() = stockList.size
}
}
Try OnClickListener instead
holder.favouriteCheckBox.setOnClickListener {
if ((it as CompoundButton).isChecked) {
stockListViewModel.updateFavourite(holder.symbolTextView.text.toString(), (it as CompoundButton).isChecked)
}
}
And provide stockListViewModel.updateFavourite code block. Is the problem reproduced when this line is commented out?
Calling stockListViewModel.deleteStock(stock) and stockListViewModel.updateFavourite(holder.symbolTextView.text.toString(), isChecked) in background thread like
withContext(Dispatchers.IO){stockListViewModel.deleteStock(stock)} might resolve the issue.

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

RecycleView list items not appearing

I've been trying to make RecyclerView work in my app where the list items are ImageViews, and the images get downloaded and put inside (asynchronously) in the onBindViewHolder method. I'm not facing any errors in my code, but for some reason
only the list items which will be visible (even partially) to the user
when the activity loads, have images loaded into them.
Though I can't see the images, I observed that the height and width of these items have been allocated correctly. And since the images get downloaded first, and then the ImageView's dimensions are determined I figure that the problem has got something to do with RecyclerView itself? If someone can shed some light on this, it would be great. Thanks.
I would also like to add, that if the Activity is paused and then resumed (by clicking on the "square" navigation button and then resuming it), the images of all the list items load correctly.
Pic #1
Pic #2
Here's my code:
onCreate method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerView {
id = ViewID.ID_LIST
}
val imgList = ArrayList<ImageView>()
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
val lv = findViewById(ViewID.ID_LIST) as RecyclerView
lv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
lv.adapter = ImageRecyclerAdapter(ctx, imgList)
}
The RecyclerView.Adapter class:
private class ImageRecyclerAdapter(val context: Context, val imageList: ArrayList<ImageView>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onViewRecycled(holder: RecyclerView.ViewHolder?) {
super.onViewRecycled(holder)
if (holder != null) {
val v = holder.itemView as ImageView
v.setImageBitmap(null)
}
}
override fun onBindViewHolder(p: RecyclerView.ViewHolder, position: Int) {
val v = p.itemView as ImageView
Ion.with(v)
.load("https://pbs.twimg.com/profile_images/616076655547682816/6gMRtQyY.jpg")
.setCallback({ exception, t ->
if (t != null) {
val dm = Point()
context.windowManager.defaultDisplay.getRealSize(dm)
val w = t.maxWidth
val h = t.maxHeight
val params = t.layoutParams
if (params != null) {
params.width = dm.x
params.height = (dm.x * (h.toDouble() / w.toDouble())).toInt()
t.layoutParams = params
t.requestLayout()
}
}
})
}
override fun getItemCount(): Int {
return imageList.size
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val v = ImageView(context)
return object : RecyclerView.ViewHolder(v) {}
}
}
It worked after I made the binding of data into a Synchronous request, and I moved the ImageView manipulation (changing LayoutParams) into the onViewAttachedToWindow overridden method of my adapter.
onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder?) {
super.onViewAttachedToWindow(holder)
val t = holder?.itemView as ImageView
val dm = Point()
context.windowManager.defaultDisplay.getRealSize(dm)
val w = t.maxWidth
val h = t.maxHeight
val params = t.layoutParams
if (params != null) {
params.width = dm.x
params.height = (dm.x * (h.toDouble() / w.toDouble())).toInt()
t.layoutParams = params
t.requestLayout()
}
}
onBindViewHolder:
override fun onBindViewHolder(p: RecyclerView.ViewHolder, position: Int) {
val v = p.itemView as ImageView
Ion.with(v)
.load(imageList[position].toString())
.tryGet()
}

Categories

Resources