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:
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.
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);
}
});
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:
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?
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
.
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.
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.