Sean Beach
Sean Beach

Reputation: 2090

WebView does not load in proper position

I have an app that loads HTML content in three WebViews. For simplicity, let's call them top, middle, and bottom.

The user is always viewing the middle WebView. When the user reaches the top of the page and swipes down, the layout of the three WebViews change so that the top view is visible. Conversely, when the user reaches the bottom of the page and swipes up, the bottom page comes into view.

Imagine a 100x100 screen. Coordinate 0,0 is the top left of the screen and 100,100 is the bottom right of the screen. The top view will have a layout with the top at -105 so that it is not viewable, the middle view will occupy the screen, and the bottom view will have a layout with the top at 105 so that it is not viewable, as well.

topLayout.topMargin = -105;
middleLayout.topMargin = 0;
bottomLayout.topMargin = 105;

top.setLayoutParams(topLayout);
middle.setLayoutParams(middleLayout);
bottom.setLayoutParams(bottomLayout);

The content are books, so when the user changes pages, the content should flow logically. When scrolling backwards (up), the bottom of the previous page should be shown. When scrolling forwards (down), the top of the next page should be shown. This is accomplished through setting the WebView's scroll positions using scrollTo(x,y). The code looks like this, where a represents the bottom of the content and b represents the top of the content:

top.scrollTo(0, a);
middle.scrollTo(0, {a,b}); // a for previous page; b for next page
bottom.scrollTo(0, b);

When the user swipes to the previous page, the top WebView's layout changes to have a top of 0 to occupy the screen; the middle changes to 105, and the bottom changes to -105 and loads different content so the app will be prepared for a future previous swipe.


Now we actually come to my question. This works exactly as intended except in Android 4.4 (KitKat). In KitKat, it works for two swipes in either direction, but then on the third and subsequent swipe in the same direction, the content is loaded in the wrong position. When scrolling backwards, the content starts to load showing the top. When scrolling forwards, the content starts to load showing the bottom.

I have stepped through the debugger and noticed that the layouts are set properly, followed by the scroll positions being set correctly. Then, after those values are set, but before the content is actually drawn, something happens in the stack that changes the scroll position.

This is where I'm totally lost. Why are the scroll values getting set correctly, then magically changing before the screen is drawn?

I already tried using an onLayoutCompleteListener, it didn't work. I will update the list of things attempted as I receive answers and try the suggestions. Thank you in advance for any and all help.


Here's a summary of what I'm doing to change pages:

public class MyWebView extends WebView {

    // Assume this is instantiated; it is by the time it is needed
    private List<MyWebView> viewArray = new List<MyWebView>(3); 

    private class CustomGestureListener extends 
        GestureDetector.SimpleOnGestureListener {

        private static final String DEBUG_TAG = "Gestures";
        private int scrollYOnTouch;
        private int scrollYOnRelease;

        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, 
                float velocityX, float velocityY) {
            Log.d(DEBUG_TAG, "onFling: " + event1.toString() 
                + event2.toString());
            Log.i(DEBUG_TAG, "onFling: vX[" + velocityX 
                + "], vY[" + velocityY + "]");

            scrollYOnRelease = getScrollY();
            int bottomOfPage = scrollYOnTouch + getMeasuredHeight();
            int endOfContent = (int) Math.floor(getContentHeight() * getScale());
            int proximity = endOfContent - bottomOfPage;
            boolean atBottom = proximity <= 1;
            Log.i(DEBUG_TAG, "atBottom = (" + proximity + " <= 1)");

            if ((velocityY > VELOCITY_THRESHOLD) 
                && (scrollYOnRelease <= 0) && (scrollYOnTouch == 0)) {
                // User flung down while at the top of the page.
                // Go to the previous page.
                changePages(PREVIOUS_PAGE);
            } else if ((velocityY < -VELOCITY_THRESHOLD) 
                && (scrollYOnRelease >= scrollYOnTouch) && atBottom) {
                // User flung up while at the bottom of the page.
                // Go to the next page.
                changePages(NEXT_PAGE);
            }
            return true;
        }
    } // end of CustomGestureListener

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Send the event to our gesture detector
        // If it is implemented, there will be a return value
        this.mDetector.onTouchEvent(event);
        // If the detected gesture is unimplemented, send it to the superclass
        return super.onTouchEvent(event);
    }

    @Override
    public void changePages(int direction) {

        int screenWidth = getScreenWidth();
        int screenHeight = getScreenHeight();

        VerticalPagingWebView previous;
        VerticalPagingWebView current;
        VerticalPagingWebView next;

        if (direction == NEXT_PAGE) {
            // Rearrange elements in webview array
            // Next page becomes current page,
            // current becomes previous,
            // previous becomes next.
            Collections.swap(viewArray, 0, 1);
            Collections.swap(viewArray, 1, 2);

            previous = viewArray.get(0);
            current = viewArray.get(1);
            next = viewArray.get(2);

            // Prepare the next page
            next.loadData(htmlContent, "text/html", null);

        } else if (direction == PREVIOUS_PAGE) {
            // Rearrange elements in webview array
            // Previous page becomes current page,
            // current becomes next,
            // next becomes previous.
            Collections.swap(viewArray, 1, 2);
            Collections.swap(viewArray, 0, 1);

            previous = viewArray.get(0);
            current = viewArray.get(1);
            next = viewArray.get(2);

            // Prepare the previous page
            previous.loadData(htmlContent, "text/html", null);
        }

        LayoutParams previousLayout = (LayoutParams) previous.getLayoutParams();
        previousLayout.leftMargin = LEFT_MARGIN;
        previousLayout.topMargin = -screenHeight - TOP_MARGIN;
        previous.setLayoutParams(previousLayout);

        LayoutParams currentLayout = (LayoutParams) current.getLayoutParams();
        currentLayout.leftMargin = LEFT_MARGIN;
        currentLayout.topMargin = 0;
        current.setLayoutParams(currentLayout);

        LayoutParams nextLayout = (LayoutParams) next.getLayoutParams();
        nextLayout.leftMargin = LEFT_MARGIN;
        nextLayout.topMargin = screenHeight + TOP_MARGIN;
        next.setLayoutParams(nextLayout);

        previous.scrollToBottom();
        next.scrollToTop();

        // I'm unsure if this is needed, but it works on everything but KitKat
        if (direction == NEXT_PAGE) {
            current.scrollToTop();
        } else {
            current.scrollToBottom();
        }
    } // end of changePages

    public void scrollToPageStart() {
        scrollTo(0,0);
    }

    public void scrollToPageBottom() { 
        // I know getScale() is deprecated; I take care of it.
        // This method works fine.
        int endOfContent = (int) Math.floor(getContentHeight() * getScale());
        int webViewHeight = getMeasuredHeight();
        scrollTo(0, endOfContent - webViewHeight);
    }
}

Upvotes: 0

Views: 1298

Answers (1)

marcin.kosiba
marcin.kosiba

Reputation: 3231

You're probably scrolling the WebView before it had finished loading the contents. The problem is that at some point during the page load the WebView resets the scroll (this is intentional, when you navigate between pages you don't want the scroll offset to persist) and sometimes this reset happens after you call scrollTo.

To fix this you have two options:

  1. scroll from JavaScript (in window.onload or something), this ensures the scroll happens after the WebView has finished loading the contents,
  2. wait for the contents to load before scrolling. This is harder since the WebView doesn't have reliable callbacks (onPageFinished will not work reliably). One option would be to poll for when the webview.getContentHeight() method is returning the height of your content before doing the scroll.

Upvotes: 1

Related Questions