4

I have anchor links (example: <a href="#P12"/>) in my HTML which I load into a WebView and this WebView is inside a NestedScrollView. When I temporarily disable the NestedScrollView because everyone advises doing that when anchor links are not working properly, these anchor links work perfectly. However, my layout does need this NestedScrollView because of multiple reasons:

  1. It has a CollapsingToolbarLayout and app:layout_behavior="@string/appbar_scrolling_view_behavior needs to be set;
  2. There are layout elements above and below the WebView;
  3. These elements have to scroll together as a whole.

The expected behaviour is that the content jumps to the correct part of the web page when an anchor link is clicked. The actual behaviour is that the content jumps to some random place and that you can no longer scroll through the entire page (either the top or bottom is cut off).

Is there a possibility to make these anchor links work inside my WebView that is inside a NestedScrollView?

On StackOverflow I have found one question that is similar to this one, but unfortunately it has no answers and it is from 2017 so I highly doubt anyone will answer it soon.

Mr. Robot
  • 397
  • 1
  • 14
  • I've was fighting for literally few working weeks to get `WebView` working properly with `layout_behavior`, without luck... ended with fixed `Toolbar` sadly. also note that `WebView` placed inside `NestedScrollView` causes `WebView` to draw whole content (e.g. lazy load of images will fire for all images), which is very unefficient – snachmsm Oct 14 '20 at 13:54
  • found my question inthis topic in [HERE](https://stackoverflow.com/questions/57654466/nestedwebview-working-properly-with-scrollingviewbehavior) – snachmsm Oct 14 '20 at 13:55

3 Answers3

1

Since it frustrates me when I stumble upon a question without answers, I will answer my own question, so others will know how I resolved this issue.

Bad news is, this particular issue cannot be resolved. At least, not yet in 2021.

What the company I am working at and the client decided to do is change the entire implementation of this screen. The entire screen is now a WebView, instead of just a part of the screen being a WebView.

Mr. Robot
  • 397
  • 1
  • 14
1

I couldn't do it with the above methods provided so i tried a diff approach

  1. enable JS with your webview webView.getSettings().setJavaScriptEnabled(true);

  2. Add and define javascript interface keyword so that it be accessed later from javascript

    //Adding Javascript interface so that javascript can access annotated functions in this Class
    webView.addJavascriptInterface ( this , "android" );`
    
  3. then create a class that extends WebViewClient

  4. enable html5 features and add some javascript code

    private void loadWebViewDatafinal(WebView wv) {
       WebSettings ws=wv.getSettings();
    
       ws.setJavaScriptEnabled(true);
       ws.setAllowFileAccess(true);
    
     //this try block enables html5
    
    try {
        Log.e("WEB_VIEW_JS", "Enabling HTML5-Features");
        Method m1=WebSettings.class.getMethod("setDomStorageEnabled", new Class[]{Boolean.TYPE});
        m1.invoke(ws, Boolean.TRUE);
    
        Method m2=WebSettings.class.getMethod("setDatabaseEnabled", new Class[]{Boolean.TYPE});
        m2.invoke(ws, Boolean.TRUE);
    
        Method m3=WebSettings.class.getMethod("setDatabasePath", new Class[]{String.class});
        m3.invoke(ws, "/data/data/" + mContext.getPackageName() + "/databases/");
    
        Method m4=WebSettings.class.getMethod("setAppCacheMaxSize", new Class[]{Long.TYPE});
        m4.invoke(ws, 1024 * 1024 * 8);
    
        Method m5= WebSettings.class.getMethod("setAppCachePath", new Class[]{String.class});
        m5.invoke(ws, "/data/data/" + mContext.getPackageName() + "/cache/");
    
        Method m6=WebSettings.class.getMethod("setAppCacheEnabled", new Class[]{Boolean.TYPE});
        m6.invoke(ws, Boolean.TRUE);
    
        Log.e("WEB_VIEW_JS", "Enabled HTML5-Features");
    } catch (Exception e) {
        Log.e("WEB_VIEW_JS", "Reflection fail", e);
    }
    
     //this javascipt code give me the offset from the top of the link which
     //i send to the android java class using the keyword 'android' which was defined above .
    
      wv.loadUrl("javascript:(function(){\n"+
        // anchor tag and perform in click on anchor click
    
            "var anchors= document.getElementsByTagName(\"a\");\n" +
            "\n" +
            "for( let i = 0;i < anchors.length;i+=1)\n" +
            "{\n" +
            "    let currentanchor=anchors[i];\n" +
            "    \n" +
            "    currentanchor.onclick= function(){\n" +
    
                    " if (this.hash !== \"\") {\n" +
                    "\n" +
                    "        event.preventDefault();\n" +
                    "\n" +
                    "        var hash = this.hash;\n" +
                    "\n" +
                    "        var div = document.getElementById(hash.replace('#',''));\n" +
                    "\n" +
    
    
     //this is the line by which we can link javascript values from html and Java 
    
    
                    "        android.OnAnchorClickScroll(div.offsetTop);\n"+
                    "\n" +
                    "                    };\n" +
                    "\n" +
    
    
            "       }" +
            "}"+
      "})();");
    

    }

  5. Now add a function in the class where u have defined that android keyword and has access to the scrollview to want to scroll

       @JavascriptInterface
        public void OnAnchorClickScroll(float pos) {
           GlobalClassForFunctions.getInstance ( ).PrintMessageOnTheConsole ( "THIS IS Anchor pos --" + pos );
           int offsetInt = (int)getDensityIndependentPixels(pos, mContext);
           scrollView.smoothScrollTo(0,webView.getTop()+offsetInt);
    
       }
    

and this is the function being used

    // this function is to be used whenever you are getting pixels from javascript and want them to be converted in android dp
//We need this, because the measured pixels in the WebView don't use density of the screen
public float getDensityIndependentPixels(float pixels, Context context){
    return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            pixels,
            context.getResources().getDisplayMetrics()
    );
}
0

Here's my working solution. Clicking on an anchor link will scroll the ScrollView appropriately. Clicking back will scroll back to the top.

class FirstFragment : Fragment() {

    private var _binding: FragmentFirstBinding? = null

    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        _binding = FragmentFirstBinding.inflate(inflater, container, false)
        return binding.root

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val wvClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                if (url != null && url.contains("#")) {
                    val anchor = url.substring(url.lastIndexOf("#") + 1)
                    view?.evaluateJavascript(
                        "(function() { " +
                                "var element = document.getElementById('$anchor');" +
                                "if (element) {" +
                                "    var rect = element.getBoundingClientRect();" +
                                "    return rect.top;" +
                                "}" +
                                "return -1;" +
                                "})();"
                    ) { value ->
                        val anchorY = value.toFloat().toInt()
                        if (anchorY != -1) {
                            // scroll to to anchor target
                            binding.svContent.scrollTo(0, anchorY.toPx())
                        }
                    }
                } else {
                    // Scroll to top
                    binding.svContent.scrollTo(0, 0)
                }
            }
        }

        with (binding.myWebview) {
            settings.javaScriptEnabled = true
            webViewClient = wvClient
            loadUrl("https://html.com/anchors-links/")
        }
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        val callback: OnBackPressedCallback =
            object : OnBackPressedCallback(true) {
                override fun handleOnBackPressed() {
                    if (binding.myWebview.canGoBack()){
                        binding.myWebview.goBack()
                    }
                }
            }
        requireActivity().onBackPressedDispatcher.addCallback(
            this,
            callback
        )
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

fun Int.toPx() : Int = (this * Resources.getSystem().displayMetrics.density).toInt()
aaronmarino
  • 3,723
  • 1
  • 23
  • 36