How to show a customized Popup window near the touch location, like what we have when we use ContextMenu?
Asked Answered
H

2

4

Background

Seeing that it's not officially possible to have a context menu which has a customized view or even icons for its rows (here), I decided to create my own solution (of custom view that acts like it).

The problem

When using a context menu on a RecyclerView, the touch position matters, so if you long touch an item, the context menu will try to appear near the touch location (sample taken from here), and without me giving this information (meaning via OnClickListener or onLongClickListener ) :

enter image description here

However, I can't find how to do this in the more basic classes.

What I've tried

Showing a PopupWindow can be done via long touch, as such:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val inflater = LayoutInflater.from(context)
    val holder = ViewHolder(inflater.inflate(R.layout.list_item_main, parent, false))
    holder.itemView.setOnLongClickListener {
        val contextMenuView=inflater.inflate(R.layout.context_menu,null)
        val popupWindow = PopupWindow(contextMenuView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true)
        popupWindow.showAsDropDown(holder.itemView,0,0);
        true
    }
    return holder
}

And, if you want to have a nice background for it instead of being transparent, you could use a workaround, of ListPopupWindow, and if you don't want a list, you can just set its promptView , as such (code available here) :

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val inflater = LayoutInflater.from(context)
    val holder = ViewHolder(inflater.inflate(R.layout.list_item_main, parent, false))
    val maxAllowedPopupWidth = context.resources.displayMetrics.widthPixels * 90 / 100
    holder.itemView.setOnLongClickListener {
        val contextMenuView = inflater.inflate(R.layout.context_menu, null)
        val listPopupWindow = ListPopupWindow(context)
        contextMenuView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
        val width = min(maxAllowedPopupWidth, contextMenuView.measuredWidth)
        listPopupWindow.setPromptView(contextMenuView)
        listPopupWindow.setContentWidth(width)
        listPopupWindow.anchorView = it
        listPopupWindow.show()
        true
    }
    return holder
}

I'm not sure about the max width that I've calculated, because I can't find what's the maximum size that a popup can have. I know that the context menu has some maximum and then it just truncates the text for some reason. Maybe it's the same as of Dialog? Except that for dialog I could find a maximum width, yet I've found a minimal : windowMinWidthMajor and windowMinWidthMinor.

But back to the issue: I can't find any function here that's related to putting the popup near the touch location.

So this is what I get, for example:

enter image description here

The questions

  1. How to set the popup window to appear near the touch location on the screen, without even handling onTouch event, as done on the sample using ContextMenu ?

  2. Does the context menu (or similar) have some attribute that I can get, to set as the max size for what I show (in short: a default max width) ? If so, how do I use it? How can I set the width&height to consider the one of the inflated view?

Hhour answered 31/7, 2019 at 12:17 Comment(0)
T
2

It has been a while since I have done this, but I think we had the same problem. Let me see if I can answer.

Not being able to make custom context menus for the EditText was one of the main reasons that I finally decided to create a library with custom components for Mongolian. Although the vertical Mongolian parts won't be useful to you, the concepts should be the same for other custom popups.

Here are a couple screenshots of what I have:

This one is a custom EditText that used a custom popup menu. It takes the user touch location to place the popup location.

enter image description here

The next one is a more general demonstration of different ways to set the popup location.

enter image description here

Both of these demos are included in the mongol-library demo app.

My custom menu was a PopupWindow subclass. You can find the source code here.

The way I placed it at a particular location was to use the showAtLocation method, which as I recall is just a normal method on PopupWindow:

private void showMongolContextMenu(MongolMenu menu, int xTouchLocation, int yTouchLocation) {
    float paddingPx = CONTEXT_MENU_TOUCH_PADDING_DP * getResources().getDisplayMetrics().density;
    Rect menuSize = menu.getDesiredSize();
    int y = yTouchLocation - menuSize.height() - (int) paddingPx;
    menu.showAtLocation(this, Gravity.NO_GRAVITY, xTouchLocation, y);
}

That code is from here.

Oh, yes, and I also used this in custom keyboards:

enter image description here

See these classes for more:

Thomson answered 1/8, 2019 at 15:31 Comment(2)
Nice, but can you please extract what's needed into a single repository, and use a sample for it. This one's purpose is for handling a language... I've also noticed that maybe you also support keep pressing to choose (called "Drag-to-Select" : androidpolice.com/2013/11/16/… ). Is it true?Hhour
@androiddeveloper, The code I referenced works well enough for my purposes so I don't have any need to extract into a separate repo, but you are welcome to do that. It doesn't support drag to select.Thomson
W
0

How to set the popup window to appear near the touch location on the screen?

For this purpose, you need to find exact coordination where the user has touch the view so you need to use setOnTouchListener()

Try this way

You this PopupWindowHelper

PopupWindowHelper

import android.view.Gravity
import android.graphics.drawable.BitmapDrawable
import android.content.Context
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.LinearLayout
import android.widget.PopupWindow

class PopupWindowHelper(private val ctx: Context) {
    private val tipWindow: PopupWindow?
    private val contentView: View
    private val inflater: LayoutInflater

    internal val isTooltipShown: Boolean
        get() = tipWindow != null && tipWindow.isShowing


    init {
        tipWindow = PopupWindow(ctx)

        inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        contentView = inflater.inflate(R.layout.popup_window, null)
    }

    internal fun showToolTip(anchor: View, event: MotionEvent) {

        tipWindow!!.height = LinearLayout.LayoutParams.WRAP_CONTENT
        tipWindow.width = LinearLayout.LayoutParams.WRAP_CONTENT

        tipWindow.isOutsideTouchable = true
        tipWindow.isTouchable = true
        tipWindow.isFocusable = true
        tipWindow.setBackgroundDrawable(BitmapDrawable())

        tipWindow.contentView = contentView

        val screenPos = IntArray(2)
        anchor.getLocationOnScreen(screenPos)

        val anchorRect =
            Rect(screenPos[0], screenPos[1], screenPos[0] + anchor.width, screenPos[1] + anchor.height)

        contentView.measure(
            LinearLayout.LayoutParams.WRAP_CONTENT,
            LinearLayout.LayoutParams.WRAP_CONTENT
        )

        val contentViewHeight = contentView.measuredHeight
        val contentViewWidth = contentView.measuredWidth

        val positionX = anchorRect.centerX() - contentViewWidth / 2
        val positionY = anchorRect.bottom - anchorRect.height() / 2

        tipWindow.showAtLocation(anchor, Gravity.NO_GRAVITY, event.x.toInt(), positionY)

    }

    internal fun dismissTooltip() {
        if (tipWindow != null && tipWindow.isShowing)
            tipWindow.dismiss()
    }


}

MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        myRecyclerView.layoutManager=LinearLayoutManager(this)
        myRecyclerView.setHasFixedSize(true)
        myRecyclerView.adapter=DataAdapter(this)
    }
}

DataAdapter

import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.row_layout.view.*
import android.view.MotionEvent
import android.view.View.OnTouchListener

class DataAdapter(context: Context) :
    RecyclerView.Adapter<DataAdapter.ViewHolder>() {
    val mContext = context
    private var lastTouchDown: Long = 0
    private val CLICK_ACTION_THRESHHOLD = 200

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view =
            LayoutInflater.from(mContext)
                .inflate(R.layout.row_layout, parent, false)

        view.setOnTouchListener { myView, event ->
            when (event?.action) {
                MotionEvent.ACTION_DOWN -> lastTouchDown = System.currentTimeMillis()
                MotionEvent.ACTION_UP -> if (System.currentTimeMillis() - lastTouchDown < CLICK_ACTION_THRESHHOLD) {
                    val popupWindowHelper = PopupWindowHelper(mContext)
                    myView?.let {
                        popupWindowHelper.showToolTip(
                            it
                            , event
                        )
                    }
                }
            }
            true
        }
        return ViewHolder(view)
    }

    override fun getItemCount(): Int {
        return 30
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        holder.tvDescription.text = "Row Description $position"
        holder.tvTitle.text = "Row Title $position"

    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvTitle = itemView.tvTitle
        val tvDescription = itemView.tvDescription
    }
}

You can find complete code from my GitHub repo

Wilmerwilmette answered 2/8, 2019 at 6:4 Comment(3)
The GIF animation seems like you did it, but I couldn't import the project to try it out. Please check it out.Hhour
@androiddeveloper please check I have updated the GitHub repoWilmerwilmette
Seems to work very well, but what I've found (and too bad I wasn't clear on this) is that what Context menu has to offer does not require you to have onTouchListener. It knows where the user has touched without me telling this to it, meaning using just onClickListener or onLongClickListener. Do you know how this is done?Hhour

© 2022 - 2024 — McMap. All rights reserved.