0

I have a web view in my Android app with some buttons. When the use press one of the button I would like to present a PopupMenu positioned to that HTML button.

I use addJavascriptInterface to communicate from the web view back to the java code and I also send the position of the HTML element relative to the top left corner of the web view.

Now I would like to position the PopupMenu at this position. What are my options? PopupMenu is easy to use, but does not seem to be that easy to position. PopupWindows is easy to position, but makes it harder to use as a menu.

I have also created a temporary view overlaying the position in the web view and created the PopupMenu from this position. The view is positioned in the exact right place, but the PopupMenu still shows up at top left corner of the activity.

Any ideas?

Johan Nordberg
  • 3,621
  • 4
  • 33
  • 58
  • You would likely need to craft some Javascript that calls a method in a Javascript bridge that you define. Similar to this: http://stackoverflow.com/questions/8200945/how-to-get-html-content-from-a-webview – drhr Nov 26 '16 at 20:24
  • I've already got that in place, but the popupmenu is displayed in the wrong place anyway. – Johan Nordberg Nov 26 '16 at 21:31

3 Answers3

3

In case anybody's wondering, here's a rough working solution. The element in the page has a left margin of 30%. After you scroll the content into view, you tap the txt div element in the WebView, and the PopupWindow will display where the element is, relative to the WebView.

src/main/your.package/MainActivity.java:

This attaches the javascript interface to the WebView with the name "Android".

public class MainActivity extends AppCompatActivity {

    private WebView webview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webview = (WebView) findViewById(R.id.webview);
        webview.getSettings().setJavaScriptEnabled(true);
        webview.addJavascriptInterface(new MyJavaScriptInterface(this), "Android");

        try {
            String content = new Scanner(getAssets().open("sample.html")).useDelimiter("\\A").next();
            webview.loadData(content, "text/html", null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void managePopup(int px, int py) {
        View view = MainActivity.this.getLayoutInflater().inflate(R.layout.view_popup, null);
        PopupWindow popup = new PopupWindow(view,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        popup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        popup.setOutsideTouchable(true);
        popup.showAsDropDown(webview, px, -webview.getHeight() + py);
    }

    private class MyJavaScriptInterface {
        private Context ctx;

        public MyJavaScriptInterface(Context ctx) {
            this.ctx = ctx;
        }

        @JavascriptInterface
        public void showMenu(int x, int y) {
            final int px = (int) (x * ctx.getResources().getDisplayMetrics().density);
            final int py = (int) (y * ctx.getResources().getDisplayMetrics().density);

            Handler mainHandler = new Handler(Looper.getMainLooper());
            Runnable myRunnable = new Runnable() {
                @Override
                public void run() {
                    managePopup(px, py);
                }
            };
            mainHandler.post(myRunnable);
        }
    }
}

Kotlin version src/main/your.package/MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        webView = findViewById<View>(R.id.webview) as WebView
        webView.settings.javaScriptEnabled = true
        webView.addJavascriptInterface(MyJavaScriptInterface(this), "Android")
        webView.loadUrl("file:///android_asset/sample.html")
    }

    private fun managePopup(px: Int, py: Int) {
        val view = this@MainActivity.layoutInflater.inflate(R.layout.view_popup, null)
        val popup = PopupWindow(
            view,
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        popup.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
        popup.isOutsideTouchable = true
        popup.showAsDropDown(webView, px, -webView!!.height + py)
    }

    private inner class MyJavaScriptInterface(private val ctx: Context) {
        @JavascriptInterface
        fun showMenu(x: Int, y: Int) {
            val px = (x * ctx.resources.displayMetrics.density).toInt()
            val py = (y * ctx.resources.displayMetrics.density).toInt()
            val mainHandler = Handler(Looper.getMainLooper())
            val myRunnable = Runnable { managePopup(px, py) }
            mainHandler.post(myRunnable)
        }
    }
}

src/main/assets/sample.html:

Here is some sample HTML that gets loaded into the WebView from the assets directory. It has a click listener on the div.

<html>
<style>
#txt {
    margin-left: 30%;
    margin-top: 400px;
    margin-bottom: 1000px;
    border: 1px red solid;
    display: inline-block;
}
</style>
<body>
    <div id="txt">some text</div>
    <script>
        var element = document.getElementById("txt");
        element.addEventListener('click', function (event) {
            var rect = element.getBoundingClientRect();
            Android.showMenu(rect.left, rect.top + rect.height);
        });
    </script>
</body>
</html>

src/main/res/layout/view_popup.xml:

This is a sample menu. I'm not using PopupMenu in this example, preferring PopupWindow#showAsDropDown(View,int,int) instead.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#000">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#FFF"
        android:text="Menu Item 1" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#FFF"
        android:text="Menu Item 2" />

</LinearLayout>

Result:

Here is a GIF of it:

visual demo of the code above

Guy West
  • 424
  • 4
  • 17
drhr
  • 2,261
  • 2
  • 17
  • 35
  • Actually this is kind the opposite of what I asked. :) I wanted to display a native PopupMenu triggered from HTML. But if I understand your code correctly you are display HTML as content of a PopupWindow, triggered by a native button. – Johan Nordberg Nov 27 '16 at 13:07
  • Ignore the showHTML name. I'll edit that. It's just passing the x, y position of the html element. – drhr Nov 27 '16 at 16:21
  • I see what you mean with the button. It wouldn't change much, you'd just have the button in html with a click callback that calls into your JavaScript interface function, instead of having a native button load javascript. And `view_popup.xml` can contain the menu. I tend to use PopupWindow instead of PopupMenu, due to its customizability. – drhr Nov 27 '16 at 17:25
  • I updated the answer to include more of the solution, and to use an HTML button to call out to Java to create the popup. – drhr Nov 27 '16 at 18:36
  • That's more what I was looking for. Think you. – Johan Nordberg Nov 27 '16 at 21:24
  • One advantage of using PopupMenu though is that it get dismissed when you click outside it or scrolling. I ended up with something similar to what you are doing, but created a temporary transparent view in the same position as the clicked html element and then showing the PopupMenu from this view. – Johan Nordberg Nov 27 '16 at 21:26
  • @JohanNordberg Yep - either way, it's a bit hacky. I edited the code sample above to make the `PopupWindow` dismiss when you tap outside, and updated the GIF. I removed the local variable `popup` and added some code that sets the background and allows outside touches, which together, oddly enough, forces it to dismiss. You'd think they'd have an API that specifically addresses this use-case, but here we are. Anyway - this should let you get rid of the extraneous anchor view if you wanted to. – drhr Nov 27 '16 at 22:18
1

If it's of use to anyone I have some functionality to solve this in a library fork I have which gives the ability to extract screen rectangles using jquery and standard id, class, and tag attributes provided here. Feel free to fork or copy and paste since the primary focus of the library is not on solving this problem.

getWebJQueryRects - returns a list of rectangles based on a list of jquery strings

getWebIDRects - the same as above but using document.getElementById

getWebTagRects - the same as above but using document.getElementsByTagName

getWebClassRects - the same as above but using document.getElementsByClassName

The rectangles returned represents on screen coordinates in pixels. In the posters scenario this could be used to find the buttons onscreen location.

Aditya Prakash
  • 1,549
  • 2
  • 15
  • 31
dirtcubed
  • 35
  • 1
  • 8
0

SOLVED by creating and showing the PopupMenu in the views OnLayoutChangeListener callback.

Johan Nordberg
  • 3,621
  • 4
  • 33
  • 58