List element with swipe actions - buttons not clickable - android

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

Related

RecyclerView on drop

I have RecyclerView Drag & Drop feature, but I'd like to do some calculations onDrop. When I put my expensiveFunction() in onMove() it's triggered at every position change until the drag is over. That's a big overkill. Is there a way to trigger function on drag end?
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
private var simpleCallback = object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP.or(ItemTouchHelper.DOWN), 0) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val startPosition = viewHolder.absoluteAdapterPosition
val endPosition = target.absoluteAdapterPosition
Collections.swap(itemList, startPosition, endPosition)
recyclerView.adapter?.notifyItemMoved(startPosition, endPosition)
expensiveFunction()
return true
}
}
You could override onSelectedChanged() which get called when the ViewHolder swiped or dragged by the ItemTouchHelper.
To catch the drop action examine the actionState value to be ItemTouchHelper.ACTION_STATE_IDLE:
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when (actionState) {
// when the item is dropped
ItemTouchHelper.ACTION_STATE_IDLE -> {
Log.d(TAG, "Item is dropped")
}
}
}

Scrolling a RecyclerView inside another RecyclerView automatically not working correctly

So am having this recyclerview which will contain holders of multiple types one of which could be a scrollable horizontal list of edge to edge images, that are being scrolled automatically and have a current item indicator. so for this i used a viewholder which will itself contain another recyclerview and a dots indicator( which itself is another recycler view, so basically recyclerview = a list of vh , where one of the vh = 2 horizontal recyclerview).
title
[A,B,C,D...]
[+ ---]
title
[A,B,C,D...]
[+ --]
title
[A,B,C,D...]
[+ --]
title
[A,B,C,D...]
[+ --]
My innermost recylerview of horizontal images is created something like this:
class ImageAdapter : RecyclerView.Adapter<ImageVH>() {
var imageResList = mutableListOf<Int>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ImageVH(parent, viewType)
override fun onBindViewHolder(holder: ImageVH, pos: Int)
= holder.bindData(imageResList[pos % imageResList.size])
override fun getItemCount() = Int.MAX_VALUE
}
class ImageVH(v: View) : RecyclerView.ViewHolder(v) {
constructor(parent: ViewGroup, viewtype: Int) : this(
LayoutInflater.from(parent.context).inflate(R.layout.item_image, parent, false)
)
fun bindData(imageRes: Int) {
Glide.with(itemView.context).load("").error(imageRes).into(itemView.ivImage)
}
}
it is basically fooling the adapter to think as if i have a million images but will actually have just a few images. this creates an impression of circular scroll.
Next i will need something to change the dots indicator of the second recyclerview. for this i went into the parent of this recyclerview and attached an onScrollListener . The onScrollListener gives me 2 function: onScrolled and onScrollStateChanged.
with onScrolled , i determine when to change the next dots recyclerview's state to show the new dot. i do this via linear layout manager. when it gives findFirstCompletelyVisibleItemPosition as positive number .
with onScrollStateChanged(), i run a kind of recursion, where whenever i get the state as SCROLL_STATE_IDLE, I post a handler to scroll the recyclerview to next item after 2 seconds. after 2 seconds, it will automatically smooth scroll and again fire the same event, causing the handler to fire the same action again.
so the code looks something like this:
data class Rails(val title: String, val images: MutableList<Int>,val autoscroll:Boolean =false)
class RailsAdapter : RecyclerView.Adapter<RailVH>() {
var railsList = mutableListOf<Rails>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = RailVH(parent, viewType)
override fun onBindViewHolder(holder: RailVH, pos: Int) = holder.bindData(railsList[pos])
override fun getItemCount() = railsList.size
}
class RailVH(v: View) : RecyclerView.ViewHolder(v) {
constructor(parent: ViewGroup, viewtype: Int) : this(
LayoutInflater.from(parent.context).inflate(R.layout.item_rails, parent, false)
)
private var autoscrollImages = false
fun bindData(rails: Rails) {
autoscrollImages = rails.autoscroll
with(itemView) {
tvTitle?.text = rails.title
rvImagers?.apply {
adapter = ImageAdapter().also {
it.imageResList = rails.images
it.notifyDataSetChanged()
}
PagerSnapHelper().attachToRecyclerView(this)
isNestedScrollingEnabled = false
onFlingListener = null
addOnScrollListener(onScrollListener)
}
}
if(autoscrollImages){
bannerChangerHandler.postDelayed(bannerChangerRunnable,bannerChangerDelayMilllis)
}
}
private val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
//super.onScrolled(recyclerView, dx, dy)
val bannerLLManager = itemView.rvImagers?.layoutManager as? LinearLayoutManager
bannerLLManager?.let { linearLayoutManager ->
val bannerCurrentPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
if (bannerCurrentPos >= 0) {
val rvDotsDataListSize = 5
val positionInRange = bannerCurrentPos % rvDotsDataListSize
Toast.makeText(
itemView.context,
"highlight dot #$positionInRange",
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
//super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if(autoscrollImages){
Log.e(">>a>>", "RecyclerView.SCROLL_STATE_IDLE!")
bannerChangerHandler.postDelayed(bannerChangerRunnable, bannerChangerDelayMilllis
)
}
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
Log.e(">>a>>", "RecyclerView.SCROLL_STATE_DRAGGING!")
bannerChangerHandler.removeCallbacks(bannerChangerRunnable)
}
else -> {
}
}
}
}
private val bannerChangerHandler: Handler = Handler()
private val bannerChangerRunnable = Runnable {
itemView.rvImagers?.apply {
val bannerManager = layoutManager as? LinearLayoutManager
bannerManager?.let {
val bannerCurrentPos = it.findFirstCompletelyVisibleItemPosition()
smoothScrollToPosition(bannerCurrentPos + 1)
}
}
}
private var bannerChangerDelayMilllis = 2000L
}
for brevity, assume whenever the toast is occuring, its going to scroll the 2nd dots indicator recyclerview .
This all seems to work in principle, but after sometimes the handler seems to fire twice or thrice , causing bad ux. sometimes it even goes berserks and stops showing any logs or anything and just makes the rails run infinetely very fast, like handler firing an autoscroll runner every millisecond.
handlers firing 2-3 times
So any help with this? i am assuming something is wrong at the implementation level, like firing handler events could be handled better?
Update:
thanks to #ADM , I got this working. I tweaked it as per my requirements, and had to forgo of circular scroll support in the reverse direction, but the given solution was enough to answer my query. thanks!
Handler is not an issue here its the Runnable. you are using and posting same Runnable each time thats why its getting piled up . You can not remove the previous call because you do not have a Tag or token to this delayed call . take a look at some of Handler's method like sendMessageDelayed these might help .
After giving it some thought i think you can move the Auto scroll part to SnapHelper. Not a full prove solution but i think it will work. You might have to put few checks in SnapHelper . Give it a try and let me know . i haven't tested it.
class AutoPagedSnapHelper(private var autoScrollInterval: Long) : PagerSnapHelper() {
private var recyclerView: RecyclerView? = null
private var currentPage = 0
private var isHold = false
private val autoScrollRunnable = Runnable {
recyclerView?.let {
if (recyclerView?.scrollState != RecyclerView.SCROLL_STATE_DRAGGING && !isHold) {
if (it.adapter != null) {
val lastPageIndex = (recyclerView?.adapter!!.itemCount - 1)
var nextIndex: Int
nextIndex = currentPage + 1
if (currentPage == lastPageIndex) {
nextIndex = 0
}
it.post {
val linearSmoothScroller = object : LinearSmoothScroller(recyclerView?.context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
linearSmoothScroller.targetPosition = nextIndex
(recyclerView?.layoutManager as LinearLayoutManager).startSmoothScroll(linearSmoothScroller)
}
}
} else {
postNextPage()
}
}
}
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
super.attachToRecyclerView(recyclerView)
if (this.recyclerView === recyclerView) {
return
}
if (autoScrollInterval != 0L) {
this.recyclerView = recyclerView
this.recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE || newState == RecyclerView.SCROLL_STATE_SETTLING) {
val itemPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
if (itemPosition != -1) {
currentPage = itemPosition
postNextPage()
}
}
}
})
postNextPage()
recyclerView?.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onInterceptTouchEvent(rv: RecyclerView, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isHold = true
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
isHold = false
}
}
return false
}
override fun onTouchEvent(rv: RecyclerView, event: MotionEvent) {}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
})
}
}
fun postNextPage() {
recyclerView?.handler?.removeCallbacks(autoScrollRunnable)
recyclerView?.postDelayed(autoScrollRunnable, autoScrollInterval)
}
companion object {
private const val MILLISECONDS_PER_INCH = 75f //default is 25f (bigger = slower)
}
}
This should take care of auto change page. You do not have to use scrollListener in Adapter. Give it a try.

RecyclerView scrolls to top when dragging item with ItemTouchHelper

I have this weird bug where my RecyclerView scrolls back to top position whenever I start dragging an item in it. It's inside ViewPager if that has any difference. You can see the behavior in .gif attached.
EDIT:
It seems that RecyclerView view scrolls to top when notifyItemMoved is called and it scrolls just as much for the first view to be at least partially displayed on screen.
View
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/accounts_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/view_account_list_item" />
Adapter
class AccountListAdapter(
private val onAccountClickListener: OnAccountClickListener) :
ListAdapter<Account, AccountListAdapter.ViewHolder>(
AccountDiffCallback()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(
inflater.inflate(
R.layout.view_account_list_item,
parent,
false
)
)
}
override fun getItemId(position: Int): Long {
return getItem(position).accountId.toLong()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), onAccountClickListener)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), OnItemDragged {
fun bind(account: Account, onAccountClickListener: OnAccountClickListener) {
itemView.account_name.text = account.name
itemView.setOnClickListener {
onAccountClickListener.onAccountClick(account)
}
}
override fun onItemSelected() {
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
R.color.background_contrast
)
)
}
override fun onItemClear() {
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
R.color.background
)
)
}
}
class AccountDiffCallback : DiffUtil.ItemCallback<Account>() {
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
return oldItem.accountId == newItem.accountId
}
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
return (oldItem.balance == newItem.balance
&& oldItem.annualReturn == newItem.annualReturn
&& oldItem.name == newItem.name)
}
}
interface OnAccountClickListener {
fun onAccountClick(account: Account)
}
interface OnItemDragged {
fun onItemSelected()
fun onItemClear()
}}
ItemTouchHelper
private fun setupListAdapter() {
accountListAdapter = AccountListAdapter(this)
accountListAdapter.setHasStableIds(true)
accounts_recycler_view.adapter = accountListAdapter
accounts_recycler_view.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
val accountTouchHelper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0
) {
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
accountViewHolder.onItemSelected()
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
accountViewHolder.onItemClear()
super.clearView(recyclerView, viewHolder)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos: Int = viewHolder.adapterPosition
val toPos: Int = target.adapterPosition
Collections.swap(_accountList, fromPos, toPos)
accountListAdapter.notifyItemMoved(fromPos, toPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
accountTouchHelper.attachToRecyclerView(accounts_recycler_view)
}
So the issues was that my Recyclerview was inside ViewPager which was inside a ConstraintLayout . View pager was constrained vertically with height set to 0dp but width was set to match_parent. All I needed was to constrain it horizontally with width set to 0dp and setHasFixedSize = true to RecyclerView
When calling notifyItemMoved for adapter, if RecyclerView is flexible, all items are redrawn and by default it focuses on the first item.
If you are using NestedScrollView just remove and instead of using other layouts use ConstraintLayout
Steps:
Use ConstrainLayout and RecylerView like this:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvDraggable"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_0"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note- you can also try to use:
viewBinding.recycler.setHasFixedSize(true)
If you are using notifyDataSetChanged() so just replace it by notifyItemMoved() in onMove() method

Android kotlin bottomSheetBehavior COLLAPSED after clik button

Hi i want tu COLLAPSED my bottomSheetBehavior after click button I do this :
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
But it doesn't work , what I do wrong
this is my bottomSheetBehavior :
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
Log.e("a","a")
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
val upperState = 0.66
val lowerState = 0.33
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_SETTLING ) {
if(slideOffset >= upperState){
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
if(slideOffset > lowerState && slideOffset < upperState){
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
if(slideOffset <= lowerState){
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
changeSize()
}
})
private fun changeSizer() {
val screenHeight = getScreenHeight(this)
bottomSheetBehavior.peekHeight = (screenHeight * 0.2).toInt()
val params: ViewGroup.LayoutParams = llBottomSheet.layoutParams
params.height = (screenHeight * 0.8).toInt()
llBottomSheet.layoutParams = params
}
Set fitToContents
bottomSheetBehavior.isFitToContents = false
Sets whether the height of the expanded sheet is determined by the height of its contents, or
if it is expanded in two stages (half the height of the parent container, full height of parent
container).
Default value is true. We have to set it to false to have option set height "manually".
Set peekHeight
Sets the height of the bottom sheet when it is collapsed.
Second parameter (true) is responsible for animating between the
old height and the new height.
bottomSheetBehavior.setPeekHeight(peekHeight, true)
Full working example
class MainActivity : AppCompatActivity() {
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private val bottomSheetHeight: Int by lazy {
bottom_sheet.height
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottomSheetBehavior = BottomSheetBehavior.from(bottom_sheet)
bottomSheetBehavior.isFitToContents = false
setInitValue()
}
/**
* Use listener, because at the beginning The UI has not been sized and laid out on the screen yet.
*/
private fun setInitValue() {
bottom_sheet.run {
viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (bottom_sheet.isShown) {
// Show only 25%
updatePeekHeight(0.25f)
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val factor: Float = when (item.itemId) {
R.id.option_full -> 1f
R.id.option_3_4 -> 0.75f
R.id.option_2_4 -> 0.50f
R.id.option_1_4 -> 0.25f
R.id.option_hide -> 0f
else -> 1f
}
updatePeekHeight(factor)
return true
}
private fun updatePeekHeight(factor: Float) {
val peekHeight: Int = (bottomSheetHeight * factor).toInt()
bottomSheetBehavior.setPeekHeight(peekHeight, true)
}
}
Demo

How to dismiss Bottom Sheet fragment when click outside in Kotlin?

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

Categories

Resources