In my case I want to open PopupWindow by long press on ViewHolder item and process motion event in this window without removing finger. How can I achieve this?
I trying to open CustomPopupWindow by follow:
override fun onBindViewHolder(holder: Item, position: Int) {
val item = items[position]
holder.bindView(testItem)
holder.itemView.view.setOnLongClickListener {
val inflater = LayoutInflater.from(parent?.context)
val view = inflater.inflate(R.layout.popup_window, null)
val popupMenu = CustomPopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popupMenu.elevation = 5f
popupMenu.showAsDropDown(holder.itemView.view)
true
}
}
and after that disable scrolling in RecyclerView:
class CustomLayoutManager(context: Context) : LinearLayoutManager(context) {
var scrollEnabled: Boolean = true
override fun canScrollVertically(): Boolean {
return scrollEnabled
}
}
Here my CustomPopupWindow:
class CustomPopupWindow(contentView: View?, width: Int, height: Int) : PopupWindow(contentView, width, height), View.OnTouchListener {
init {
contentView?.setOnTouchListener(this)
setTouchInterceptor(this)
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.i("Touch", "Touch")
}
MotionEvent.ACTION_MOVE -> {
Log.i("Touch", "Event {${event.x}; ${event.y}}")
}
MotionEvent.ACTION_UP-> {
Log.i("Touch", "Up")
}
}
return true
}
}
In this case onTouch() event never called in CustomPopupWindow only if I remove finger and tap again.
Thanks advance!
SOLVED
I solved this by adding a touch listener to the anchor view:
holder.itemView.view.setOnLongClickListener {
val inflater = LayoutInflater.from(parent?.context)
val view = inflater.inflate(R.layout.popup_window, null)
val popupMenu = CustomPopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popupMenu.elevation = 5f
it.setOnTouchListener(popupMenu) // solution
popupMenu.showAsDropDown(it)
true
}
Thanks #Brucelet
If you can refactor to using a PopupMenu, then I think PopupMenu.getDragToOpenListener() will do what you want. Similar for ListPopupWindow.createDragToOpenListener().
You could also look at the implementation of those methods for inspiration in creating your own.
Related
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.
In my RecyclerView OnLongClicking an item I want to change that item's layout by setting some TextViews to View.GONE and others to View.VISIBLE. Everything works except that when I long press the item and the layout changes my RecyclerView scrolls to top and I can no longer see the LongPressed view if it was at the bottom.
This is ListAdapter that I wrote:
class AssetsListAdapter(
private val onAssetClickListener: OnAssetClickListener,
private val onAssetLongClickListener: OnAssetLongClickListener
) :
ListAdapter<Asset, AssetsListAdapter.ViewHolder>(
AssetDiffCallback()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(
inflater.inflate(
R.layout.list_item,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), onAssetClickListener, onAssetLongClickListener)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(
asset: Asset,
onAssetClickListener: OnAssetClickListener,
onAssetLongClickListener: OnAssetLongClickListener
) {
itemView.item_name.text = asset.name
// Set icons relatively to category
when (asset.category) {
"Cash" -> itemView.item_image.setImageResource(R.drawable.ic_cash)
"Bank Account" -> itemView.item_image.setImageResource(R.drawable.ic_bank)
"Investment" -> itemView.item_image.setImageResource(R.drawable.ic_invest)
"Salary" -> itemView.item_image.setImageResource(R.drawable.ic_job)
}
itemView.setOnClickListener {
onAssetClickListener.onAssetClick(asset)
}
// On long click listeners pulls up quick action options
itemView.setOnLongClickListener {
view.item_end_text.visibility = View.GONE
view.quick_actions_layout.visibility = View.VISIBLE
onAssetLongClickListener.onAssetLongClick(asset, itemView)
true
}
}
}
class AssetDiffCallback : DiffUtil.ItemCallback<Asset>() {
override fun areItemsTheSame(oldItem: Asset, newItem: Asset): Boolean {
return oldItem.assetId == newItem.assetId
}
override fun areContentsTheSame(oldItem: Asset, newItem: Asset): Boolean {
return oldItem == newItem
}
}
interface OnAssetClickListener {
fun onAssetClick(asset: Asset)
}
interface OnAssetLongClickListener {
fun onAssetLongClick(asset: Asset, view: View)
}
}
Ok, so I found out that if you set View.GONE the whole item is redrawn and it resets the RecyclerView? Because setting it to View.INVISIBLE solves the issue.
itemView.setOnLongClickListener {
view.item_end_text.visibility = View.GONE
view.quick_actions_layout.visibility = View.VISIBLE
onAssetLongClickListener.onAssetLongClick(asset, itemView)
true
}
to
itemView.setOnLongClickListener {
view.item_end_text.visibility = View.INVISIBLE
view.quick_actions_layout.visibility = View.VISIBLE
onAssetLongClickListener.onAssetLongClick(asset, itemView)
true
}
I'm trying to do a proof of concept where a Webview loads local HTML files and users are able to swipe left/right to go to the next file, all while in fullscreen. I'm utilizing ViewPager2 and normal Webviews. I have that part working, but what I want to do is, upon the user doing a single tap, show or hide the toolbar, status bar and navigation controls. Right now I have the setOnTouchListener code on the viewPager, but it looks like the touch events are being consumed by the webview.
How can I accomplish where a single tap would do a toggle between fullscreen and non-fullscreen mode, without disrupting ViewPager paging and long press in the webview?
Here is most of my code. I'm leaving parts out for brevity that should not be related.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewPager.setOnTouchListener { _, event ->
when(event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = event.rawX
initialY = event.rawY
moved = false
}
MotionEvent.ACTION_MOVE -> {
if (event.rawX != initialX || event.rawY != initialY) {
moved = true
}
}
MotionEvent.ACTION_UP -> {
if (!moved) {
toggle()
}
}
}
true
}
viewPager.adapter = MyAdapter(items, this)
}
}
class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.view_pager_item, parent, false) as WebView
return MyViewHolder(view, items)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return items.size
}
}
class MyViewHolder (private val webView: WebView, private val items: List<String>) :
RecyclerView.ViewHolder(webView) {
internal fun bind(position: Int) {
webView.loadUrl("file://" + items[position])
}
}
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
}
}
Okay, there's a weird thing happening in me.
I have an ImageButton named tab_btn from other layout which I imported and set onTouchListener which is working.
package com.xx
import kotlinx.android.synthetic.main.tab_btn_layout.*
import kotlinx.android.synthetic.main.btnNext_layout.*
class EventDetails : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_event_details)
tab_btn.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
if (event!!.action == MotionEvent.ACTION_DOWN) {
val icon: Drawable = ContextCompat.getDrawable(applicationContext, R.drawable.talk_bt_tab)
icon.setColorFilter(Color.GRAY,PorterDuff.Mode.MULTIPLY)
tab_btn.setImageDrawable(icon)
}else if (event!!.action == MotionEvent.ACTION_UP) {
tab_btn.clearColorFilter()
}
return true
}
})
btnNext.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(p0: View?, ev: MotionEvent?): Boolean {
if (ev!!.action == MotionEvent.ACTION_DOWN){
val icon: Drawable = ContextCompat.getDrawable(applicationContext, R.drawable.layer_bt_next)
icon.setColorFilter(Color.GRAY,PorterDuff.Mode.MULTIPLY)
btnNext.setImageDrawable(icon)
}else if(ev!!.action == MotionEvent.ACTION_UP){
btnNext.clearColorFilter()
}
return true
}
})
}
}
and below that I have another ImageButton from other layout named btnNext. I set the same OnTouchListener on it. But it gives me error .
And btnNext gives me error:
Attempt to invoke virtual method 'void android.widget.ImageButton.setOnTouchListener(android.view.View$OnTouchListener)' on a null object reference
note: I have imported both layout of the Image button. tab_btn is working but btnNext is not working.
Okay, I solved my own problem.
The real suspect is that my btnNext is in the fragment(viewpager) which I have to inflate first the rootview in oncreateview method.
note: you cannot access the element of the fragment without inflating it.
here's ma code:
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
val rootView = inflater!!.inflate(R.layout.btnNext_layout, container, false)
var next : ImageButton = rootView.findViewById(R.id.btnNext)
next.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(p0: View?, ev: MotionEvent?): Boolean {
if (ev!!.action == MotionEvent.ACTION_DOWN){
val icon: Drawable = ContextCompat.getDrawable(activity.applicationContext, R.drawable.layer_bt_next)
icon.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY)
btn_next.setImageDrawable(icon)
}else if (ev!!.action == MotionEvent.ACTION_UP){
val icon: Drawable = ContextCompat.getDrawable(activity.applicationContext, R.drawable.layer_bt_next)
icon.setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)
btn_next.setImageDrawable(icon)
}
return true
}
})
}