在ScrollView中使用WebView

30

我必须将WebView放入ScrollView中,但我需要在webview之前将一些视图放入相同的scrollview中。所以它看起来像这样:

<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>

我从后端获取了一些HTML信息。它没有包含任何body或head标签,只是一些被<p><h4>或其他标签包围的数据。此外,其中也包含了<img>标签。有时候图片会比当前屏幕宽度还要宽。因此,我在HTML开头添加了一些CSS。我这样将数据加载到webview中:

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");
有时当页面加载时,滚动视图会滚动到网页视图开始的位置。我不知道该如何解决。
此外,有时在网页视图内容之后会出现巨大的空白区域。我也不知道该怎么办。
有时滚动视图的滚动条在我滚动时开始随机抖动...
我知道将网页视图放入滚动视图中是不正确的,但似乎我别无选择。有人能建议正确的方法将所有视图和所有 HTML 内容放置到网页视图中吗?
6个回答

19
使用:
android:descendantFocusability="blocksDescendants"

在包装布局中,这将防止WebView跳转到其开头。
使用:
webView.clearView();
mNewsContent.requestLayout();

每次更改 WebView 的大小以使布局失效,这将删除空白间隙。

1
很抱歉,第二部分无法运行(可能是因为“mNewsContent”名称所指的内容不太清楚)。+1给第一部分回答。 - Giulio Piancastelli
android:descendantFocusability="blocksDescendants" 这对我起作用了! - Mejonzhan
1
如果您禁用了焦点功能,那么您将无法在Web视图中使用输入字段。 - spatialist

16
2014-11-13更新: 自从Android KitKat以后,下面所描述的解决方案都不再适用了--你需要寻找不同的方法,比如Manuel Peinado的FadingActionBar,它为WebView提供了一个滚动标题栏。 2012-07-08更新: "Nobu games"友好地创建了一个TitleBarWebView类,将期望的行为带回到Android Jelly Bean。在旧平台上使用时,它将使用隐藏的setEmbeddedTitleBar()方法,在Jelly Bean或更高版本上使用时,它将模仿相同的行为。源代码在Apache 2许可证下在Google Code上可用。 2012-06-30更新: 看起来setEmbeddedTitleBar()方法已经在Android 4.1即Jelly Bean中被删除 :-(
原始答案:
WebView放入ScrollView是可以实现的,并且确实有效。我在Android 1.6设备上在GoodNews中使用它。主要缺点是用户无法进行"对角线"滚动,即:如果网页内容超出屏幕宽度,则ScrollView负责垂直滚动,WebView负责水平滚动。由于它们中只有一个处理触摸事件,因此您只能水平或垂直滚动,但不能对角线滚动。
此外,正如您所描述的那样,还存在一些令人讨厌的问题(例如,在加载比以前更小的内容时出现空白垂直空间)。我在GoodNews中找到了所有这些问题的解决方法,但现在无法记住它们,因为我找到了一个更好的解决方案:
如果您只是将WebView放入ScrollView以在网页内容之上放置控件,并且您可以支持仅Android 2及以上版本,则可以使用WebView的隐藏内部setEmbeddedTitleBar()方法。它已经在API级别5中引入,并且(意外地?)在恰好一个发布版中变为公共方法(我认为是3.0)。
该方法允许您将布局嵌入到WebView中,该布局将放置在网页内容之上。当垂直滚动时,此布局将滚出屏幕,但在水平滚动网页内容时将保持相同的水平位置。

由于此方法未在API中导出,因此您需要使用Java反射来调用它。我建议按照以下方式派生一个新类:

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));
    }
}

然后在您的布局文件中,您可以使用以下方式包含它:
<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"
        />

在您的代码的其他地方,您可以使用要嵌入到 WebView 中的布局的ID调用 setTitle()方法。


我已经在GoodNews中找到了所有问题的解决方法。你是如何解决对角线滚动问题的? - Mark
@Mark,这个答案中描述的解决方案利用旧版WebView中的隐藏功能实现了一个基于头部的滚动,以便对角线滚动可以无障碍地进行(请参见上文中的详细信息)。不幸的是,自从JellyBean以来,这个解决方案已经不再起作用。请查看我在答案开头的更新。 - sven
你在2014年11月13日提出的建议看起来很有前途。不幸的是,在Android 5.0和某些4.0.X设备上无法工作。http://imgur.com/GEXzRWT我已经确定了问题所在,它位于ObservableWebViewWithHeader类的onDraw()方法中。似乎translate方法在某些Android版本上的行为不同。有人有解决这个问题的主意吗? - Johan
@Johan 我和你一样遇到了同样的问题!真正的问题不在于翻译。如果你使用 visibleHeaderHeight 来进行修正,你会发现滚动出 WebView 的内容将无法返回!有人能解决这个问题吗? - zzy

9
这是一个包含另一个“标题栏”视图的WebView的实现。
它的外观如下:
红色条+3个按钮是“标题栏”,下面是Web视图,所有内容都在一个矩形中滚动和剪切。
这很简洁,短小,可以在API 8到16及以上使用(稍加努力即可在API<8上使用)。它不使用任何隐藏函数,例如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();
   }
}

用法:在布局xml中将标题栏视图层次结构放置在< WebView >元素中。WebView继承ViewGroup,因此它可以包含子项,尽管ADT插件会抱怨它不能。 示例:

<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>

请注意使用layerType="software",在API 11+上硬件上的WebView在具有hw层时无法正确地进行动画和绘制。

滚动工作完美,标题栏上的点击,网页上的点击,选择网页中的文本等都可以正常使用。


指针空值。这个功能非常好,但它只适用于垂直视图,当方向改变为水平时,滚动非常困难。 - hemant
大多数情况下很好用,但对于文本输入和其他输入元素效果不佳。 - Mdlc

5
感谢您的提问,@Dmitriy!同样感谢@sven的答案。
但是我希望在需要在ScrollView中嵌套WebView的情况下有一个解决方法。正如sven所正确指出的,主要问题是滚动视图截取了所有可用于垂直滚动的触摸事件。因此,解决方法很明显-扩展滚动视图并以这样一种方式覆盖ViewGroup.onInterceptTouchEvent(MotionEvent e)方法,使得滚动视图变得容忍它的子视图:
处理所有可用于垂直滚动的触摸事件。
将所有这些事件分派给子视图。
只截取具有垂直滚动的事件 (dx = 0, dy > 0)。
当然,这个解决方案只能与适当的布局配对使用。我制作了一个示例布局,可以说明主要思路。
    <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>

所以,主要思想是通过将所有视图(标题、Web视图、页脚)放入垂直线性布局中,避免垂直滚动冲突,并正确分配触摸事件以允许水平和对角线滚动。 我不确定它是否有用,但我已经创建了一个带有TolerantScrollView和布局示例的样本项目,您可以在此处下载。
另外,我已经查看了通过反射访问关闭API的解决方案,我认为它相当棘手。在某些带有Android ICS的HTC设备上,由于Web视图上的灰色矩形伪影,它也无法正常工作。也许有人知道问题所在,我们该如何解决?

你使用透明背景吗? - Antzi
你的解决方案非常好,但它不能正确地允许文本选择。当用户拖动选择图标以垂直移动时,整个ScrollView会移动。你会如何修改你的代码来解决这个问题? - Marcin
目前我们正在开发另一种解决方案,更适合开发指南,并且具有较少的错误和不一致性。一旦准备好,我会为您提供代码片段。 - rus1f1kat0R

2

这些答案对我都不适用,所以这是我的解决方案。首先,我们需要重写WebView以便能够处理其滚动。您可以在此处找到如何实现:https://dev59.com/TWUq5IYBdhLWcg3wJNHg#14753235

然后创建具有两个视图的xml,其中一个是WebView,另一个是标题布局。

这是我的xml代码:

<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>

接下来需要处理WebView的滚动并根据滚动值适当移动标题。在此处,您必须使用NineOldAndroids

@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());
    }
}

您需要为WebView内容添加填充,以便它不会覆盖标题:

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);
        }
});

这里的dp的值是多少? - Yogesh Seralia
@YogeshSeralia 只是屏幕密度。您可以从 context.getResources().getDisplayMetrics().density 获取它。 - rstk
@YogeshSeralia 但是有更好的方法可以在px和dp之间进行转换,使用TypedValue.applyDimension() - rstk

0
我知道将webview放入scrollview中是不正确的,但似乎我别无选择。
实际上你有其他选择,因为你想要的方法不能保证可靠性。
但在webview之前,我必须将一些视图放入同一个scrollview中。
删除ScrollView。将WebView的android:layout_height更改为fill_parent。WebView将填充LinearLayout中其他小部件未使用的所有剩余空间。

@CommonsWare 好的,谢谢 - 我想知道如果我在 WebView 上禁用滚动并将其设置为 wrap_content,是否会起作用。我不需要 WebView 滚动其内容。在 Android 中有显示正确样式的 CSS/HTML 的替代方法吗? - Jarrod Smith
顺便说一句,在本机Android电子邮件客户端中它可靠地工作,所以必须以某种方式完成。也许在ScrollView中覆盖触摸事件分发并在此处进行一些魔法可能有所帮助。看起来主要问题是当ScrollView自身不消耗垂直移动时,它只将触摸事件分派给子元素。 - Pointer Null
@CommonsWare 有一些使用 WebView 来样式化内容的用例。Html.fromHtml()不处理上标和下标。它不处理自定义字体。您可以让 TextView 使用自定义字体,但是该字体将不会被调整字距,而使用 WebView 字体则会正常渲染。它也不允许您控制处理链接。在 Android 获得更好的文本呈现引擎之前,WebView 可能是您唯一的选择。 - dcow
@DavidCowden:是的,但我仍然不建议在ScrollView中放置WebView - CommonsWare
非常有用的技巧!我可能会基于此创建一个轻量级的 Web 视图头部库(将在适当的地方致谢)。非常感谢! - phazedlite
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接