ConversationWebView.java revision 916f819b1dfde084b8bd7941fd6f6d70d564e3e9
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 = null;
106        }
107    }
108
109    /**
110     * Enable this WebView to also draw to an internal software canvas until
111     * {@link #onVisibilityChanged(boolean)} is called. The software draw will happen every time
112     * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn
113     * (i.e. drawn in hardware) with the results of software rendering.
114     * <p>
115     * This is useful when you know that the WebView draws sooner to a software layer than it does
116     * to its normal hardware layer.
117     */
118    public void setUseSoftwareLayer(boolean useSoftware) {
119        mUseSoftwareLayer = useSoftware;
120        if (useSoftware) {
121            if (sMatrix == null) {
122                final Resources res = getContext().getResources();
123                sWebviewInitialDelay = res.getInteger(R.integer.webview_initial_delay);
124
125                final Display display = ((WindowManager) getContext().getSystemService(
126                        Context.WINDOW_SERVICE)).getDefaultDisplay();
127                sBitmapWidth = display.getWidth();
128                sBitmapHeight = display.getHeight();
129
130                sMatrix = new Matrix();
131                sPaint = new Paint();
132            }
133
134            // Create an offscreen bitmap.
135            mBitmap = Bitmap.createBitmap(sBitmapWidth, sBitmapHeight, Bitmap.Config.RGB_565);
136            mCanvas = new Canvas(mBitmap);
137        }
138    }
139
140    /**
141     * Notifies the {@link CustomWebView} that it has become visible. It can use this signal to
142     * switch between software and hardware layer.
143     */
144    public void onVisibilityChanged(boolean isVisible) {
145        if (isVisible && mUseSoftwareLayer) {
146            // Schedule to switch from software layer to hardware layer in 1s.
147            mHandler.postDelayed(mNotifyPageRenderedInHardwareLayer, sWebviewInitialDelay);
148        }
149    }
150
151    // NARROW_COLUMNS reflow can trigger the document to change size, so notify interested parties.
152    public interface ContentSizeChangeListener {
153        void onHeightChange(int h);
154    }
155
156    private ContentSizeChangeListener mSizeChangeListener;
157
158    private int mCachedContentHeight;
159
160    private final int mViewportWidth;
161    private final float mDensity;
162
163    private final Set<ScrollListener> mScrollListeners =
164            new CopyOnWriteArraySet<ScrollListener>();
165
166    /**
167     * True when WebView is handling a touch-- in between POINTER_DOWN and
168     * POINTER_UP/POINTER_CANCEL.
169     */
170    private boolean mHandlingTouch;
171
172    private static final String LOG_TAG = LogTag.getLogTag();
173
174    public ConversationWebView(Context c) {
175        this(c, null);
176    }
177
178    public ConversationWebView(Context c, AttributeSet attrs) {
179        super(c, attrs);
180
181        mViewportWidth = getResources().getInteger(R.integer.conversation_webview_viewport_px);
182        mDensity = getResources().getDisplayMetrics().density;
183    }
184
185    @Override
186    public void addScrollListener(ScrollListener l) {
187        mScrollListeners.add(l);
188    }
189
190    @Override
191    public void removeScrollListener(ScrollListener l) {
192        mScrollListeners.remove(l);
193    }
194
195    public void setContentSizeChangeListener(ContentSizeChangeListener l) {
196        mSizeChangeListener = l;
197    }
198
199
200    @Override
201    public int computeVerticalScrollRange() {
202        return super.computeVerticalScrollRange();
203    }
204
205    @Override
206    public int computeVerticalScrollOffset() {
207        return super.computeVerticalScrollOffset();
208    }
209
210    @Override
211    public int computeVerticalScrollExtent() {
212        return super.computeVerticalScrollExtent();
213    }
214
215    @Override
216    public int computeHorizontalScrollRange() {
217        return super.computeHorizontalScrollRange();
218    }
219
220    @Override
221    public int computeHorizontalScrollOffset() {
222        return super.computeHorizontalScrollOffset();
223    }
224
225    @Override
226    public int computeHorizontalScrollExtent() {
227        return super.computeHorizontalScrollExtent();
228    }
229
230    @Override
231    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
232        super.onScrollChanged(l, t, oldl, oldt);
233
234        // Update the visible portion of webview.
235        mLeft = l;
236        mTop = t;
237
238        for (ScrollListener listener : mScrollListeners) {
239            listener.onNotifierScroll(l, t);
240        }
241    }
242
243    @Override
244    public void invalidate() {
245        super.invalidate();
246
247        if (mSizeChangeListener != null) {
248            final int contentHeight = getContentHeight();
249            if (contentHeight != mCachedContentHeight) {
250                mCachedContentHeight = contentHeight;
251                mSizeChangeListener.onHeightChange(contentHeight);
252            }
253        }
254    }
255
256    @Override
257    public boolean onTouchEvent(MotionEvent ev) {
258        final int action = ev.getActionMasked();
259
260        switch (action) {
261            case MotionEvent.ACTION_DOWN:
262                mHandlingTouch = true;
263                break;
264            case MotionEvent.ACTION_POINTER_DOWN:
265                LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
266                requestDisallowInterceptTouchEvent(true);
267                break;
268            case MotionEvent.ACTION_CANCEL:
269            case MotionEvent.ACTION_UP:
270                mHandlingTouch = false;
271                break;
272        }
273
274        return super.onTouchEvent(ev);
275    }
276
277    public boolean isHandlingTouch() {
278        return mHandlingTouch;
279    }
280
281    public int getViewportWidth() {
282        return mViewportWidth;
283    }
284
285    /**
286     * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
287     * determined by the ratio of actual screen pixels to logical HTML pixels.
288     * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
289     * tag.
290     */
291    public float getInitialScale() {
292        // an HTML meta-viewport width of "device-width" and unspecified (medium) density means
293        // that the default scale is effectively the screen density.
294        return mDensity;
295    }
296
297    public int screenPxToWebPx(int screenPx) {
298        return (int) (screenPx / getInitialScale());
299    }
300
301    public int webPxToScreenPx(int webPx) {
302        return (int) (webPx * getInitialScale());
303    }
304
305    public float screenPxToWebPxError(int screenPx) {
306        return screenPx / getInitialScale() - screenPxToWebPx(screenPx);
307    }
308
309    public float webPxToScreenPxError(int webPx) {
310        return webPx * getInitialScale() - webPxToScreenPx(webPx);
311    }
312}
313