/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.gfx; 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.DisplayMetrics; import android.view.GestureDetector; import org.mozilla.gecko.ZoomConstraints; import org.mozilla.gecko.ui.PanZoomController; import org.mozilla.gecko.ui.PanZoomTarget; import org.mozilla.gecko.ui.SimpleScaleGestureDetector; /** * 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 implements PanZoomTarget { 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 ZoomConstraints mZoomConstraints; private boolean mForceRedraw; public LayerController(Context context) { mContext = context; mForceRedraw = true; DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); mViewportMetrics = new ImmutableViewportMetrics(new ViewportMetrics(displayMetrics)); mPanZoomController = new PanZoomController(this); mView = new LayerView(context, this); mCheckerboardShouldShowChecks = true; mZoomConstraints = new ZoomConstraints(false); } public void setRoot(Layer layer) { mRootLayer = layer; } public void setLayerClient(GeckoLayerClient layerClient) { mLayerClient = layerClient; layerClient.setLayerController(this); } public void destroy() { mPanZoomController.destroy(); } public void setForceRedraw() { mForceRedraw = true; notifyLayerClientOfGeometryChange(); } public Layer getRoot() { return mRootLayer; } public LayerView getView() { return mView; } public Context getContext() { return mContext; } public ImmutableViewportMetrics getViewportMetrics() { return mViewportMetrics; } public Object getLock() { return this; } public FloatSize getViewportSize() { return mViewportMetrics.getSize(); } 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; } public 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(); } } /** Sets the current page rect. You must hold the monitor while calling this. */ public void setPageRect(RectF rect, RectF cssRect) { // Since the "rect" is always just a multiple of "cssRect" we don't need to // check both; this function assumes that both "rect" and "cssRect" are relative // the zoom factor in mViewportMetrics. if (mViewportMetrics.getCssPageRect().equals(cssRect)) return; ViewportMetrics viewportMetrics = new ViewportMetrics(mViewportMetrics); viewportMetrics.setPageRect(rect, cssRect); 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.pageRectUpdated(); mView.requestRender(); } }); } /** * Sets the entire viewport metrics at once. * You must hold the monitor while calling this. */ public void setViewportMetrics(ViewportMetrics viewport) { mViewportMetrics = new ImmutableViewportMetrics(viewport); mView.requestRender(); notifyLayerClientOfGeometryChange(); } 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); } } public boolean post(Runnable action) { return mView.post(action); } private 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 setZoomConstraints(ZoomConstraints constraints) { mZoomConstraints = constraints; } public ZoomConstraints getZoomConstraints() { return mZoomConstraints; } }