| /* |
| * Copyright (C) 2012 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mail.browse; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.ScaleGestureDetector; |
| import android.view.ScaleGestureDetector.OnScaleGestureListener; |
| import android.webkit.WebView; |
| |
| import com.android.mail.R; |
| import com.android.mail.utils.LogTag; |
| import com.android.mail.utils.LogUtils; |
| |
| import java.util.Set; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| |
| public class ConversationWebView extends WebView implements ScrollNotifier { |
| /** The initial delay when rendering in hardware layer. */ |
| private final int mWebviewInitialDelay; |
| |
| private Bitmap mBitmap; |
| private Canvas mCanvas; |
| |
| private boolean mUseSoftwareLayer; |
| /** |
| * Whether this view is user-visible; we don't bother doing supplemental software drawing |
| * if the view is off-screen. |
| */ |
| private boolean mVisible; |
| |
| /** {@link Runnable} to be run when the page is rendered in hardware layer. */ |
| private final Runnable mNotifyPageRenderedInHardwareLayer = new Runnable() { |
| @Override |
| public void run() { |
| // Switch to hardware layer. |
| mUseSoftwareLayer = false; |
| destroyBitmap(); |
| invalidate(); |
| } |
| }; |
| |
| @Override |
| public void onDraw(Canvas canvas) { |
| // Always render in hardware layer to avoid flicker when switch. |
| super.onDraw(canvas); |
| |
| // Render in software layer on top if needed, and we're visible (i.e. it's worthwhile to |
| // do all this) |
| if (mUseSoftwareLayer && mVisible && getWidth() > 0 && getHeight() > 0) { |
| if (mBitmap == null) { |
| try { |
| // Create an offscreen bitmap. |
| mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565); |
| mCanvas = new Canvas(mBitmap); |
| } catch (OutOfMemoryError e) { |
| // just give up |
| mBitmap = null; |
| mCanvas = null; |
| mUseSoftwareLayer = false; |
| } |
| } |
| |
| if (mBitmap != null) { |
| final int x = getScrollX(); |
| final int y = getScrollY(); |
| |
| mCanvas.save(); |
| mCanvas.translate(-x, -y); |
| super.onDraw(mCanvas); |
| mCanvas.restore(); |
| |
| canvas.drawBitmap(mBitmap, x, y, null /* paint */); |
| } |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| destroyBitmap(); |
| removeCallbacks(mNotifyPageRenderedInHardwareLayer); |
| |
| super.destroy(); |
| } |
| |
| /** |
| * Destroys the {@link Bitmap} used for software layer. |
| */ |
| private void destroyBitmap() { |
| if (mBitmap != null) { |
| mBitmap = null; |
| mCanvas = null; |
| } |
| } |
| |
| /** |
| * Enable this WebView to also draw to an internal software canvas until |
| * {@link #onRenderComplete()} is called. The software draw will happen every time |
| * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn |
| * (i.e. drawn in hardware) with the results of software rendering. |
| * <p> |
| * This is useful when you know that the WebView draws sooner to a software layer than it does |
| * to its normal hardware layer. |
| */ |
| public void setUseSoftwareLayer(boolean useSoftware) { |
| mUseSoftwareLayer = useSoftware; |
| } |
| |
| /** |
| * Notifies the {@link ConversationWebView} that it has become visible. It can use this signal |
| * to switch between software and hardware layer. |
| */ |
| public void onRenderComplete() { |
| if (mUseSoftwareLayer) { |
| // Schedule to switch from software layer to hardware layer in 1s. |
| postDelayed(mNotifyPageRenderedInHardwareLayer, mWebviewInitialDelay); |
| } |
| } |
| |
| public void onUserVisibilityChanged(boolean visible) { |
| mVisible = visible; |
| } |
| |
| // NARROW_COLUMNS reflow can trigger the document to change size, so notify interested parties. |
| public interface ContentSizeChangeListener { |
| void onHeightChange(int h); |
| } |
| |
| private ContentSizeChangeListener mSizeChangeListener; |
| |
| private ScaleGestureDetector mScaleDetector; |
| |
| private int mCachedContentHeight; |
| |
| private final int mViewportWidth; |
| private final float mDensity; |
| |
| private final Set<ScrollListener> mScrollListeners = |
| new CopyOnWriteArraySet<ScrollListener>(); |
| |
| /** |
| * True when WebView is handling a touch-- in between POINTER_DOWN and |
| * POINTER_UP/POINTER_CANCEL. |
| */ |
| private boolean mHandlingTouch; |
| private boolean mIgnoringTouch; |
| |
| private static final String LOG_TAG = LogTag.getLogTag(); |
| |
| public ConversationWebView(Context c) { |
| this(c, null); |
| } |
| |
| public ConversationWebView(Context c, AttributeSet attrs) { |
| super(c, attrs); |
| |
| final Resources r = getResources(); |
| mViewportWidth = r.getInteger(R.integer.conversation_webview_viewport_px); |
| mWebviewInitialDelay = r.getInteger(R.integer.webview_initial_delay); |
| mDensity = r.getDisplayMetrics().density; |
| } |
| |
| @Override |
| public void addScrollListener(ScrollListener l) { |
| mScrollListeners.add(l); |
| } |
| |
| @Override |
| public void removeScrollListener(ScrollListener l) { |
| mScrollListeners.remove(l); |
| } |
| |
| public void setContentSizeChangeListener(ContentSizeChangeListener l) { |
| mSizeChangeListener = l; |
| } |
| |
| public void setOnScaleGestureListener(OnScaleGestureListener l) { |
| if (l == null) { |
| mScaleDetector = null; |
| } else { |
| mScaleDetector = new ScaleGestureDetector(getContext(), l); |
| } |
| } |
| |
| @Override |
| public int computeVerticalScrollRange() { |
| return super.computeVerticalScrollRange(); |
| } |
| |
| @Override |
| public int computeVerticalScrollOffset() { |
| return super.computeVerticalScrollOffset(); |
| } |
| |
| @Override |
| public int computeVerticalScrollExtent() { |
| return super.computeVerticalScrollExtent(); |
| } |
| |
| @Override |
| public int computeHorizontalScrollRange() { |
| return super.computeHorizontalScrollRange(); |
| } |
| |
| @Override |
| public int computeHorizontalScrollOffset() { |
| return super.computeHorizontalScrollOffset(); |
| } |
| |
| @Override |
| public int computeHorizontalScrollExtent() { |
| return super.computeHorizontalScrollExtent(); |
| } |
| |
| @Override |
| protected void onScrollChanged(int l, int t, int oldl, int oldt) { |
| super.onScrollChanged(l, t, oldl, oldt); |
| |
| for (ScrollListener listener : mScrollListeners) { |
| listener.onNotifierScroll(l, t); |
| } |
| } |
| |
| @Override |
| public void invalidate() { |
| super.invalidate(); |
| |
| if (mSizeChangeListener != null) { |
| final int contentHeight = getContentHeight(); |
| if (contentHeight != mCachedContentHeight) { |
| mCachedContentHeight = contentHeight; |
| mSizeChangeListener.onHeightChange(contentHeight); |
| } |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| final int action = ev.getActionMasked(); |
| |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mHandlingTouch = true; |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN"); |
| requestDisallowInterceptTouchEvent(true); |
| if (mScaleDetector != null) { |
| mIgnoringTouch = true; |
| final MotionEvent fakeCancel = MotionEvent.obtain(ev); |
| fakeCancel.setAction(MotionEvent.ACTION_CANCEL); |
| super.onTouchEvent(fakeCancel); |
| } |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| mHandlingTouch = false; |
| mIgnoringTouch = false; |
| break; |
| } |
| |
| final boolean handled = mIgnoringTouch || super.onTouchEvent(ev); |
| |
| if (mScaleDetector != null) { |
| mScaleDetector.onTouchEvent(ev); |
| } |
| |
| return handled; |
| } |
| |
| public boolean isHandlingTouch() { |
| return mHandlingTouch; |
| } |
| |
| public int getViewportWidth() { |
| return mViewportWidth; |
| } |
| |
| /** |
| * Similar to {@link #getScale()}, except that it returns the initially expected scale, as |
| * determined by the ratio of actual screen pixels to logical HTML pixels. |
| * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport |
| * tag. |
| */ |
| public float getInitialScale() { |
| // an HTML meta-viewport width of "device-width" and unspecified (medium) density means |
| // that the default scale is effectively the screen density. |
| return mDensity; |
| } |
| |
| public int screenPxToWebPx(int screenPx) { |
| return (int) (screenPx / getInitialScale()); |
| } |
| |
| public int webPxToScreenPx(int webPx) { |
| return (int) (webPx * getInitialScale()); |
| } |
| |
| public float screenPxToWebPxError(int screenPx) { |
| return screenPx / getInitialScale() - screenPxToWebPx(screenPx); |
| } |
| |
| public float webPxToScreenPxError(int webPx) { |
| return webPx * getInitialScale() - webPxToScreenPx(webPx); |
| } |
| |
| } |