Everything works great, but one thing is not really working.
The delete icon is only rendered on the first element of the recycler view list as you can see in the image.
Here is my code of the ItemTouchHelper class:
class ItemSwipeCallback(val context: Context) : ItemTouchHelper.Callback() {
private val listeners = ArrayList<OnItemSwipe>()
private val paint = Paint()
val theme = context.themeId
val icon = ContextCompat.getDrawable(context, R.drawable.ic_delete_filled_white_24dp)!!
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
return true
}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val direction = context.sharedPreferences.getInt(Preferences.SWIPE_DIRECTION, Preferences.SWIPE_VALUE_RIGHT)
return when (direction) {
Preferences.SWIPE_VALUE_RIGHT -> makeMovementFlags(0, ItemTouchHelper.RIGHT)
Preferences.SWIPE_VALUE_LEFT -> makeMovementFlags(0, ItemTouchHelper.LEFT)
else -> makeMovementFlags(0, ItemTouchHelper.RIGHT)
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
listeners.forEach { it.onSwiped(viewHolder.layoutPosition, direction) }
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
if (dX != 0f && isCurrentlyActive) {
val itemView = viewHolder.itemView
paint.color = Color.parseColor("#D32F2F")
val top = (itemView.height - icon.intrinsicHeight) / 2
val left = itemView.width - icon.intrinsicWidth - top
if (theme == Preferences.THEME_VALUE_DARK) {
icon.setTint(Color.BLACK)
} else {
icon.setTint(Color.WHITE)
}
if (dX < 0) {
val background = RectF(itemView.right.toFloat() + dX, itemView.top.toFloat(),
itemView.right.toFloat(), itemView.bottom.toFloat())
c.drawRect(background, paint)
icon.setBounds(left, top, left + icon.intrinsicWidth, top + icon.intrinsicHeight)
} else {
val background = RectF(itemView.left.toFloat() + dX, itemView.top.toFloat(),
itemView.left.toFloat(), itemView.bottom.toFloat())
c.drawRect(background, paint)
icon.setBounds(top, top, top + icon.intrinsicWidth, top + icon.intrinsicHeight)
}
icon.draw(c)
}
}
fun addOnItemSwipeListener(onItemSwipe: OnItemSwipe) {
listeners.add(onItemSwipe)
}
}
Maybe the icon which is loaded on the head of the class is only usable once? I tried already to convert it into a Bitmap and use it. I also tried loading it in the onChildDraw function.
The solution was too easy. I always used itemView.height instead of itemView.top.
The canvas includes all items. Not every item has its own canvas. So i have to add the height of the above items too.
The working code looks like this:
val top = itemView.top + (itemView.height - intrinsicHeight) / 2
val left = itemView.width - intrinsicWidth - (itemView.height - intrinsicHeight) / 2
val right = left + intrinsicHeight
val bottom = top + intrinsicHeight
if (dX < 0) {
background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
icon.setBounds(left, top, right, bottom)
} else if (dX > 0) {
background.setBounds(itemView.left + dX.toInt(), itemView.top, itemView.left, itemView.bottom)
icon.setBounds(top, top, top, bottom)
}
background.draw(c)
icon.draw(c)
Did u check the value of this parameter: isCurrentlyActive?
I guess there is no error in image creation(icon). because the image is successfully created first time. So, the problem is in looping.
if (dX < 0) {...}else{...}
Here, No matter the value of dX, the image is gonna added to children(row).
if (dX != 0f && isCurrentlyActive)
This is the only check point(considerably) in your code. Technically the whole block will skipped, if isCurrentlyActive boolean is false.
Related
I know there are already a bunch of questions related to the "trying to use a recycled bitmap" crash, but none helped me.
Details:
There are no calls to Bitmap.recycle() anywhere in this project
All images are loaded using Glide (4.11.0)
Glide calls are all simple, and not using placeholders
The crash seems to happen randomly when switching fragments
Only thing I could think of were the transformations.
There are only 2 transformations in this project.
CircleTransformation (clip image as a circle with a custom radius):
class CircleTransformation(private val radius: Float) : BitmapTransformation() {
companion object {
private const val ID = "com.project.transformation.circle"
private val ID_BYTES: ByteArray = ID.toByteArray()
}
public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val paint = Paint()
paint.isAntiAlias = true
paint.shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
val halfWidth = source.width / 2f
val output = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
canvas.drawCircle(
halfWidth,
(source.height / 2).toFloat(),
halfWidth * radius,
paint
)
return output
}
// Caching helpers
override fun equals(other: Any?): Boolean {
return other is CircleTransformation && other.hashCode() == hashCode()
}
override fun hashCode(): Int {
return ID.hashCode()
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
}
}
And ClipWhiteTransformation (remove white border from image):
class ClipWhiteTransformation() : BitmapTransformation() {
companion object {
private const val ID = "com.project.transformation.clipWhite"
private val ID_BYTES: ByteArray = ID.toByteArray()
// Config
const val white = 253 // White pixel, if all channels are equal or greater than this
const val transparent = 50 // Transparent pixel, if Less than this
}
public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val width = source.width - 1
val height = source.height - 1
val halfX = width / 2
val halfY = height / 2
var startY = 0
// Left Margin
var left = 0
for (x in 0 until halfX) {
val pixel = source.getPixel(x, halfY)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
left = x
if (x > 2) {
startY = 2
}
break
}
}
// Right Margin
var right = 0
for (x in 0 until halfX) {
val pixel = source.getPixel(width - x, halfY)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
right = x
if (x > 2) {
startY = 2
}
break
}
}
// Top Margin
var top = 0
for (y in startY until halfY) {
val pixel = source.getPixel(halfX, y)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
top = y
break
}
}
// Bottom Margin
var bottom = 0
for (y in startY until halfY) {
val pixel = source.getPixel(halfX, height - y)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
bottom = y
break
}
}
// Clip, scale and return
val newWidth = width - (left + right)
val newHeight = height - (top + bottom)
val scale = if (abs(newWidth - outWidth) > abs(newHeight - outHeight)) outWidth / newWidth.toFloat() else outHeight / newHeight.toFloat()
val matrix = Matrix().apply { setScale(scale, scale) }
return Bitmap.createBitmap(source, left, top, newWidth, newHeight, matrix, false)
}
// Caching helpers
override fun equals(other: Any?): Boolean {
return other is ClipWhiteTransformation && other.hashCode() == hashCode()
}
override fun hashCode(): Int {
return ID.hashCode()
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
}
}
Was using the BitmapPool initially, removing it didn't stop the crash.
By the way, this is the extension used to load images:
fun ImageView.setURL(url: String,
#DrawableRes error: Int? = null,
#DrawableRes placeholder: Int? = null,
size: Int? = null,
options: ((RequestBuilder<Drawable>) -> Unit)? = null,
completion: ((resource: Drawable?) -> Unit)? = null) {
// No URL, use Placeholder if exists, if not, use the error image
if (url.isEmpty()) {
placeholder?.also{ setImageResource(it) } ?: run { error?.also{ setImageResource(it) } }
return
}
Glide.with(applicationInstance) // (I'm using an application instance directly here)
.load(url).apply {
completion?.also { completion ->
this.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
completion(null)
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
completion(resource)
return false
}
})
}
}
.apply { size?.also { this.override(it)} }
.apply { options?.invoke(this) }
.placeholder(placeholder ?: 0)
.error(error ?: 0)
.transition(DrawableTransitionOptions.withCrossFade(350))
.into(this)
}
Sorry for pasting so much code here (hope it's useful to someone).
Can these transformations or loader cause the crash?
To shape(circle/square/oval) your image You do not need to Transform your image .
MaterialDesign has introduce ShapeableImageView which let you shape your image at runtime, also you can add border with color .
add matrial dependecies :
implementation 'com.google.android.material:material:1.3.0-alpha01'
Add shapeableImageView in your xyz.xml:
<com.google.android.material.imageview.ShapeableImageView
android:id="#+id/imgStudent"
android:layout_width="100dp"
android:layout_height="100dp"
app:shapeAppearanceOverlay="#style/circleImageView"
android:padding="2dp"
app:strokeColor="#color/white"
app:strokeWidth="5dp"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
tools:srcCompat="#drawable/ic_kid_placeholder"
/>
Add style inside res/values/style.xml file
<style name="circleImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
<item name="android:shadowRadius">100</item>
<item name="android:shadowColor">#color/gray</item>
<item name="backgroundOverlayColorAlpha">12</item>
</style>
And at last load your image with glide .
Setup: I have a RecyclerView with a vertical LinearLayoutManager. Each ViewHolder has 2 TextViews (green and red in the picture), which can be very long. RED can be scrolled horizontally thanks to a HorizontalScrollView. GREEN can be scrolled vertically thanks to a ScrollView.
Now I have implemented ItemTouchHelper, to swipe LEFT, UP or DOWN. The problem is that my ScrollViews don't work anymore. Instead, even when I swipe GREEN UP or DOWN, it is my ViewHolder that moves. How do I prevent my ItemTouchHelper from getting the touch event?
I've tried:
-android:descendantFocusability="blocksDescendants"
-NestedScrollView instead of ScrollView
-implementing setOnTouchListener inside my adapter to return true (the listener is triggered, but the event is not consumed)
-thought about using RecyclerView.findChildViewUnder inside onChildDraw to scroll manually and block the super method but I can't find a way to get the coordinates
Nothing works so far. How do I do this? Here is my ItemTouchHelper:
private fun enableSwipe() {
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
if (direction == ItemTouchHelper.LEFT) {
val content = mAdapter.getContentForPosition(position)
//do stuff...
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val mdX = if (abs(dY) > abs(dX)) {
0f
}else{
dX
}
val mdY = if (abs(dY) > abs(dX)) {
when{
dY > 0 -> if (dY > 500f) 500f else dY
else -> if (dY < -500f) -500f else dY
}
}else{
0f
}
//override viewHolder in case we swipe up or down on the other content
val mViewHolder: RecyclerView.ViewHolder = if (abs(dY) > abs(dX)) {
if (dY < 0) {
//swiping up
getViewHolder(1) ?: viewHolder
} else {
getViewHolder(0) ?: viewHolder
}
}else {
viewHolder
}
super.onChildDraw(c, recyclerView, mViewHolder, mdX, mdY, actionState, isCurrentlyActive)
}
}
val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
itemTouchHelper.attachToRecyclerView(recyclerViewClash)
}
Here is the only thing that works, finally found it. It is important to understand that the ItemTouchHelper will implement onItemTouchListener:
An OnItemTouchListener allows the application to intercept touch events in progress at the view hierarchy level of the RecyclerView before those touch events are considered for RecyclerView's own scrolling behavior.
textView.setOnTouchListener { v, event ->
//let the scrollView handle the scroll event
scrollView.onTouchEvent(event)
//this is the key feature
v.parent.requestDisallowInterceptTouchEvent(true)
//return true to consume the event (mandatory)
true
}
Nothing else is needed. Also, a ScrollView is enough, no need for a NestedScrollView.
I have to add internal colored grid to my RecyclerView with GridLayoutManager.
Adding space to ViewHolders is not my solution because I have rounded background in recyclerView and background in viewHolders breaks them.
Standart DividerItemDecoration gives me exactly what I need, except I can choose ONLY horizontal or vertical lines and not both.
I've found answers which gave me internal grid by spacing via getItemOffsets(), but I can't color fill outRect.
What is the best way to achieve this idea?
So I've achieved this task by simplifying standart androidx.recyclerview.widget.DividerItemDecoration by deleting all separation by HORIZONTAL and VERTICAL in code. As the result I have internal horizontal+vertical lines.
Applying decoration:
myRecyclerView.addItemDecoration(GridDividerItemDecoration(requireContext()))
The whole decoration class:
class GridDividerItemDecoration(context: Context) : ItemDecoration() {
private val mBounds = Rect()
private var mDivider: Drawable?
fun setDrawable(drawable: Drawable) {
mDivider = drawable
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null || mDivider == null) {
return
}
drawVertical(c, parent)
drawHorizontal(c, parent)
}
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(left, parent.paddingTop, right,
parent.height - parent.paddingBottom)
} else {
left = 0
right = parent.width
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.bottom + child.translationY.roundToInt()
val top = bottom - mDivider!!.intrinsicHeight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) {
canvas.save()
val top: Int
val bottom: Int
if (parent.clipToPadding) {
top = parent.paddingTop
bottom = parent.height - parent.paddingBottom
canvas.clipRect(parent.paddingLeft, top,
parent.width - parent.paddingRight, bottom)
} else {
top = 0
bottom = parent.height
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDivider!!.intrinsicWidth
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
if (mDivider == null) {
outRect[0, 0, 0] = 0
return
}
outRect[0, 0, 0] = mDivider!!.intrinsicHeight
outRect[0, 0, mDivider!!.intrinsicWidth] = 0
}
companion object {
private const val TAG = "DividerItem"
private val ATTRS = intArrayOf(R.attr.listDivider)
}
init {
val a = context.obtainStyledAttributes(ATTRS)
mDivider = a.getDrawable(0)
if (mDivider == null) {
Log.w(TAG, "#android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()")
}
a.recycle()
}
}
Right now I have a funtional swipe left to delete in recyclerview with two layouts(foreground and background). I use itemtouchhelper in the code. However, I would like to have BOTH swipe left and swipe right showing different colors and icons, like in Google Inbox. How can I implement that?
What I want is:
swipe rightswipe left
what I have is:
only swipe right
the code is just the standard itemtouchhelper.simplecallback with 2 layouts in the xml. And I googled everywhere and only found single swipe option with single icon and single color
Use ItemTouchHelper to Implement the Gmail Like Feature
call the following function after setting the recyclerView
private fun initSwipe() {
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
if (direction == ItemTouchHelper.LEFT) {
//Logic to do when swipe left
} else {
//Logic to do when swipe right
}
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
val icon: Bitmap
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
//Drawing for Swife Right
val itemView = viewHolder.itemView
val height = itemView.bottom.toFloat() - itemView.top.toFloat()
val width = height / 3
if (dX > 0) {
p.color = Color.parseColor("#2F2FD3")
val background = RectF(itemView.left.toFloat(), itemView.top.toFloat(), dX, itemView.bottom.toFloat())
c.drawRect(background, p)
icon = BitmapFactory.decodeResource(resources, R.drawable.ic_archive)
val icon_dest = RectF(itemView.left.toFloat() + width, itemView.top.toFloat() + width, itemView.left.toFloat() + 2 * width, itemView.bottom.toFloat() - width)
c.drawBitmap(icon, null, icon_dest, p)
} else {
//Drawing for Swife Left
p.color = Color.parseColor("#D32F2F")
val background = RectF(itemView.right.toFloat() + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
c.drawRect(background, p)
icon = BitmapFactory.decodeResource(resources, R.drawable.ic_delete)
val icon_dest = RectF(itemView.right.toFloat() - 2 * width, itemView.top.toFloat() + width, itemView.right.toFloat() - width, itemView.bottom.toFloat() - width)
c.drawBitmap(icon, null, icon_dest, p)
}
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback)
itemTouchHelper.attachToRecyclerView(YOUR_RECYCLER_VIEW)
}
Where
Object p is an object of paint Paint p = new Paint()
Hope this may help you.
I have tried to add a click listener to itemdecoration like below but still no luck. Please help.
recyclerview.addOnItemTouchListener( object : RecyclerView.OnItemTouchListener{
override fun onTouchEvent(rv: RecyclerView?, e: MotionEvent?) {
}
override fun onInterceptTouchEvent(rv: RecyclerView?, e: MotionEvent?): Boolean {
val view = rv!!.findChildViewUnder(e!!.x, e!!.y)
if(view == null) return false
when(view.id){
R.id.list_item_section_text->{
Log.d("Clicked","Header")
}
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
}
})
ItemDecoration will help you draw something between items, but the View you're drawing in is actually the RecyclerView itself (according to Layout inspector), via its Canvas. So you won't be able to add an basic onClickListener on that decorations.
According to your code, I guess you have an item decoration for each of your items header?
What I would do for this would be not to consider my header as an ItemDecoration but as an item (with different type) I would feed my RecyclerView with.
sealed class RecyclerViewItem
object MainItem : RecyclerViewItem()
object Header : RecyclerViewItem()
In your adapter (with items: RecyclerViewItem) you override getItemViewType method using custom ids. Then in onBindViewHolder you can check the item view type and add your onClickListener on your view if it's a Header.
For more info, you can search for building RecyclerView with different item type.
You can customize a RecyclerView and check if touch event arrives to ItemDecorator or no:
class YourRecyclerView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
private val clicksFlow: MutableSharedFlow<ClickEvent> = MutableSharedFlow(
extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST
)
fun getClicksFlow() = clicksFlow as Flow<RecyclerClickEvent>
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent) : Boolean{
if (e.action == MotionEvent.ACTION_DOWN) {
for (i in 0 until itemDecorationCount) {
val decor = getItemDecorationAt(i)
if (decor is YourDecoration && decor.isOnTouched(this, e)) {
clicksFlow.tryEmit(ClickEvent(e.x, e.y))
return true
}
}
}
return super.onTouchEvent(e)
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN) {
for (i in 0 until itemDecorationCount) {
val decor = getItemDecorationAt(i)
if (decor is YourDecoration && decor.isOnTouched(this, e)) {
return true
}
}
}
return super.onInterceptTouchEvent(e)
}
}
class YourDecoration(/*some stuff*/) : RecyclerView.ItemDecoration() {
fun isOnTouched(parent: YourRecyclerView , e: MotionEvent): Boolean {
val w = abs(scaleX * width)
val h = abs(scaleY * height)
val top = topMargin - height / 2F - parent.paddingTop
return if (scaleX > 0) {
val side = parent.measuredWidth - sideMargin
e.y >= top && e.y <= (top + h) && side >= e.x && (side - w) <= e.x
} else {
val side = sideMargin
e.y >= top && e.y <= (top + h) && side <= e.x && e.x <= (side + w)
}
}
}