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 27 import com.android.mail.R; 28 import com.android.mail.utils.LogTag; 29 import com.android.mail.utils.LogUtils; 30 31 import java.util.Set; 32 import java.util.concurrent.CopyOnWriteArraySet; 33 34 public 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 /** 220 * Returns the effective width available for HTML content in DP units. This width takes into 221 * account the given margin (in screen px) by excluding it. This is not the same as DOM width, 222 * since the document is rendered at CSS px={@link #mViewportWidth}. 223 * 224 * @param sideMarginPx HTML body margin, if any (in screen px) 225 * @return width available for HTML content (in dp) 226 */ 227 public int getWidthInDp(int sideMarginPx) { 228 return (int) ((getWidth() - sideMarginPx * 2) / mDensity); 229 } 230 231 /** 232 * Similar to {@link #getScale()}, except that it returns the initially expected scale, as 233 * determined by the ratio of actual screen pixels to logical HTML pixels. 234 * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport 235 * tag. 236 */ 237 public float getInitialScale() { 238 final float scale; 239 if (getSettings().getLoadWithOverviewMode()) { 240 // in overview mode (aka auto-fit mode), the base ratio is screen px : viewport px 241 scale = (float) getWidth() / getViewportWidth(); 242 } else { 243 // in no-zoom mode, the base ratio is just screen px : mdpi css px (i.e. density) 244 scale = mDensity; 245 } 246 return scale; 247 } 248 249 public int screenPxToWebPx(int screenPx) { 250 return (int) (screenPx / getInitialScale()); 251 } 252 253 public int webPxToScreenPx(int webPx) { 254 return (int) (webPx * getInitialScale()); 255 } 256 257 public float screenPxToWebPxError(int screenPx) { 258 return screenPx / getInitialScale() - screenPxToWebPx(screenPx); 259 } 260 261 public float webPxToScreenPxError(int webPx) { 262 return webPx * getInitialScale() - webPxToScreenPx(webPx); 263 } 264 265 } 266