Open URL in WebView instead of default Browser

Himanshu Agarwal picture Himanshu Agarwal · Apr 9, 2014 · Viewed 8.5k times · Source

I am creating simple Webview application with some links on textview and open those links in webview instead of default browser. My textview contains various different URLS and i am trying to open each link in webview of my app.

Here code:

tv.setText("www.google.com  www.facebook.com  www.yahoo.com");
tv.setMovementMethod(LinkMovementMethod.getInstance());;
tv.setText(Html.fromHtml(tv.getText().toString()));
Linkify.addLinks(tv, Linkify.WEB_URLS);


WebViewClient yourWebClient = new WebViewClient()
       {
           // Override page so it's load on my view only
           @Override
           public boolean shouldOverrideUrlLoading(WebView  view, String  url)
           {
            // This line we let me load only pages inside Firstdroid Webpage
            if ( url.contains("www") == true )
               // Load new URL Don't override URL Link
               return false;

            // Return true to override url loading (In this case do nothing).
            return true;
           }
       };

wv.getSettings().setJavaScriptEnabled(true);   
    wv.getSettings().setSupportZoom(true);      

    wv.getSettings().setBuiltInZoomControls(true); 
    wv.setWebViewClient(yourWebClient);

    // Load URL
    wv.loadUrl(url);

Already tried with this, this and this examples but couldn't solve my issue with multiple links in the textview. Please help me to solve this problem. Thanks for your help.

Edit My Textview contains strings like:

hello xyz some more statements... xyz.com/abc/
hello xyz some more statements... xyz.com/abc/
hello xyz some more statements... xyz.com/abc/
hello xyz some more statements... xyz.com/abc/

Like this it has many strings and multiple URL's

Answer

Emanuel Moecklin picture Emanuel Moecklin · Apr 18, 2014

The following problems need to be solved:

  1. Linkify the TextView
  2. Find a way to listen to a click on a link in the TextView
  3. Get the url of the clicked link and load it in the WebView
  4. Optional: make the TextView clickable without losing the ability to select text
  5. Optional: handle formatted text in the TextView (different text sizes and styles)

#1 Linkify the TextView

That's the easiest problem and you already solved that one. I suggest to do it this way:

String text = "These are some sample links:\nwww.google.com\nwww.facebook.com\nwww.yahoo.com";
Spannable spannable = new SpannableString( Html.fromHtml(text) );
Linkify.addLinks(spannable, Linkify.WEB_URLS);

I'm using a Spannable here to solve problem #2.

#2 + #3 Listen to clicks on links and open them in the WebView

To find out when a link is clicked and retrieve the URL we have to open, we replace all URLSpans in the TextView by our LinkSpan (that's why we need a Spannable):

URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
for (URLSpan urlSpan : spans) {
    LinkSpan linkSpan = new LinkSpan(urlSpan.getURL());
    int spanStart = spannable.getSpanStart(urlSpan);
    int spanEnd = spannable.getSpanEnd(urlSpan);
    spannable.setSpan(linkSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    spannable.removeSpan(urlSpan);
}

Our LinkSpan simply grabs the clicked url and opens it in the WebView:

private class LinkSpan extends URLSpan {
    private LinkSpan(String url) {
        super(url);
    }

    @Override
    public void onClick(View view) {
        String url = getURL();
        if (mWebView != null && url != null) {
            mWebView.loadUrl(url);
        }
    }
}

Now obviously we have to keep a reference to the WebView in an instance variable to make this work. To make this answer as short as possible I chose to define LinkSpan as an inner class but I'd recommend to define it as a top-level. Register a listener or pass the WebView as a parameter to the constructor instead.

Without setting the MovementMethod to LinkMovementMethod the TextView won't open links at all:

tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(spannable, BufferType.SPANNABLE);

Last but not least let's make sure the WebView doesn't start the browser but loads the page within the app:

mWebView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // we handle the url ourselves if it's a network url (http / https) 
        return ! URLUtil.isNetworkUrl(url);
    }
});

#4 Clickable and selectable TextView + #5 formatted Text

If the MovementMethod is set to LinkMovementMethod you can click links but you can't select text any more (you need ArrowKeyMovementMethod for that). To solve this I created an custom MovementMethod class that inherits from ArrowKeyMovementMethod and adds the ability to click links. On top of that it is able to deal with formatted text. So if you decide to use different font sizes and styles in the TextView the following MovementMethod will have it covered (works with EditTexts as well):

/**
 * ArrowKeyMovementMethod does support selection of text but not the clicking of links.
 * LinkMovementMethod does support clicking of links but not the selection of text.
 * This class adds the link clicking to the ArrowKeyMovementMethod.
 * We basically take the LinkMovementMethod onTouchEvent code and remove the line
 *      Selection.removeSelection(buffer);
 * which deselects all text when no link was found.
 */
public class EnhancedLinkMovementMethod extends ArrowKeyMovementMethod {

    private static EnhancedLinkMovementMethod sInstance;

    private static Rect sLineBounds = new Rect();

    public static MovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new EnhancedLinkMovementMethod();
        }
        return sInstance;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            int index = getCharIndexAt(widget, event);
            if (index != -1) {
                ClickableSpan[] link = buffer.getSpans(index, index, ClickableSpan.class);
                if (link.length != 0) {
                    if (action == MotionEvent.ACTION_UP) {
                        link[0].onClick(widget);
                    }
                    else if (action == MotionEvent.ACTION_DOWN) {
                        Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
                    }
                    return true;
                }
            }
            /*else {
                Selection.removeSelection(buffer);
            }*/

        }

        return super.onTouchEvent(widget, buffer, event);
    }

    private int getCharIndexAt(TextView textView, MotionEvent event) {
        // get coordinates
        int x = (int) event.getX();
        int y = (int) event.getY();
        x -= textView.getTotalPaddingLeft();
        y -= textView.getTotalPaddingTop();
        x += textView.getScrollX();
        y += textView.getScrollY();

        /*
         * Fail-fast check of the line bound.
         * If we're not within the line bound no character was touched
         */
        Layout layout = textView.getLayout();
        int line = layout.getLineForVertical(y);
        synchronized (sLineBounds) {
            layout.getLineBounds(line, sLineBounds);
            if (! sLineBounds.contains(x, y)) {
                return -1;
            }
        }

        // retrieve line text
        Spanned text = (Spanned) textView.getText();
        int lineStart = layout.getLineStart(line);
        int lineEnd = layout.getLineEnd(line);
        int lineLength = lineEnd - lineStart;
        if (lineLength == 0) {
            return -1;
        }
        Spanned lineText = (Spanned) text.subSequence(lineStart, lineEnd);

        // compute leading margin and subtract it from the x coordinate
        int margin = 0;
        LeadingMarginSpan[] marginSpans = lineText.getSpans(0, lineLength, LeadingMarginSpan.class);
        if (marginSpans != null) {
            for (LeadingMarginSpan span : marginSpans) {
                margin += span.getLeadingMargin(true);
            }
        }
        x -= margin;

        // retrieve text widths
        float[] widths = new float[lineLength];
        TextPaint paint = textView.getPaint();
        paint.getTextWidths(lineText, 0, lineLength, widths);

        // scale text widths by relative font size (absolute size / default size)
        final float defaultSize = textView.getTextSize();
        float scaleFactor = 1f;
        AbsoluteSizeSpan[] absSpans = lineText.getSpans(0, lineLength, AbsoluteSizeSpan.class);
        if (absSpans != null) {
            for (AbsoluteSizeSpan span : absSpans) {
                int spanStart = lineText.getSpanStart(span);
                int spanEnd = lineText.getSpanEnd(span);
                scaleFactor = span.getSize() / defaultSize;
                int start = Math.max(lineStart, spanStart);
                int end = Math.min(lineEnd, spanEnd);
                for (int i = start; i < end; i++) {
                    widths[i] *= scaleFactor;
                }
            }
        }

        // find index of touched character
        float startChar = 0;
        float endChar = 0;
        for (int i = 0; i < lineLength; i++) {
            startChar = endChar;
            endChar += widths[i];
            if (endChar >= x) {
                // which "end" is closer to x, the start or the end of the character?
                int index = lineStart + (x - startChar < endChar - x ? i : i + 1);
                //Logger.e(Logger.LOG_TAG, "Found character: " + (text.length()>index ? text.charAt(index) : ""));
                return index;
            }
        }

        return -1;
    }
}

Complete Activity code

Here's the complete sample Activity code that I used. It should do exactly what you want. It's using my EnhandedMovementMethod but you can use a simple LinkMovementMethod (with the drawbacks mentioned before).

public class LinkTestActivity extends Activity {

    private WebView mWebView;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        mWebView = (WebView) findViewById(R.id.webView);
        TextView tv = (TextView) findViewById(R.id.textView);

        String text = "These are some sample links:\nwww.google.com\nwww.facebook.com\nwww.yahoo.com";

        // Linkify the TextView
        Spannable spannable = new SpannableString( Html.fromHtml(text) );
        Linkify.addLinks(spannable, Linkify.WEB_URLS);

        // Replace each URLSpan by a LinkSpan
        URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
        for (URLSpan urlSpan : spans) {
            LinkSpan linkSpan = new LinkSpan(urlSpan.getURL());
            int spanStart = spannable.getSpanStart(urlSpan);
            int spanEnd = spannable.getSpanEnd(urlSpan);
            spannable.setSpan(linkSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            spannable.removeSpan(urlSpan);
        }

        // Make sure the TextView supports clicking on Links
        tv.setMovementMethod(EnhancedLinkMovementMethod.getInstance());
        tv.setText(spannable, BufferType.SPANNABLE);

        // Make sure we handle clicked links ourselves
        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // we handle the url ourselves if it's a network url (http / https) 
                return ! URLUtil.isNetworkUrl(url);
            }
        });

        mWebView.getSettings().setJavaScriptEnabled(true);   
        mWebView.getSettings().setSupportZoom(true);      
        mWebView.getSettings().setBuiltInZoomControls(true);
    }

    private class LinkSpan extends URLSpan {
        private LinkSpan(String url) {
            super(url);
        }

        @Override
        public void onClick(View view) {
            String url = getURL();
            if (mWebView != null && url != null) {
                mWebView.loadUrl(url);
            }
        }
    }
}