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