30

I have to place WebView into ScrollView. But I have to put some views into the same scrollview before webview. So it looks like this:

<ScrollView
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  >
<LinearLayout
    android:id="@+id/articleDetailPageContentLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
  <LinearLayout
      android:id="@+id/articleDetailRubricLine"
      android:layout_width="fill_parent"
      android:layout_height="3dip"
      android:background="@color/fashion"/>

  <ImageView
      android:id="@+id/articleDetailImageView"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:adjustViewBounds="true"
      android:scaleType="fitStart"
      android:src="@drawable/article_detail_image_test"/>

  <TextView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="5dip"
      android:text="PUBLISH DATE"/>

  <WebView
      android:id="@+id/articleDetailContentView"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:background="@color/fashion"
      android:isScrollContainer="false"/>
</LinearLayout>

I'm getting some HTML info from backend. It has no any body or head tags, just data surrounded by <p> or <h4> or some other tags. Also it has <img> tags in there. Sometimes pictures are too wide for current screen width. So I added some css in the begining of HTML. So I loads data to webview like this:

private final static String WEBVIEW_MIME_TYPE = "text/html";
    private final static String WEBVIEW_ENCODING = "utf-8";
String viewport = "<head><meta name=\"viewport\" content=\"target-densitydpi=device-dpi\" /></head>";
        String css = "<style type=\"text/css\">" +
            "img {width: 100%;}" +
            "</style>";
        articleContent.loadDataWithBaseURL("http://", viewport + css + articleDetail.getContent(), WEBVIEW_MIME_TYPE,
            WEBVIEW_ENCODING, "about:blank");

Sometimes when page loaded, scrollview scrolls to place where webview begins. And I don't know how to fix that.

Also, sometimes there is huge white empty space appears after webview content. I also don't know what to do with that.

Sometimes scrollview's scrollbars starts twitch randomly while I scrolling...

I know that it's not right to place webview into scrollview, but it seems like I have no other choise. Could anyone suggest rigth way to place all views and all HTML content to webview?

Dmitriy
  • 830
  • 3
  • 15
  • 29

6 Answers6

19

use:

android:descendantFocusability="blocksDescendants"

on the wrapping layout this will prevent the WebView from jumping to its start.

use:

webView.clearView();
mNewsContent.requestLayout();

every time you change the WebView size to invalidate the layout, this will remove the empty spacing.

Faraz
  • 2,144
  • 1
  • 18
  • 28
Evgeni Roitburg
  • 1,958
  • 21
  • 33
16

Update 2014-11-13: Since Android KitKat neither of the solutions described below are working -- you will need to look for different approaches like e.g. Manuel Peinado's FadingActionBar which provides a scrolling header for WebViews.

Update 2012-07-08: "Nobu games" kindly created a TitleBarWebView class bringing the expected behavior back to Android Jelly Bean. When used on older platforms it will use the hidden setEmbeddedTitleBar() method and when used on Jelly Bean or above it will mimic the same behavior. The source code is available under the Apache 2 license at google code

Update 2012-06-30: It seems as if the setEmbeddedTitleBar() method has been removed in Android 4.1 aka Jelly Bean :-(

Original answer:

It is possible to place a WebView into a ScrollView and it does work. I am using this in GoodNews on Android 1.6 devices. The main drawback is that the user cannot scroll "diagonal" meaning: If the web content exceeds the width of the screen the ScrollView is responsible for vertical scrolling at the WebView for horizontal scrolling. As only one of them handles the touch events you can either scroll horizontally or vertically but not diagonal.

Further on there are some annoying problems as described by you (e.g. empty vertical space when loading a content smaller than the previous one). I've found workarounds for all of them in GoodNews, but cannot remember them now, because I've found a much better solution:

If you only put the WebView into the ScrollView to place Controls above the web content and you are OK to support only Android 2 and above, then you can use the hidden internal setEmbeddedTitleBar() method of the WebView. It has been introduced in API level 5 and (accidentally?) became public for exactly one release (I think it was 3.0).

This method allows you to embed a layout into the WebView which will be placed above the web content. This layout will scroll out the screen when scrolling vertically but will be kept at the same horizontal position when the web content is scrolled horizontally.

As this method isn't exported by the API you need to use Java reflections to call it. I suggest to derive a new class as followed:

public final class WebViewWithTitle extends ExtendedWebView {
    private static final String LOG_TAG = "WebViewWithTitle";
    private Method setEmbeddedTitleBarMethod = null;

    public WebViewWithTitle(Context context) {
        super(context);
        init();
    }

    public WebViewWithTitle(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        try {
            setEmbeddedTitleBarMethod = WebView.class.getMethod("setEmbeddedTitleBar", View.class);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "could not find setEmbeddedTitleBar", ex);
        }
    }

    public void setTitle(View view) {
        if (setEmbeddedTitleBarMethod != null) {
            try {
                setEmbeddedTitleBarMethod.invoke(this, view);
            } catch (Exception ex) {
                Log.e(LOG_TAG, "failed to call setEmbeddedTitleBar", ex);
            }
        }
    }

    public void setTitle(int resId) {
        setTitle(inflate(getContext(), resId, null));
    }
}

Then in your layout file you can include this using

<com.mycompany.widget.WebViewWithTitle
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        />

and somewhere else in your code you can call the setTitle() method with the ID of the layout to be embedded into the WebView.

sven
  • 4,161
  • 32
  • 33
  • "I've found workarounds for all of them in GoodNews." How did you solve the diagonal scrolling issue? – Mark Nov 12 '14 at 02:01
  • @Mark, the solution described in this answer implements a header based on hidden features in the old `WebView` so that diagonal scrolling worked without problems (see details above). Unfortunately this solution isn't working anymore since JellyBean. See my updates at the beginning of the answer. – sven Nov 13 '14 at 14:32
  • Your proposal from 2014-11-13 look promising. Unfortunately it does not work under Android 5.0 and on some 4.0.X devices. http://imgur.com/GEXzRWT I've located the problem to the ObservableWebViewWithHeader class in the method onDraw(). Seems like the translate method behaves differently on certain Android version. Does anybody have an idea how to solve this? – Johan Nov 20 '14 at 08:43
  • @Johan I have the same problem with you ! The real problem is not the translate. If you correct the translate use visibleHeaderHeight. You can see the content scrolled out of webview won't come back! Anyone can solve this? – zzy Nov 29 '14 at 07:46
9

Here is implementation of WebView containing another "Title bar" view at top of it.

How it looks:

Red bar + 3 buttons is a "Title bar", below is web view, all is scrolled and clipped together in one rectangle.

It's clean, short, works all way from API 8 to 16 and up (with small effort it can work also on API<8). It doesn't use any hidden functions such as WebView.setEmbeddedTitleBar.

public class TitleWebView extends WebView{

   public TitleWebView(Context context, AttributeSet attrs){
      super(context, attrs);
   }

   private int titleHeight;

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      // determine height of title bar
      View title = getChildAt(0);
      titleHeight = title==null ? 0 : title.getMeasuredHeight();
   }

   @Override
   public boolean onInterceptTouchEvent(MotionEvent ev){
      return true;   // don't pass our touch events to children (title bar), we send these in dispatchTouchEvent
   }

   private boolean touchInTitleBar;
   @Override
   public boolean dispatchTouchEvent(MotionEvent me){

      boolean wasInTitle = false;
      switch(me.getActionMasked()){
      case MotionEvent.ACTION_DOWN:
         touchInTitleBar = (me.getY() <= visibleTitleHeight());
         break;

      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
         wasInTitle = touchInTitleBar;
         touchInTitleBar = false;
         break;
      }
      if(touchInTitleBar || wasInTitle) {
         View title = getChildAt(0);
         if(title!=null) {
            // this touch belongs to title bar, dispatch it here
            me.offsetLocation(0, getScrollY());
            return title.dispatchTouchEvent(me);
         }
      }
      // this is our touch, offset and process
      me.offsetLocation(0, -titleHeight);
      return super.dispatchTouchEvent(me);
   }

   /**
    * @return visible height of title (may return negative values)
    */
   private int visibleTitleHeight(){
      return titleHeight-getScrollY();
   }       

   @Override
   protected void onScrollChanged(int l, int t, int oldl, int oldt){
      super.onScrollChanged(l, t, oldl, oldt);
      View title = getChildAt(0);
      if(title!=null)   // undo horizontal scroll, so that title scrolls only vertically
         title.offsetLeftAndRight(l - title.getLeft());
   }

   @Override
   protected void onDraw(Canvas c){

      c.save();
      int tH = visibleTitleHeight();
      if(tH>0) {
         // clip so that it doesn't clear background under title bar
         int sx = getScrollX(), sy = getScrollY();
         c.clipRect(sx, sy+tH, sx+getWidth(), sy+getHeight());
      }
      c.translate(0, titleHeight);
      super.onDraw(c);
      c.restore();
   }
}

Usage: put your title bar view hierarchy inside of <WebView> element in layout xml. WebView inherits ViewGroup, so it can contain children, despite of ADT plugin complaining that it can't. Example:

<com.test.TitleWebView
   android:id="@+id/webView"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layerType="software" >

   <LinearLayout
      android:id="@+id/title_bar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:background="#400" >

      <Button
         android:id="@+id/button2"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Button" />

      <Button
         android:id="@+id/button3"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Button" />

      <Button
         android:id="@+id/button5"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Button" />
   </LinearLayout>
</com.test.TitleWebView>

Note usage of layerType="software", WebView in hardware on API 11+ isn't properly animated and drawn when it has hw layer.

Scrolling works perfectly, as well as clicks on title bar, clicks on web, selecting text in web, etc.

Pointer Null
  • 39,597
  • 13
  • 90
  • 111
  • -Pointer Null This works really well but it works only with vertical view and when orientation changes to horizontal it is very difficult to scroll. – hemant Jan 22 '14 at 06:21
  • Excellent for most cases, however, this wont work well with text inputs and other input elements. – Mdlc Oct 04 '15 at 14:19
5

Thanks for your question, @Dmitriy! And also thanks a lot to @sven for the answer.

But I hope there is a workaround for the cases where we need to put WebView inside the ScrollView. As sven correctly noticed the main issue is that scroll view intercepts all the touch events that can be used for the vertical scrolling. So workaround is obvious - extend scroll view and override ViewGroup.onInterceptTouchEvent(MotionEvent e) method in such a way the scroll view become tolerant to it's child views:

  1. process all touch events that can be used for vertical scrolling.
  2. dispatch all of them to the child views.
  3. intercept events with vertical scrolling only (dx = 0, dy > 0)

Ofcourse this solution can only be used in couple with appropriate layout. I have made sample layout that can illustrate the main idea.

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.rus1f1kat0r.view.TolerantScrollView
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content" 
            android:orientation="vertical">

            <TextView
                android:id="@+id/textView1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Large Text"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <TextView
                android:id="@+id/textView2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Large Text"
                android:textAppearance="?android:attr/textAppearanceLarge" />

            <WebView
                android:id="@+id/webView1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <TextView
                android:id="@+id/textView3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Medium Text"
                android:textAppearance="?android:attr/textAppearanceMedium" />

           <TextView
               android:id="@+id/textView4"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="Medium Text"
               android:textAppearance="?android:attr/textAppearanceMedium" />

        </LinearLayout>
    </com.rus1f1kat0r.view.TolerantScrollView>
</RelativeLayout>

So that the main idea itself is to avoid vertical scrolling conflicts by putting all the views (header, webview, footer) inside vertical linear layout, and correctly dispatch touch events in order to allow horizontal and diagonal scrolling. I'm not sure whether it could be useful, but I have created the sample project with TolerantScrollView and layout sample, you can download it here.

What's more - I have viewed the solution with accessing to the closed API via reflections and I guess it is rather hucky. It is also don't work for me because of gray rectangle artifacts over the webview on some HTC devices with Android ICS. Maybe someone knows what is the problem and how we can solve it?

rus1f1kat0R
  • 1,645
  • 1
  • 16
  • 22
  • Do you use a transparent background ? – Antzi Jun 13 '13 at 15:21
  • Your solution is very nice, but it doesn't allow for text selection to be made properly. When user drags the selection icon to move it vertically, the entire ScrollView moves instead. How would you modify your code to fix that? – Marcin Dec 05 '13 at 19:43
  • By the moment we are working on the another solution that is better suits to dev guides and has less bugs and inconsistences. I'll provide you with the code snippets once ready. – rus1f1kat0R Dec 10 '13 at 07:47
2

None of this answers work for me, so here is my solution. First of all we need to override WebView to have ability to handle its scrolling. You can find how to do it here: https://stackoverflow.com/a/14753235/1285670

Then create your xml with two view, where one is WebView and another is your title layout.

This is my xml code:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <com.project.ObservableWebView
      android:id="@+id/post_web_view"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="#fff" />

  <LinearLayout
      android:id="@+id/post_header"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="#fff"
      android:orientation="vertical"
      android:paddingLeft="@dimen/common_ui_space"
      android:paddingRight="@dimen/common_ui_space"
      android:paddingTop="@dimen/common_ui_space" >

      ////some stuff

    </LinearLayout>

</FrameLayout>

Next is handle WebView scroll and move title appropriate to scroll value. You must use NineOldAndroids here.

@Override
public void onScroll(int l, int t, int oldl, int oldt) {
    if (t < mTitleLayout.getHeight()) {
        ViewHelper.setTranslationY(mTitleLayout, -t);
    } else if (oldt < mTitleLayout.getHeight()) {
        ViewHelper.setTranslationY(mTitleLayout, -mTitleLayout.getHeight());
    }
}

And you must add padding to your WebView content, so it will not overlay title:

v.getViewTreeObserver().addOnGlobalLayoutListener(
    new OnGlobalLayoutListener() {
        @SuppressWarnings("deprecation")
        @SuppressLint("NewApi")
        @Override
        public void onGlobalLayout() {
            if (mTitleLayout.getHeight() != 0) {
                if (Build.VERSION.SDK_INT >= 16)
                    v.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                else
                    v.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }

                mWebView.loadDataWithBaseURL(null, "<div style=\"padding-top: " 
                  + mTitleLayout.getHeight() / dp + "px\">" + mPost.getContent() 
                  + "</div>", "text/html", "UTF8", null);
        }
});
Community
  • 1
  • 1
rstk
  • 414
  • 5
  • 14
0

I know that it's not right to place webview into scrollview, but it seems like I have no other choise.

Yes you do, because what you want will not work reliably.

But I have to put some views into the same scrollview before webview.

Delete the ScrollView. Change the android:layout_height of your WebView to fill_parent. The WebView will fill up all remaining space in the LinearLayout not consumed by the other widgets.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • if I do like you said, only webview area will be scrollable. But I need that whole area with ImageView and TextView be able to scroll. – Dmitriy Mar 15 '12 at 13:01
  • @Dmitriy: Then put the text and the image in the HTML and get rid of the widgets, so all you have is a `WebView`, and the whole thing can scroll in unison. – CommonsWare Mar 15 '12 at 13:10
  • @CommonsWare "because what you want will not work reliably." Can you please elaborate? I have the exact same situation with a WebView within a ScrollView being loaded with different content and no re-sizing properly, or the ScrollView's scrollbar glitching/twitching constantly. Putting everything inside the WebView is not an option for me. – sleep Apr 19 '12 at 13:52
  • @JarrodSmith: "Can you please elaborate?" -- there is nothing to elaborate on. It does not work reliably, as you are discovering. Do something else. "Putting everything inside the WebView is not an option for me" -- then redesign the app so either is is an option for you, or eliminate the `ScrollView`, or eliminate the `WebView`. – CommonsWare Apr 19 '12 at 13:59
  • @CommonsWare Ok thanks - I guess I was just wondering if it would work if I disabled scrolling on the WebView, and set it to wrap_content. I don't need the _WebView_ itself to scroll its content. Is there an alternative way of displaying properly styled CSS/HTML in Android? – sleep Apr 19 '12 at 23:17
  • @JarrodSmith: `WebView` won't honor `wrap_content` AFAIK. "Is there an alternative way of displaying properly styled CSS/HTML in Android?" -- HTML, yes, via `Html.fromHtml()` and a `TextView`. CSS, no. – CommonsWare Apr 19 '12 at 23:36
  • Btw, it works reliably in native Android email client, so it has to be done somehow. Maybe overriding touch even dispatch in ScrollView and do some magic here could help. It looks like main problem is that ScrollView dispatches touches to children only when it doesn't consume the vertical movement itself. – Pointer Null Jul 11 '12 at 18:57
  • @CommonsWare there are use cases for webview being used to style content. `Html.fromHtml()` does not handle super and subscript. It does not handle custom fonts. You can have a textview use a custom font but the font will not be kerned whereas with webview fonts are rendered properly. It does not give you control of handling links either. Until Android gets a better text rendering engine, webview may be your only choice.. – dcow Nov 17 '13 at 23:12
  • @DavidCowden: Yes, but I still would not recommend putting a `WebView` in a `ScrollView`. – CommonsWare Nov 17 '13 at 23:14
  • Very useful Trick! I might actually base a light weight web view header library based off of this (will give credit where credit is due). Thanks so Much! – phazedlite Jun 14 '15 at 07:01