blob: 506674c099d284bbf0bc909594ad58e7d44c1b9e [file] [log] [blame]
/*
* 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 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 MailWebView 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;
}
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);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
for (ScrollListener listener : mScrollListeners) {
listener.onNotifierScroll(t);
}
}
@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);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mHandlingTouch = false;
mIgnoringTouch = false;
break;
}
final boolean handled = mIgnoringTouch || super.onTouchEvent(ev);
return handled;
}
public boolean isHandlingTouch() {
return mHandlingTouch;
}
public int getViewportWidth() {
return mViewportWidth;
}
/**
* Returns the effective width available for HTML content in DP units. This width takes into
* account the given margin (in screen px) by excluding it. This is not the same as DOM width,
* since the document is rendered at CSS px={@link #mViewportWidth}.
*
* @param sideMarginPx HTML body margin, if any (in screen px)
* @return width available for HTML content (in dp)
*/
public int getWidthInDp(int sideMarginPx) {
return (int) ((getWidth() - sideMarginPx * 2) / mDensity);
}
/**
* 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() {
final float scale;
if (getSettings().getLoadWithOverviewMode()) {
// in overview mode (aka auto-fit mode), the base ratio is screen px : viewport px
scale = (float) getWidth() / getViewportWidth();
} else {
// in no-zoom mode, the base ratio is just screen px : mdpi css px (i.e. density)
scale = mDensity;
}
return scale;
}
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);
}
}