ConversationWebView.java revision 4dc732387454eef3ee6d89f9fa393630eb6213f9
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26
27import com.android.mail.R;
28import com.android.mail.utils.LogTag;
29import com.android.mail.utils.LogUtils;
30
31import java.util.Set;
32import java.util.concurrent.CopyOnWriteArraySet;
33
34public class ConversationWebView extends MailWebView implements ScrollNotifier {
35    /** The initial delay when rendering in hardware layer. */
36    private final int mWebviewInitialDelay;
37
38    private Bitmap mBitmap;
39    private Canvas mCanvas;
40
41    private boolean mUseSoftwareLayer;
42    /**
43     * Whether this view is user-visible; we don't bother doing supplemental software drawing
44     * if the view is off-screen.
45     */
46    private boolean mVisible;
47
48    /** {@link Runnable} to be run when the page is rendered in hardware layer. */
49    private final Runnable mNotifyPageRenderedInHardwareLayer = new Runnable() {
50        @Override
51        public void run() {
52            // Switch to hardware layer.
53            mUseSoftwareLayer = false;
54            destroyBitmap();
55            invalidate();
56        }
57    };
58
59    @Override
60    public void onDraw(Canvas canvas) {
61        // Always render in hardware layer to avoid flicker when switch.
62        super.onDraw(canvas);
63
64        // Render in software layer on top if needed, and we're visible (i.e. it's worthwhile to
65        // do all this)
66        if (mUseSoftwareLayer && mVisible && getWidth() > 0 && getHeight() > 0) {
67            if (mBitmap == null) {
68                try {
69                    // Create an offscreen bitmap.
70                    mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
71                    mCanvas = new Canvas(mBitmap);
72                } catch (OutOfMemoryError e) {
73                    // just give up
74                    mBitmap = null;
75                    mCanvas = null;
76                    mUseSoftwareLayer = false;
77                }
78            }
79
80            if (mBitmap != null) {
81                final int x = getScrollX();
82                final int y = getScrollY();
83
84                mCanvas.save();
85                mCanvas.translate(-x, -y);
86                super.onDraw(mCanvas);
87                mCanvas.restore();
88
89                canvas.drawBitmap(mBitmap, x, y, null /* paint */);
90            }
91        }
92    }
93
94    @Override
95    public void destroy() {
96        destroyBitmap();
97        removeCallbacks(mNotifyPageRenderedInHardwareLayer);
98
99        super.destroy();
100    }
101
102    /**
103     * Destroys the {@link Bitmap} used for software layer.
104     */
105    private void destroyBitmap() {
106        if (mBitmap != null) {
107            mBitmap = null;
108            mCanvas = null;
109        }
110    }
111
112    /**
113     * Enable this WebView to also draw to an internal software canvas until
114     * {@link #onRenderComplete()} is called. The software draw will happen every time
115     * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn
116     * (i.e. drawn in hardware) with the results of software rendering.
117     * <p>
118     * This is useful when you know that the WebView draws sooner to a software layer than it does
119     * to its normal hardware layer.
120     */
121    public void setUseSoftwareLayer(boolean useSoftware) {
122        mUseSoftwareLayer = useSoftware;
123    }
124
125    /**
126     * Notifies the {@link ConversationWebView} that it has become visible. It can use this signal
127     * to switch between software and hardware layer.
128     */
129    public void onRenderComplete() {
130        if (mUseSoftwareLayer) {
131            // Schedule to switch from software layer to hardware layer in 1s.
132            postDelayed(mNotifyPageRenderedInHardwareLayer, mWebviewInitialDelay);
133        }
134    }
135
136    public void onUserVisibilityChanged(boolean visible) {
137        mVisible = visible;
138    }
139
140    private final int mViewportWidth;
141    private final float mDensity;
142
143    private final Set<ScrollListener> mScrollListeners =
144            new CopyOnWriteArraySet<ScrollListener>();
145
146    /**
147     * True when WebView is handling a touch-- in between POINTER_DOWN and
148     * POINTER_UP/POINTER_CANCEL.
149     */
150    private boolean mHandlingTouch;
151    private boolean mIgnoringTouch;
152
153    private static final String LOG_TAG = LogTag.getLogTag();
154
155    public ConversationWebView(Context c) {
156        this(c, null);
157    }
158
159    public ConversationWebView(Context c, AttributeSet attrs) {
160        super(c, attrs);
161
162        final Resources r = getResources();
163        mViewportWidth = r.getInteger(R.integer.conversation_webview_viewport_px);
164        mWebviewInitialDelay = r.getInteger(R.integer.webview_initial_delay);
165        mDensity = r.getDisplayMetrics().density;
166    }
167
168    @Override
169    public void addScrollListener(ScrollListener l) {
170        mScrollListeners.add(l);
171    }
172
173    @Override
174    public void removeScrollListener(ScrollListener l) {
175        mScrollListeners.remove(l);
176    }
177
178    @Override
179    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
180        super.onScrollChanged(l, t, oldl, oldt);
181
182        for (ScrollListener listener : mScrollListeners) {
183            listener.onNotifierScroll(t);
184        }
185    }
186
187    @Override
188    public boolean onTouchEvent(MotionEvent ev) {
189        final int action = ev.getActionMasked();
190
191        switch (action) {
192            case MotionEvent.ACTION_DOWN:
193                mHandlingTouch = true;
194                break;
195            case MotionEvent.ACTION_POINTER_DOWN:
196                LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
197                requestDisallowInterceptTouchEvent(true);
198                break;
199            case MotionEvent.ACTION_CANCEL:
200            case MotionEvent.ACTION_UP:
201                mHandlingTouch = false;
202                mIgnoringTouch = false;
203                break;
204        }
205
206        final boolean handled = mIgnoringTouch || super.onTouchEvent(ev);
207
208        return handled;
209    }
210
211    public boolean isHandlingTouch() {
212        return mHandlingTouch;
213    }
214
215    public int getViewportWidth() {
216        return mViewportWidth;
217    }
218
219    public int getWidthInDp() {
220        return (int) (getWidth() / mDensity);
221    }
222
223    /**
224     * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
225     * determined by the ratio of actual screen pixels to logical HTML pixels.
226     * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
227     * tag.
228     */
229    public float getInitialScale() {
230        final float scale;
231        if (getSettings().getLoadWithOverviewMode()) {
232            // in overview mode (aka auto-fit mode), the base ratio is screen px : viewport px
233            scale = (float) getWidth() / getViewportWidth();
234        } else {
235            // in no-zoom mode, the base ratio is just screen px : mdpi css px (i.e. density)
236            scale = mDensity;
237        }
238        return scale;
239    }
240
241    public int screenPxToWebPx(int screenPx) {
242        return (int) (screenPx / getInitialScale());
243    }
244
245    public int webPxToScreenPx(int webPx) {
246        return (int) (webPx * getInitialScale());
247    }
248
249    public float screenPxToWebPxError(int screenPx) {
250        return screenPx / getInitialScale() - screenPxToWebPx(screenPx);
251    }
252
253    public float webPxToScreenPxError(int webPx) {
254        return webPx * getInitialScale() - webPxToScreenPx(webPx);
255    }
256
257}
258