Веб-просмотр в режиме прокрутки

Мне нужно поместить 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>

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

Иногда при загрузке страницы scrollview прокручивается до места, где начинается веб-просмотр. И я не знаю, как это исправить.

Кроме того, иногда после содержимого веб-просмотра появляется огромное белое пустое пространство. Я тоже не знаю, что с этим делать.

Иногда полосы прокрутки scrollview начинают случайным образом дергаться, пока я прокручиваю ...

Я знаю, что размещать веб-просмотр в режиме прокрутки неправильно, но, похоже, у меня нет другого выбора. Может ли кто-нибудь предложить правильный способ разместить все представления и весь HTML-контент в веб-просмотре?


person Dmitriy    schedule 15.03.2012    source источник


Ответы (6)


Обновление от 13 ноября 2014 г. Поскольку Android KitKat ни одно из описанных ниже решений не работает, вам придется искать другие подходы, например FadingActionBar Мануэля Пейнадо, который обеспечивает прокручиваемый заголовок для WebViews.

Обновление 2012-07-08: компания «Nobu games» любезно создала TitleBarWebView, возвращающий ожидаемое поведение в Android Jelly Bean. При использовании на более старых платформах он будет использовать скрытый метод setEmbeddedTitleBar(), а при использовании на Jelly Bean или выше он будет имитировать такое же поведение. Исходный код доступен по лицензии Apache 2 в коде Google

Обновление 30.06.2012: Похоже, что метод setEmbeddedTitleBar() был удален в Android 4.1, также известном как Jelly Bean :-(

Исходный ответ:

можно поместить WebView в ScrollView, и это действительно работает. Я использую это в GoodNews на устройствах Android 1.6. Основным недостатком является то, что пользователь не может прокручивать "по диагонали", что означает: если веб-контент превышает ширину экрана, ScrollView отвечает за вертикальную прокрутку, а WebView - за горизонтальную. Поскольку только один из них обрабатывает события касания, вы можете выполнять прокрутку по горизонтали или по вертикали, но не по диагонали.

Кроме того, возникают некоторые неприятные проблемы, описанные вами (например, пустое вертикальное пространство при загрузке содержимого, меньшего, чем предыдущее). Я нашел обходные пути для всех из них в GoodNews, но сейчас не могу их вспомнить, потому что нашел гораздо лучшее решение:

Если вы поместите WebView в ScrollView, чтобы разместить элементы управления над веб-контентом, и вы готовы поддерживать только Android 2 и выше, то вы можете использовать скрытый внутренний setEmbeddedTitleBar() метод WebView. Он был представлен в 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"
        />

и где-нибудь еще в вашем коде вы можете вызвать метод setTitle() с идентификатором макета, который будет встроен в WebView.

person sven    schedule 30.05.2012
comment
Я нашел обходные пути для всех из них в GoodNews. Как вы решили проблему диагональной прокрутки? - person Mark; 12.11.2014
comment
@Mark, решение, описанное в этом ответе, реализует заголовок на основе скрытых функций в старом WebView, так что диагональная прокрутка работала без проблем (см. Подробности выше). К сожалению, это решение больше не работает со времен JellyBean. Смотрите мои обновления в начале ответа. - person sven; 13.11.2014
comment
Ваше предложение от 13 ноября 2014 г. выглядит многообещающим. К сожалению, он не работает под Android 5.0 и на некоторых устройствах 4.0.X. imgur.com/GEXzRWT Я обнаружил проблему в классе ObservableWebViewWithHeader в методе onDraw (). Похоже, что метод translate ведет себя по-разному в определенной версии Android. Кто-нибудь знает, как это решить? - person Johan; 20.11.2014
comment
@Johan, у меня с тобой такая же проблема! Настоящая проблема не в переводе. Если вы исправляете перевод, используйте visibleHeaderHeight. Вы можете видеть, что контент, прокрученный из веб-просмотра, больше не вернется! Кто-нибудь может это решить? - person zzy; 29.11.2014

использовать:

android:descendantFocusability="blocksDescendants"

в компоновке упаковки это предотвратит скачок WebView к своему началу.

использовать:

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

каждый раз, когда вы изменяете размер WebView, чтобы сделать макет недействительным, пустой интервал будет удален.

person Evgeni Roitburg    schedule 21.02.2013
comment
+1 за первую часть ответа. К сожалению, вторая часть не работает (возможно, из-за того, что не совсем понятно, что вы имеете в виду под именем mNewsContent). - person Giulio Piancastelli; 22.01.2014
comment
android: DescendantsFocusability = blocksDescendants эта работа для меня! - person Mejonzhan; 10.07.2015
comment
Если вы отключите фокусируемость, вы не сможете использовать поля ввода в веб-просмотре. - person spatialist; 09.10.2015

Вот реализация WebView, содержащая еще одно представление «Строка заголовка» вверху.

Как это выглядит:

Красная полоса + 3 кнопки - это «Строка заголовка», ниже - веб-вид, все прокручиваются и обрезаются в один прямоугольник.

Он чистый, короткий, работает от 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();
   }
}

Использование: поместите иерархию представления строки заголовка внутри элемента ‹WebView› в макете xml. 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", WebView в аппаратном обеспечении на API 11+ не анимирован и отрисован должным образом, если у него есть hw-слой.

Прокрутка работает отлично, равно как и щелчки по строке заголовка, щелчки в Интернете, выделение текста в Интернете и т. Д.

person Pointer Null    schedule 11.07.2012
comment
-Pointer Null Это работает очень хорошо, но работает только с вертикальным обзором, а когда ориентация меняется на горизонтальную, прокручивать очень сложно. - person hemant; 22.01.2014
comment
Однако отлично подходит для большинства случаев, это не будет хорошо работать с вводом текста и другими элементами ввода. - person Mdlc; 04.10.2015

Спасибо за вопрос, @Dmitriy! А также большое спасибо @sven за ответ.

Но я надеюсь, что есть обходной путь для случаев, когда нам нужно поместить WebView в ScrollView. Как правильно заметил sven, основная проблема заключается в том, что режим прокрутки перехватывает все события касания, которые можно использовать для вертикальной прокрутки. Таким образом, обходной путь очевиден - расширьте представление прокрутки и переопределите метод ViewGroup.onInterceptTouchEvent(MotionEvent e) таким образом, чтобы представление прокрутки стало терпимым к его дочерним представлениям:

  1. обрабатывать все события касания, которые можно использовать для вертикальной прокрутки.
  2. отправьте их все дочерним представлениям.
  3. перехватывать события только с вертикальной прокруткой (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>

Таким образом, основная идея заключается в том, чтобы избежать конфликтов вертикальной прокрутки, поместив все представления (заголовок, веб-просмотр, нижний колонтитул) внутри вертикального линейного макета и правильно отправляя события касания, чтобы разрешить горизонтальную и диагональную прокрутку. Я не уверен, что это может быть полезно, но я создал образец проекта с TolerantScrollView и образец макета, вы можете скачать его здесь.

Более того - я просмотрел решение с доступом к закрытому API через размышления, и я думаю, что это довольно удачно. Это также не работает для меня из-за артефактов серого прямоугольника в веб-просмотре на некоторых устройствах HTC с Android ICS. Может кто знает, в чем проблема и как ее решить?

person rus1f1kat0R    schedule 18.10.2012
comment
Вы используете прозрачный фон? - person Antzi; 13.06.2013
comment
Ваше решение очень хорошее, но оно не позволяет правильно выделить текст. Когда пользователь перетаскивает значок выбора, чтобы переместить его по вертикали, вместо этого перемещается весь ScrollView. Как бы вы изменили свой код, чтобы это исправить? - person Marcin; 05.12.2013
comment
На данный момент мы работаем над другим решением, которое лучше подходит для руководств разработчиков и имеет меньше ошибок и несоответствий. Я предоставлю вам фрагменты кода, когда они будут готовы. - person rus1f1kat0R; 10.12.2013

Ни один из этих ответов не работает для меня, поэтому вот мое решение. Прежде всего нам нужно переопределить WebView, чтобы иметь возможность обрабатывать его прокрутку. Вы можете узнать, как это сделать, здесь: https://stackoverflow.com/a/14753235/1285670

Затем создайте свой 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 scroll и переместить заголовок, соответствующий значению прокрутки. Здесь вы должны использовать 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);
        }
});
person rstk    schedule 24.09.2014
comment
какое здесь значение dp - person Yogesh Seralia; 27.09.2016
comment
@YogeshSeralia Просто плотность экрана. Вы можете получить его от context.getResources().getDisplayMetrics().density - person rstk; 28.09.2016
comment
@YogeshSeralia, но нет лучшего способа конвертировать между px и dp, используйте TypedValue.applyDimension() - person rstk; 28.09.2016

Я знаю, что размещать веб-просмотр в режиме прокрутки неправильно, но, похоже, у меня нет другого выбора.

Да, вы делаете, потому что то, что вы хотите, не будет работать надежно.

Но я должен поместить несколько представлений в тот же просмотр прокрутки перед просмотром веб-страниц.

Удалите ScrollView. Измените android:layout_height вашего WebView на fill_parent. WebView заполнит все оставшееся пространство в LinearLayout, не занятое другими виджетами.

person CommonsWare    schedule 15.03.2012
comment
если я сделаю так, как вы сказали, будет прокручиваться только область веб-просмотра. Но мне нужно, чтобы вся эта область с ImageView и TextView могла прокручиваться. - person Dmitriy; 15.03.2012
comment
@Dmitriy: Затем поместите текст и изображение в HTML и избавьтесь от виджетов, так что все, что у вас есть, это WebView, и все это может прокручиваться в унисон. - person CommonsWare; 15.03.2012
comment
@CommonsWare, потому что то, что вы хотите, не будет работать надежно. Не могли бы вы уточнить? У меня точно такая же ситуация с WebView в ScrollView, загружаемом с другим контентом и без правильного изменения размера, или с постоянным сбоем / подергиванием полосы прокрутки ScrollView. Размещение всего в WebView для меня не вариант. - person Jarrod Smith; 19.04.2012
comment
@JarrodSmith: Не могли бы вы уточнить? - нечего уточнять. Как вы сами понимаете, это не работает надежно. Сделай что-нибудь еще. Размещение всего в WebView не вариант для меня - затем перепроектируйте приложение так, чтобы либо это было вариантом для вас, либо удалите ScrollView, либо удалите WebView. - person CommonsWare; 19.04.2012
comment
@CommonsWare Хорошо, спасибо - я думаю, мне просто интересно, будет ли это работать, если я отключу прокрутку в WebView и установлю для него wrap_content. Мне не нужен сам WebView для прокрутки его содержимого. Есть ли альтернативный способ отображения правильно оформленных CSS / HTML в Android? - person Jarrod Smith; 20.04.2012
comment
@JarrodSmith: WebView не будет уважать wrap_content AFAIK. Есть ли альтернативный способ отображения правильно оформленных CSS / HTML в Android? - HTML, да, через Html.fromHtml() и TextView. CSS, нет. - person CommonsWare; 20.04.2012
comment
Кстати, он надежно работает в родном почтовом клиенте Android, так что это нужно как-то делать. Возможно, переопределение касания, даже отправка в ScrollView и волшебство здесь может помочь. Похоже, основная проблема заключается в том, что ScrollView отправляет касания дочерним элементам только тогда, когда не использует само вертикальное перемещение. - person Pointer Null; 11.07.2012
comment
@CommonsWare есть варианты использования webview для стилизации контента. Html.fromHtml() не обрабатывает верхний и нижний индекс. Он не обрабатывает пользовательские шрифты. Вы можете использовать в текстовом представлении собственный шрифт, но этот шрифт не будет кернингован, тогда как в веб-просмотре шрифты отображаются правильно. Это также не дает вам контроля над обработкой ссылок. Пока Android не получит улучшенный движок отрисовки текста, веб-просмотр может быть вашим единственным выбором. - person dcow; 18.11.2013
comment
@DavidCowden: Да, но я все равно не рекомендую ставить WebView в ScrollView. - person CommonsWare; 18.11.2013
comment
Очень полезный трюк! На самом деле я мог бы основать легкую библиотеку заголовков веб-представлений на основе этого (буду отдавать должное, если это необходимо). Спасибо! - person phazedlite; 14.06.2015