ConversationWebView.java revision afc9b365dc9199ee9b2a1e598b8f40b3c78b6d9f
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.graphics.Matrix;
25import android.graphics.Paint;
26import android.os.Handler;
27import android.util.AttributeSet;
28import android.view.Display;
29import android.view.MotionEvent;
30import android.view.WindowManager;
31import android.webkit.WebView;
32
33import com.android.mail.R;
34import com.android.mail.utils.LogTag;
35import com.android.mail.utils.LogUtils;
36
37import java.util.Set;
38import java.util.concurrent.CopyOnWriteArraySet;
39
40public class ConversationWebView extends WebView implements ScrollNotifier {
41    /** The initial delay when rendering in hardware layer. */
42    private static int sWebviewInitialDelay;
43
44    /** The dimension of the offscreen bitmap used for software layer. */
45    private static int sBitmapWidth;
46    private static int sBitmapHeight;
47
48    /** The default {@link Matrix} and {@link Paint} used to draw software layer. */
49    private static Matrix sMatrix;
50    private static Paint sPaint;
51
52    private final Handler mHandler = new Handler();
53
54    private Bitmap mBitmap;
55    private Canvas mCanvas;
56
57    private boolean mUseSoftwareLayer;
58
59    /** The visible portion of webview. */
60    private int mLeft = 0;
61    private int mTop = 0;
62
63    /** {@link Runnable} to be run when the page is rendered in hardware layer. */
64    private final Runnable mNotifyPageRenderedInHardwareLayer = new Runnable() {
65        @Override
66        public void run() {
67            // Switch to hardware layer.
68            mUseSoftwareLayer = false;
69            destroyBitmap();
70            invalidate();
71        }
72    };
73
74    @Override
75    public void onDraw(Canvas canvas) {
76        // Always render in hardware layer to avoid flicker when switch.
77        super.onDraw(canvas);
78
79        // Render in software layer on top if needed.
80        if (mUseSoftwareLayer && mBitmap != null) {
81            mCanvas.save();
82            mCanvas.translate(-mLeft, -mTop);
83            super.onDraw(mCanvas);
84            mCanvas.restore();
85            canvas.save();
86            canvas.translate(mLeft, mTop);
87            canvas.drawBitmap(mBitmap, sMatrix, sPaint);
88            canvas.restore();
89        }
90    }
91
92    @Override
93    public void destroy() {
94        destroyBitmap();
95        mHandler.removeCallbacksAndMessages(null);
96
97        super.destroy();
98    }
99
100    /**
101     * Destroys the {@link Bitmap} used for software layer.
102     */
103    private void destroyBitmap() {
104        if (mBitmap != null) {
105            mBitmap.recycle();
106            mBitmap = null;
107        }
108    }
109
110    /**
111     * Enable this WebView to also draw to an internal software canvas until
112     * {@link #onVisibilityChanged(boolean)} is called. The software draw will happen every time
113     * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn
114     * (i.e. drawn in hardware) with the results of software rendering.
115     * <p>
116     * This is useful when you know that the WebView draws sooner to a software layer than it does
117     * to its normal hardware layer.
118     */
119    public void setUseSoftwareLayer(boolean useSoftware) {
120        mUseSoftwareLayer = useSoftware;
121    }
122
123    @Override
124    public void onFinishInflate() {
125        if (sMatrix == null) {
126            final Resources res = getContext().getResources();
127            sWebviewInitialDelay = res.getInteger(R.integer.webview_initial_delay);
128
129            final Display display = ((WindowManager) getContext()
130                    .getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
131            sBitmapWidth = display.getWidth();
132            sBitmapHeight = display.getHeight();
133
134            sMatrix = new Matrix();
135            sPaint = new Paint();
136        }
137
138        // Create an offscreen bitmap.
139        mBitmap = Bitmap.createBitmap(sBitmapWidth, sBitmapHeight, Bitmap.Config.RGB_565);
140        mCanvas = new Canvas(mBitmap);
141    }
142
143    /**
144     * Notifies the {@link CustomWebView} that it has become visible. It can use this signal to
145     * switch between software and hardware layer.
146     */
147    public void onVisibilityChanged(boolean isVisible) {
148        if (isVisible && mUseSoftwareLayer) {
149            // Schedule to switch from software layer to hardware layer in 1s.
150            mHandler.postDelayed(mNotifyPageRenderedInHardwareLayer, sWebviewInitialDelay);
151        }
152    }
153
154    // NARROW_COLUMNS reflow can trigger the document to change size, so notify interested parties.
155    public interface ContentSizeChangeListener {
156        void onHeightChange(int h);
157    }
158
159    private ContentSizeChangeListener mSizeChangeListener;
160
161    private int mCachedContentHeight;
162
163    private final int mViewportWidth;
164    private final float mDensity;
165
166    private final Set<ScrollListener> mScrollListeners =
167            new CopyOnWriteArraySet<ScrollListener>();
168
169    /**
170     * True when WebView is handling a touch-- in between POINTER_DOWN and
171     * POINTER_UP/POINTER_CANCEL.
172     */
173    private boolean mHandlingTouch;
174
175    private static final String LOG_TAG = LogTag.getLogTag();
176
177    public ConversationWebView(Context c) {
178        this(c, null);
179    }
180
181    public ConversationWebView(Context c, AttributeSet attrs) {
182        super(c, attrs);
183
184        mViewportWidth = getResources().getInteger(R.integer.conversation_webview_viewport_px);
185        mDensity = getResources().getDisplayMetrics().density;
186    }
187
188    @Override
189    public void addScrollListener(ScrollListener l) {
190        mScrollListeners.add(l);
191    }
192
193    @Override
194    public void removeScrollListener(ScrollListener l) {
195        mScrollListeners.remove(l);
196    }
197
198    public void setContentSizeChangeListener(ContentSizeChangeListener l) {
199        mSizeChangeListener = l;
200    }
201
202
203    @Override
204    public int computeVerticalScrollRange() {
205        return super.computeVerticalScrollRange();
206    }
207
208    @Override
209    public int computeVerticalScrollOffset() {
210        return super.computeVerticalScrollOffset();
211    }
212
213    @Override
214    public int computeVerticalScrollExtent() {
215        return super.computeVerticalScrollExtent();
216    }
217
218    @Override
219    public int computeHorizontalScrollRange() {
220        return super.computeHorizontalScrollRange();
221    }
222
223    @Override
224    public int computeHorizontalScrollOffset() {
225        return super.computeHorizontalScrollOffset();
226    }
227
228    @Override
229    public int computeHorizontalScrollExtent() {
230        return super.computeHorizontalScrollExtent();
231    }
232
233    @Override
234    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
235        super.onScrollChanged(l, t, oldl, oldt);
236
237        // Update the visible portion of webview.
238        mLeft = l;
239        mTop = t;
240
241        for (ScrollListener listener : mScrollListeners) {
242            listener.onNotifierScroll(l, t);
243        }
244    }
245
246    @Override
247    public void invalidate() {
248        super.invalidate();
249
250        if (mSizeChangeListener != null) {
251            final int contentHeight = getContentHeight();
252            if (contentHeight != mCachedContentHeight) {
253                mCachedContentHeight = contentHeight;
254                mSizeChangeListener.onHeightChange(contentHeight);
255            }
256        }
257    }
258
259    @Override
260    public boolean onTouchEvent(MotionEvent ev) {
261        final int action = ev.getActionMasked();
262
263        switch (action) {
264            case MotionEvent.ACTION_DOWN:
265                mHandlingTouch = true;
266                break;
267            case MotionEvent.ACTION_POINTER_DOWN:
268                LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
269                requestDisallowInterceptTouchEvent(true);
270                break;
271            case MotionEvent.ACTION_CANCEL:
272            case MotionEvent.ACTION_UP:
273                mHandlingTouch = false;
274                break;
275        }
276
277        return super.onTouchEvent(ev);
278    }
279
280    public boolean isHandlingTouch() {
281        return mHandlingTouch;
282    }
283
284    public int getViewportWidth() {
285        return mViewportWidth;
286    }
287
288    /**
289     * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
290     * determined by the ratio of actual screen pixels to logical HTML pixels.
291     * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
292     * tag.
293     */
294    public float getInitialScale() {
295        // an HTML meta-viewport width of "device-width" and unspecified (medium) density means
296        // that the default scale is effectively the screen density.
297        return mDensity;
298    }
299
300    public int screenPxToWebPx(int screenPx) {
301        return (int) (screenPx / getInitialScale());
302    }
303
304    public int webPxToScreenPx(int webPx) {
305        return (int) (webPx * getInitialScale());
306    }
307
308    public float screenPxToWebPxError(int screenPx) {
309        return screenPx / getInitialScale() - screenPxToWebPx(screenPx);
310    }
311
312    public float webPxToScreenPxError(int webPx) {
313        return webPx * getInitialScale() - webPxToScreenPx(webPx);
314    }
315}
316