/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Android code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009-2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Patrick Walton * Chris Lord * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.gecko.gfx; import org.mozilla.gecko.gfx.Layer; import org.mozilla.gecko.ui.PanZoomController; import org.mozilla.gecko.ui.SimpleScaleGestureDetector; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PointF; import android.graphics.RectF; import android.util.Log; import android.view.GestureDetector; import android.view.View.OnTouchListener; /** * The layer controller manages a tile that represents the visible page. It does panning and * zooming natively by delegating to a panning/zooming controller. Touch events can be dispatched * to a higher-level view. * * Many methods require that the monitor be held, with a synchronized (controller) { ... } block. */ public class LayerController { private static final String LOGTAG = "GeckoLayerController"; private Layer mRootLayer; /* The root layer. */ private LayerView mView; /* The main rendering view. */ private Context mContext; /* The current context. */ /* This is volatile so that we can read and write to it from different threads. * We avoid synchronization to make getting the viewport metrics from * the compositor as cheap as possible. The viewport is immutable so * we don't need to worry about anyone mutating it while we're reading from it. * Specifically: * 1) reading mViewportMetrics from any thread is fine without synchronization * 2) writing to mViewportMetrics requires synchronizing on the layer controller object * 3) whenver reading multiple fields from mViewportMetrics without synchronization (i.e. in * case 1 above) you should always frist grab a local copy of the reference, and then use * that because mViewportMetrics might get reassigned in between reading the different * fields. */ private volatile ImmutableViewportMetrics mViewportMetrics; /* The current viewport metrics. */ /* * The panning and zooming controller, which interprets pan and zoom gestures for us and * updates our visible rect appropriately. */ private PanZoomController mPanZoomController; private GeckoLayerClient mLayerClient; /* The layer client. */ /* The new color for the checkerboard. */ private int mCheckerboardColor = Color.WHITE; private boolean mCheckerboardShouldShowChecks; private boolean mAllowZoom; private float mDefaultZoom; private boolean mForceRedraw; public LayerController(Context context) { mContext = context; mForceRedraw = true; mViewportMetrics = new ImmutableViewportMetrics(new ViewportMetrics()); mPanZoomController = new PanZoomController(this); mView = new LayerView(context, this); mCheckerboardShouldShowChecks = true; } public void setRoot(Layer layer) { mRootLayer = layer; } public void setLayerClient(GeckoLayerClient layerClient) { mLayerClient = layerClient; layerClient.setLayerController(this); } public void setForceRedraw() { mForceRedraw = true; } public Layer getRoot() { return mRootLayer; } public LayerView getView() { return mView; } public Context getContext() { return mContext; } public ImmutableViewportMetrics getViewportMetrics() { return mViewportMetrics; } public RectF getViewport() { return mViewportMetrics.getViewport(); } public RectF getCssViewport() { return mViewportMetrics.getCssViewport(); } public FloatSize getViewportSize() { return mViewportMetrics.getSize(); } public FloatSize getPageSize() { return mViewportMetrics.getPageSize(); } public FloatSize getCssPageSize() { return mViewportMetrics.getCssPageSize(); } public PointF getOrigin() { return mViewportMetrics.getOrigin(); } public float getZoomFactor() { return mViewportMetrics.zoomFactor; } public Bitmap getBackgroundPattern() { return getDrawable("background"); } public Bitmap getShadowPattern() { return getDrawable("shadow"); } public PanZoomController getPanZoomController() { return mPanZoomController; } public GestureDetector.OnGestureListener getGestureListener() { return mPanZoomController; } public SimpleScaleGestureDetector.SimpleScaleGestureListener getScaleGestureListener() { return mPanZoomController; } public GestureDetector.OnDoubleTapListener getDoubleTapListener() { return mPanZoomController; } private Bitmap getDrawable(String name) { Resources resources = mContext.getResources(); int resourceID = resources.getIdentifier(name, "drawable", mContext.getPackageName()); BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; return BitmapFactory.decodeResource(mContext.getResources(), resourceID, options); } /** * The view calls this function to indicate that the viewport changed size. It must hold the * monitor while calling it. * * TODO: Refactor this to use an interface. Expose that interface only to the view and not * to the layer client. That way, the layer client won't be tempted to call this, which might * result in an infinite loop. */ public void setViewportSize(FloatSize size) { ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); viewportMetrics.setSize(size); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); if (mLayerClient != null) { mLayerClient.viewportSizeChanged(); } } /** Scrolls the viewport by the given offset. You must hold the monitor while calling this. */ public void scrollBy(PointF point) { ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); PointF origin = viewportMetrics.getOrigin(); origin.offset(point.x, point.y); viewportMetrics.setOrigin(origin); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); notifyLayerClientOfGeometryChange(); mView.requestRender(); } /** Sets the current page size. You must hold the monitor while calling this. */ public void setPageSize(FloatSize size, FloatSize cssSize) { if (mViewportMetrics.getCssPageSize().equals(cssSize)) return; ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); viewportMetrics.setPageSize(size, cssSize); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); // Page size is owned by the layer client, so no need to notify it of // this change. mView.post(new Runnable() { public void run() { mPanZoomController.pageSizeUpdated(); mView.requestRender(); } }); } /** * Sets the entire viewport metrics at once. This function does not notify the layer client or * the pan/zoom controller, so you will need to call notifyLayerClientOfGeometryChange() or * notifyPanZoomControllerOfGeometryChange() after calling this. You must hold the monitor * while calling this. */ public void setViewportMetrics(ViewportMetrics viewport) { mViewportMetrics = new ImmutableViewportMetrics(viewport); mView.requestRender(); } public void setAnimationTarget(ViewportMetrics viewport) { if (mLayerClient != null) { // We know what the final viewport of the animation is going to be, so // immediately request a draw of that area by setting the display port // accordingly. This way we should have the content pre-rendered by the // time the animation is done. ImmutableViewportMetrics metrics = new ImmutableViewportMetrics(viewport); DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null); mLayerClient.adjustViewport(displayPort); } } /** * Scales the viewport, keeping the given focus point in the same place before and after the * scale operation. You must hold the monitor while calling this. */ public void scaleWithFocus(float zoomFactor, PointF focus) { ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); viewportMetrics.scaleTo(zoomFactor, focus); mViewportMetrics = new ImmutableViewportMetrics(viewportMetrics); // We assume the zoom level will only be modified by the // PanZoomController, so no need to notify it of this change. notifyLayerClientOfGeometryChange(); mView.requestRender(); } public boolean post(Runnable action) { return mView.post(action); } /** * The view as well as the controller itself use this method to notify the layer client that * the geometry changed. */ public void notifyLayerClientOfGeometryChange() { if (mLayerClient != null) mLayerClient.geometryChanged(); } /** Aborts any pan/zoom animation that is currently in progress. */ public void abortPanZoomAnimation() { if (mPanZoomController != null) { mView.post(new Runnable() { public void run() { mPanZoomController.abortAnimation(); } }); } } /** * Returns true if this controller is fine with performing a redraw operation or false if it * would prefer that the action didn't take place. */ public boolean getRedrawHint() { if (mForceRedraw) { mForceRedraw = false; return true; } if (!mPanZoomController.getRedrawHint()) { return false; } return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics, mPanZoomController.getVelocityVector(), mLayerClient.getDisplayPort()); } /** * Converts a point from layer view coordinates to layer coordinates. In other words, given a * point measured in pixels from the top left corner of the layer view, returns the point in * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the * events being sent to Gecko are processed in FIFO order, this calculation should always be * correct. */ public PointF convertViewPointToLayerPoint(PointF viewPoint) { if (mLayerClient == null) { return null; } ImmutableViewportMetrics viewportMetrics = mViewportMetrics; PointF origin = viewportMetrics.getOrigin(); float zoom = viewportMetrics.zoomFactor; ViewportMetrics geckoViewport = mLayerClient.getGeckoViewportMetrics(); PointF geckoOrigin = geckoViewport.getOrigin(); float geckoZoom = geckoViewport.getZoomFactor(); // viewPoint + origin gives the coordinate in device pixels from the top-left corner of the page. // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page. // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from // the current Gecko coordinate in CSS pixels. PointF layerPoint = new PointF( ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom), ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom)); return layerPoint; } /** Retrieves whether we should show checkerboard checks or not. */ public boolean checkerboardShouldShowChecks() { return mCheckerboardShouldShowChecks; } /** Retrieves the color that the checkerboard should be. */ public int getCheckerboardColor() { return mCheckerboardColor; } /** Sets whether or not the checkerboard should show checkmarks. */ public void setCheckerboardShowChecks(boolean showChecks) { mCheckerboardShouldShowChecks = showChecks; mView.requestRender(); } /** Sets a new color for the checkerboard. */ public void setCheckerboardColor(int newColor) { mCheckerboardColor = newColor; mView.requestRender(); } public void setAllowZoom(final boolean aValue) { mAllowZoom = aValue; mView.post(new Runnable() { public void run() { mView.getTouchEventHandler().setDoubleTapEnabled(aValue); } }); } public boolean getAllowZoom() { return mAllowZoom; } public void setDefaultZoom(float aValue) { mDefaultZoom = aValue; } public float getDefaultZoom() { return mDefaultZoom; } }