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))
Related
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
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.
I have a list item with three swipe actions which looks like this:
The regular list item and the buttons are two different layouts defined in xml.
To reveal the button actions I use ItemTouchHelper.SimpleCallback. In onChildDraw I tell the item list item's x-axis to be only drawn until it reaches the width of the button controls.
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val foreground = (viewHolder as? NachrichtViewHolder)?.binding?.nachrichtListItem
val background = (viewHolder as? NachrichtViewHolder)?.binding?.background
val x: Float = when {
dX.absoluteValue > background?.measuredWidth?.toFloat() ?: dX -> background?.measuredWidth?.toFloat()
?.unaryMinus() ?: dX
else -> dX
}
getDefaultUIUtil().onDraw(
c,
recyclerView,
foreground,
x,
dY,
actionState,
isCurrentlyActive
)
}
Here is an abbreviated layout file demonstrating the way I built the ui:
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/background"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:clickable="#{backgroundVisible}"
android:focusable="#{backgroundVisible}"
android:focusableInTouchMode="#{backgroundVisible}"
android:elevation="#{backgroundVisible ? 4 : 0}">
<ImageButton
android:id="#+id/actionReply"/>
<ImageButton
android:id="#+id/actionShare"/>
<ImageButton
android:id="#+id/actionDelete"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/nachrichtListItem"
android:elevation="#{backgroundVisible ? 0 : 4}"
android:clickable="#{!backgroundVisible}"
android:focusable="#{!backgroundVisible}"
android:focusableInTouchMode="#{!backgroundVisible}">
<!-- regular list item -->
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
My problem is that the buttons are not clickable.
What I tried so far:
set elevation to bring element on top
set items clickable depending on the visibility state of the buttons
This can be seen in the layout file. I want to define the elements inside xml and not draw them manually if possible.
The problem is solved. ItemTouchHelper.SimpleCallback swallows all your touch events. So you need to register a TouchListener for the buttons. The buttons come in my case from xml. Inspired by this I came up with the following solution:
#SuppressLint("ClickableViewAccessibility")
class NachrichtItemSwipeCallback(private val recyclerView: RecyclerView) :
ItemTouchHelper.SimpleCallback(0, LEFT) {
private val itemTouchHelper: ItemTouchHelper
private var binding: ListItemNachrichtBinding? = null
private var lastSwipedPosition: Int = -1
init {
// Disable animations as they don't work with custom list actions
(this.recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
this.recyclerView.setOnTouchListener { _, touchEvent ->
if (lastSwipedPosition < 0) return#setOnTouchListener false
if (touchEvent.action == MotionEvent.ACTION_DOWN) {
val viewHolder =
this.recyclerView.findViewHolderForAdapterPosition(lastSwipedPosition)
val swipedItem: View = viewHolder?.itemView ?: return#setOnTouchListener false
val rect = Rect()
swipedItem.getGlobalVisibleRect(rect)
val point = Point(touchEvent.rawX.toInt(), touchEvent.rawY.toInt())
if (rect.top < point.y && rect.bottom > point.y) {
// Consume touch event directly
val buttons =
binding?.buttonActionBar?.children
.orEmpty()
.filter { it.isClickable }
.toList()
val consumed = consumeTouchEvents(buttons, point.x, point.y)
if (consumed) {
animateClosing(binding?.nachrichtListItem)
}
return#setOnTouchListener false
}
}
return#setOnTouchListener false
}
this.itemTouchHelper = ItemTouchHelper(this)
this.itemTouchHelper.attachToRecyclerView(this.recyclerView)
}
// Only for drag & drop functionality
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
binding = (viewHolder as? NachrichtViewHolder)?.binding
val foreground = binding?.nachrichtListItem
val background = binding?.buttonActionBar
val backgroundWidth = background?.measuredWidth?.toFloat()
// only draw until start of action buttons
val x: Float = when {
dX.absoluteValue > backgroundWidth ?: dX -> backgroundWidth?.unaryMinus() ?: dX
else -> dX
}
foreground?.translationX = x
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
this.lastSwipedPosition = viewHolder.adapterPosition
recyclerView.adapter?.notifyItemChanged(this.lastSwipedPosition)
}
private fun animateClosing(
foreground: ConstraintLayout?
) {
foreground ?: return
ObjectAnimator.ofFloat(foreground, "translationX", 0f).apply {
duration = DURATION_ANIMATION
start()
}.doOnEnd { applyUiWorkaround() }
}
// See more at https://stackoverflow.com/a/37342327/3734116
private fun applyUiWorkaround() {
itemTouchHelper.attachToRecyclerView(null)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun consumeTouchEvents(
views: List<View?>,
x: Int,
y: Int
): Boolean {
views.forEach { view: View? ->
val viewRect = Rect()
view?.getGlobalVisibleRect(viewRect)
if (viewRect.contains(x, y)) {
view?.performClick()
return true
}
}
return false
}
companion object {
private const val DURATION_ANIMATION: Long = 250
}
}
I make bottom sheet fragment like this:
val bottomSheet = PictureBottomSheetFragment(fragment)
bottomSheet.isCancelable = true
bottomSheet.setListener(pictureListener)
bottomSheet.show(ac.supportFragmentManager, "PictureBottomSheetFragment")
But its not dismiss when I touch outside. and dismiss or isCancelable not working.
try this
behavior.setState(BottomSheetBehavior.STATE_HIDDEN));
You can override method and indicate, for example, in onViewCreated what you need:
class ModalDialogSuccsesDataPatient : ModalDialog() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false //or true
}
}
Let's try to design reusable functions to solve this problem and similar ones if the need arises.
We can create extension functions on View that tell whether a point on the screen is contained within the View or not.
fun View.containsPoint(rawX: Int, rawY: Int): Boolean {
val rect = Rect()
this.getGlobalVisibleRect(rect)
return rect.contains(rawX, rawY)
}
fun View.doesNotContainPoint(rawX: Int, rawY: Int) = !containsPoint(rawX, rawY)
Now we can override the dispatchTouchEvent(event: MotionEvent) method of Activity to know where exactly the user clicked on the screen.
private const val SCROLL_THRESHOLD = 10F // To filter out scroll gestures from clicks
private var downX = 0F
private var downY = 0F
private var isClick = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
isClick = true
}
MotionEvent.ACTION_MOVE -> {
val xThreshCrossed = abs(downX - event.x) > SCROLL_THRESHOLD
val yThreshCrossed = abs(downY - event.y) > SCROLL_THRESHOLD
if (isClick and (xThreshCrossed or yThreshCrossed)) {
isClick = false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (isClick) onScreenClick(event.rawX, event.rawY)
}
else -> { }
}
return super.dispatchTouchEvent(event)
}
private fun onScreenClick(rawX: Float, rawY: Float) { }
Now, you can simply use the above-defined functions to achieve the required result
private fun onScreenClick(rawX: Float, rawY: Float) {
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}
What more? If you have a BaseActivity which is extended by all your Activities then you can add the click detection code to it. You can make the onScreenClick an protected open method so that it can be overridden by the sub-classes.
protected open fun onScreenClick(rawX: Float, rawY: Float) { }
Usage:
override fun onScreenClick(rawX: Float, rawY: Float) {
super.onScreenClick(rawX, rawY)
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}
So i tried out kotlin in android and wanted to begin with painting a circle on a view, where i get a touch input. It worked fine, when i had the onTouchEvent in the View-Class, but i wanted to put the logic outside of the graphic part, so i made a method in the view class, which changes the coordinates of the circle and calls invalidate(). Now i put the onTouchEvent in to the MainActivity and called the method with the new coordinates. The methods gets called as i can see by system.out.println(), but invalidate() doesn't calls on Draw() so the circle doesn't change. I think there is a simple solution for that problem, but i can't find it. So i would be very happy to get some help.
Here's my code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(GameView(this))
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
GameView(this).updateCircle(event!!.x,event!!.y)
return true
}
}
MainActivity.kt
class GameView(context: Context): View(context) {
val paint : Paint
var circleX : Float
var circleY : Float
init {
paint = Paint();
paint.isFilterBitmap = true;
paint.isAntiAlias = true;
paint.color = Color.YELLOW;
circleX = 100f;
circleY = 100f;
}
override fun onDraw(canvas: Canvas?) {
System.out.println("Gets Called")
super.onDraw(canvas)
canvas?.drawColor(Color.WHITE);
canvas?.drawCircle(circleX,circleY,50f,paint)
}
fun drawGrid(height: Int,width: Int){
}
fun updateCircle(x:Float,y:Float){
circleX = x
circleY = y
invalidate()
System.out.println(circleX)
}
}
GameView.kt