Ripple effect in custom view - android

I am currently creating an Android view in which, when the use tap it, I will display a sort of ripple around the coordinate of the tap.
But I'm not sure on how to do it. My first idea was to invalidate the cache and just make the circle bigger each time but it doesn't seem appropriate nor efficient to do this like that.
If anyone faced the same problem before and would love the share some tips on how to do it it would be much appreciated.

I finally found out a solution. Not a perfect one but it works for now.
Here is the code I did. Basically when I need it I change a boolean to true so my onDrawfunction knows that it have to execute the drawFingerPrintfunction.
The drawFingerPrint function, in the other end, just draw a circle that's bigger and bigger between each iteration until it reaches the diameter needed
private fun drawFingerPrint(canvas: Canvas) {
canvas.drawCircle(pointerX, pointerY, radius, paint)
if(radius<= 100F){
radius+=10F
invalidate()
}
else{
radius = 0F
drawAroundFinger = false
invalidate()
}
}
I hope someone else will find this useful sometimes!
Matthieu

As already mentioned #Matthieu, you need to draw circle on the canvas and invalidate the view. Here I provide a more complete example.
So we have an open class RippleView:
open class RippleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var rippleX: Float? = null
private var rippleY: Float? = null
private var rippleRadius: Float? = null
var maxRippleRadius: Float = 100f // Retrieve from resources
var rippleColor: Int = 0x88888888.toInt()
private val ripplePaint = Paint().apply {
color = rippleColor
}
private val animationExpand = object : Runnable {
override fun run() {
rippleRadius?.let { radius ->
if (radius < maxRippleRadius) {
rippleRadius = radius + maxRippleRadius * 0.1f
invalidate()
postDelayed(this, 10L)
}
}
}
}
private val animationFade = object : Runnable {
override fun run() {
ripplePaint.color.let { color ->
if (color.alpha > 10) {
ripplePaint.color = color.adjustAlpha(0.9f)
invalidate()
postDelayed(this, 10L)
} else {
rippleX = null
rippleY = null
rippleRadius = null
invalidate()
}
}
}
}
fun startRipple(x: Float, y: Float) {
rippleX = x
rippleY = y
rippleRadius = maxRippleRadius * 0.15f
ripplePaint.color = rippleColor
animationExpand.run()
}
fun stopRipple() {
if (rippleRadius != null) {
animationFade.run()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val x = rippleX ?: return
val y = rippleY ?: return
val r = rippleRadius ?: return
canvas.drawCircle(x, y, r, ripplePaint)
}
}
fun Int.adjustAlpha(factor: Float): Int =
(this.ushr(24) * factor).roundToInt() shl 24 or (0x00FFFFFF and this)
inline val Int.alpha: Int
get() = (this shr 24) and 0xFF
Now you can extend any custom view with RippleView and use startRipple method when you get ACTION_DOWN, and stopRipplemethod when you get ACTION_UP:
class ExampleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : RippleView(context, attrs), View.OnTouchListener {
init {
setOnTouchListener(this)
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (isInsideArea(event.x, event.y)) {
startRipple(event.x, event.y)
}
}
MotionEvent.ACTION_UP -> {
stopRipple()
}
}
return false
}
private fun isInsideArea(x: Float, y: Float): Boolean {
TODO("Not yet implemented")
}
}

Related

how to make ellipsized text with rules character more than 65 in android?

class EllipsizedTextView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): AppCompatTextView(context, attrs, defStyleAttr) {
private var ellipsis = getDefaultEllipsis().toString()
private var ellipsisColor = getDefaultEllipsisColor()
private var isEllipsis = false
private var ellipsizedText: CharSequence? = null
private var callbackEllipsized: MoreClickableSpan? = null
init {
attrs?.let { attributeSet ->
context.theme.obtainStyledAttributes(attributeSet, R.styleable.EllipsizedTextView, 0, 0).apply {
ellipsis = getString(R.styleable.EllipsizedTextView_ellipsis) ?: getDefaultEllipsis().toString()
ellipsisColor = getColor(R.styleable.EllipsizedTextView_ellipsisColor, getDefaultEllipsisColor())
recycle()
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
/*
Prepare to set custom ellipsize text
*/
val availableScreenWidth = measuredWidth - compoundPaddingLeft.toFloat() - compoundPaddingRight.toFloat()
var availableTextWidth = availableScreenWidth * maxLines
ellipsizedText = TextUtils.ellipsize(text, paint, availableTextWidth, ellipsize, false) { start, end ->
isEllipsis = start != 0 && end >= 65
}
if (isEllipsis) { // check if current text is ellipsized or not
printLog("isEllipsis: $ellipsizedText ellipsis: $ellipsis, text : $text")
// If the ellipsizedText is different than the original text, this means that it didn't fit and got indeed ellipsized.
// Calculate the new availableTextWidth by taking into consideration the size of the custom ellipsis, too.
availableTextWidth = (availableScreenWidth - paint.measureText(ellipsis)) * maxLines
ellipsizedText = TextUtils.ellipsize(text, paint, availableTextWidth, ellipsize, false){ start, end ->
isEllipsis = start != 0 && end >= 65
}
}
setEllipsizedText(ellipsizedText, isEllipsis)
}
private fun setEllipsizedText(value: CharSequence?, isEllipsized: Boolean){
printLog("setEllipsizedText > $isEllipsized")
if(isEllipsized){
val resultText = "$value$ellipsis"
val startPoint = resultText.indexOf(ellipsis)
val endPoint = startPoint + ellipsis.length
val spannable = SpannableString(resultText).apply {
callbackEllipsized?.let { callback ->
printLog("setEllipsizedText > implement callbackEllipsized")
// set event click on spannable
setSpan(callback, startPoint, endPoint, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// set color on target text while enable to click
setSpan(ForegroundColorSpan(ellipsisColor), startPoint, endPoint, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
//val defaultEllipsisStart = value.indexOf(getDefaultEllipsis())
//val defaultEllipsisEnd = defaultEllipsisStart + 1
//text = spannableStringBuilder.append(ellipsizedText).replace(defaultEllipsisStart, defaultEllipsisEnd, ellipsisSpannable)
text = spannable
movementMethod = LinkMovementMethod.getInstance()
printLog("setEllipsizedText > text : $text")
}
}
private fun getDefaultEllipsis(): Char {
return Typography.ellipsis
}
private fun getDefaultEllipsisColor(): Int {
return textColors.defaultColor
}
fun setActionClickEllipse(callback: MoreClickableSpan){
this.callbackEllipsized = callback
}
fun isReadMore(): Boolean = isEllipsis
#Suppress("unused_parameter")
fun printLog(msg: String) { }
open class MoreClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {}
override fun updateDrawState(ds: TextPaint) {}
}
}
How to implement ellipsized text with rules if more than 65 character? my existing code below like this
Why don't you add this to your TextView in the xml file?It will limit it to 65 chars and it will show ellipse after that
android:maxLength="65"
android:ellipsize="end"
android:maxLines="1"

Why is my List not being recognized as it has been initialized in onDraw()?

I have initialized my List in the function below in MainActivitywith DataPoint(data class in GraphView)
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var graph_view = GraphView(this)
graph_view.setData(generateRandomDataPoints())
}
fun generateRandomDataPoints(): List<GraphView.DataPoint> {
val random = Random()
return (0..20).map {
GraphView.DataPoint(it, random.nextInt(50) + 1)
}
}
}
GraphView
private val dataSet = arrayListOf<DataPoint>()
private var xMin = 0
private var xMax = 0
private var yMin = 0
private var yMax = 0
private val dataPointPaint = Paint().apply {
color = Color.BLACK
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
Log.e("D SIze", dataSet.size.toString())
for(i in 0 until dataSet.size){
val realX = dataSet[i].xVal.toRealX()
val realY = dataSet[i].yVal.toRealY()
Log.e("TAG", realX.toString())
Log.e("TAG", realY.toString())
Log.e("TAG", "NOT BEingf claeed")
canvas.drawCircle(realX, realY, 17f, dataPointPaint)
}
}
fun setData(newDataSet: List<DataPoint>) {
Log.e("TAG", newDataSet.size.toString())
xMin = newDataSet.minByOrNull { it.xVal }?.xVal ?: 0
xMax = newDataSet.maxByOrNull { it.xVal }?.xVal ?: 0
yMin = newDataSet.minByOrNull { it.yVal }?.yVal ?: 0
yMax = newDataSet.maxByOrNull { it.yVal }?.yVal ?: 0
dataSet.clear() // clear if any point are entered
dataSet.addAll(newDataSet) // add all points fro newDataSet
for(i in 0 .. dataSet.size-1){
Log.e("!!!!", dataSet[i].toString() )
}
invalidate()
}
private fun Int.toRealX() = toFloat() / xMax * width
private fun Int.toRealY() = toFloat() / yMax * height
data class DataPoint(
val xVal: Int,
val yVal: Int)
}
All of my logs work as expected and print's what is expected.
But when onDraw() is called from the invalidate() in the setData() function the log at the start returns 0. Which is why the for loop in onDraw() is never called and no points dots are being printed to the canvas?
I do not know why as all my other logs show that the data has been added for example the for loop in setData() prints
DataPoint(xVal=1, yVal=20)
DataPoint(xVal=3, yVal=37)
DataPoint(xVal=4, yVal=39)
....
Why is this and how can I solve it please?

Pixel art creator: loading saved pixel data from Room causes strange Canvas bug/glitch

Note: my question is very specific, apologies if the title isn't clear enough as to what the problem is.
I'm creating a pixel art editor application using Canvas, and the pixel art data is saved into a Room database.
Here's the canvas code:
package com.realtomjoney.pyxlmoose.customviews
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.realtomjoney.pyxlmoose.activities.canvas.*
import com.realtomjoney.pyxlmoose.converters.JsonConverter
import com.realtomjoney.pyxlmoose.database.AppData
import com.realtomjoney.pyxlmoose.listeners.CanvasFragmentListener
import com.realtomjoney.pyxlmoose.models.Pixel
import kotlin.math.sqrt
class MyCanvasView(context: Context, val spanCount: Double) : View(context) {
lateinit var extraCanvas: Canvas
lateinit var extraBitmap: Bitmap
val rectangles = mutableMapOf<RectF, Paint?>()
private lateinit var caller: CanvasFragmentListener
private var thisWidth: Int = 0
private var scale: Double = 0.0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
thisWidth = w
caller = context as CanvasFragmentListener
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
scale = (w / spanCount)
for (i in 0 until spanCount.toInt()) {
for (i_2 in 0 until spanCount.toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; color = Color.WHITE })
}
}
}
private fun drawRectAt(x: Float, y: Float) {
for (rect in rectangles.keys) {
if (rect.contains(x, y)) {
caller.onPixelTapped(this, rect)
invalidate()
}
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> drawRectAt(event.x, event.y)
MotionEvent.ACTION_DOWN -> drawRectAt(event.x, event.y)
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
fun saveData(): List<Pixel> {
val data = mutableListOf<Pixel>()
for (pair in rectangles) {
data.add(Pixel(pair.value?.color))
}
return data
}
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
}
Here's an example of how a 10 by 10 canvas may look like:
The pixel data is saved into a Room database as a Json String, and whenever we want to access this data we convert the Json String back to a List<Pixel>, et cetera:
Dao:
#Dao
interface PixelArtCreationsDao {
#Insert
suspend fun insertPixelArt(pixelArt: PixelArt)
#Query("SELECT * FROM PixelArt ")
fun getAllPixelArtCreations(): LiveData<List<PixelArt>>
#Query("DELETE FROM PixelArt WHERE objId=:pixelArtId")
fun deletePixelArtCreation(pixelArtId: Int)
#Query("UPDATE PixelArt SET item_bitmap=:bitmap WHERE objId=:id_t")
fun updatePixelArtCreationBitmap(bitmap: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_pixel_data=:pixelData WHERE objId=:id_t")
fun updatePixelArtCreationPixelData(pixelData: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_favourited=:favorited WHERE objId=:id_t")
fun updatePixelArtCreationFavorited(favorited: Boolean, id_t: Int): Int
}
PixelArt database:
#Database(entities = [PixelArt::class], version = 1)
abstract class PixelArtDatabase: RoomDatabase() {
abstract fun pixelArtCreationsDao(): PixelArtCreationsDao
companion object {
private var instance: PixelArtDatabase? = null
fun getDatabase(context: Context): PixelArtDatabase {
if (instance == null) {
synchronized(PixelArtDatabase::class) {
if (instance == null) instance = Room.databaseBuilder(context.applicationContext, PixelArtDatabase::class.java, AppData.dbFileName).allowMainThreadQueries().build()
}
}
return instance!!
}
}
}
AppData:
class AppData {
companion object {
var dbFileName = "pixel_art_db"
lateinit var db: PixelArtDatabase
}
}
Model:
#Entity
data class PixelArt(
#ColumnInfo(name = "item_bitmap") var bitmap: String,
#ColumnInfo(name = "item_title") var title: String,
#ColumnInfo(name = "item_pixel_data") var pixelData: String,
#ColumnInfo(name = "item_favourited") var favourited: Boolean,
#ColumnInfo(name = "item_date_created") var dateCreated: String = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())) {
#PrimaryKey(autoGenerate = true) var objId = 0
}
Now, say we have two projects like so with two different spanCount values:
Once we click on the first item, the following occurs:
For some reason it's setting the grid size to be equal to that of the second item, and I'm really trying to understand why this is the case. I've tried for a couple of hours to fix this weird glitch and have had no luck doing so.
But, for some reason when we go to our second item it renders properly:
If we create a new 80 x 80 canvas and then go back to the second creation it will render like so:
I'm assuming that it's setting the spanCount to that of the latest item in the database, but I'm unsure why this is happening.
I suspect it has something to do with the code that takes the List<Pixel> and draws it onscreen:
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
Although I'm not entirely sure where the source of the bug is coming from because it seems I'm doing everything right. It's honestly been a brainfuck trying to fix this lol
Any help would be appreciated to fix this annoying glitch so I can finish my pixel art editor app.
This bug was fixed by calling invalidate() on the Fragment's Canvas property after the user taps the back button. It took me a couple of days to get to fix this, so I'm posting an answer here in case someone has a similar bug.
fun CanvasActivity.extendedOnBackPressed() {
canvasFragmentInstance.myCanvasViewInstance.invalidate()
startActivity(Intent(context, MainActivity::class.java))
}

Pause progress of ObjectAnimator

I'm implementing a custom view which draws some kind ProgressBar view, taking two views as parameters (origin and destination). Like this:
This is the complete class:
class BarView #JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var valueAnimator: ObjectAnimator? = null
private lateinit var path: Path
private val pathMeasure = PathMeasure()
private var pauseProgress: Int = dip(40)
var progress = 0f
set(value) {
field = value.coerceIn(0f, pathMeasure.length)
invalidate()
}
private var originPoint: PointF? = null
private var destinationPoint: PointF? = null
private val cornerEffect = CornerPathEffect(dip(10).toFloat())
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = dip(10f).toFloat()
color = ContextCompat.getColor(context, android.R.color.darker_gray)
pathEffect = cornerEffect
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
if (progress < pathMeasure.length) {
val intervals = floatArrayOf(progress, pathMeasure.length - progress)
val progressEffect = DashPathEffect(intervals, 0f)
linePaint.pathEffect = ComposePathEffect(progressEffect, cornerEffect)
}
canvas.drawPath(path, linePaint)
}
object PROGRESS : Property<BarView, Float>(Float::class.java, "progress") {
override fun set(view: BarView, progress: Float) {
view.progress = progress
}
override fun get(view: BarView) = view.progress
}
private fun startAnimator() {
valueAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 0f, pathMeasure.length).apply {
duration = 500L
interpolator = LinearInterpolator()
}
setPauseListener()
valueAnimator!!.start()
}
fun resume() {
valueAnimator!!.resume()
}
fun reset() {
startAnimator()
}
fun setPoints(originView: View, destinationView: View) {
originPoint = PointF(originView.x + originView.width / 2, 0f)
destinationPoint = PointF(destinationView.x + destinationView.width / 2, 0f)
setPath()
startAnimator()
}
private fun setPath() {
path = Path()
path.moveTo(originPoint!!.x, originPoint!!.y)
path.lineTo(destinationPoint!!.x, destinationPoint!!.y)
pathMeasure.setPath(path, false)
}
private fun setPauseListener() {
valueAnimator!!.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(valueAnimator: ValueAnimator?) {
val progress = valueAnimator!!.getAnimatedValue("progress") as Float
if (progress > pauseProgress) {
valueAnimator.pause()
this#BarView.valueAnimator!!.removeUpdateListener(this)
}
}
})
}
}
What im trying to do is to pause the animation at a specific progress, 40dp in this case:
private fun setPauseListener() {
valueAnimator!!.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(valueAnimator: ValueAnimator?) {
val progress = valueAnimator!!.getAnimatedValue("progress") as Float
if (progress > pauseProgress) {
valueAnimator.pause()
this#BarView.valueAnimator!!.removeUpdateListener(this)
}
}
})
}
But the animations have different speeds since the views have different path lengths, and all of them have to finish in 500ms. They are not pausing at the same distance from the origin:
I tried switching from a LinearInterpolator to a AccelerateInterpolator, to make the start of the animation slower, but i'm still not satisfied with the results.
The next step for me, would be to try to implement a custom TimeInterpolator to make the animation start speed the same no matter how long the path is on each view, but I cannot wrap my head arrow the maths to create the formula needed.
valueAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 0f, pathMeasure.length).apply {
duration = 500L
interpolator = TimeInterpolator { input ->
// formula here
}
}
Any help with that would be well received. Any suggestions about a different approach. What do you think?
1) As I mentioned I would use determinate ProgressBar and regulate bar width by maximum amount of progress, assigned to certain view
2) I would use ValueAnimator.ofFloat with custom interpolator and set progress inside it
3) I would extend my custom interpolator from AccelerateInterpolator to make it look smthng like this:
class CustomInterpolator(val maxPercentage: Float) : AccelerateInterpolator(){
override fun getInterpolation(input: Float): Float {
val calculatedVal = super.getInterpolation(input)
return min(calculatedVal, maxPercentage)
}
}
where maxPercentage is a fraction of view width (from 0 to 1) you bar should occupy
Hope it helps

How to add a click listener to ItemDecoration in RecyclerView Android?

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

Categories

Resources