mobile/android/base/gfx/LayerView.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.gfx;
     8 import org.mozilla.gecko.GeckoAccessibility;
     9 import org.mozilla.gecko.GeckoAppShell;
    10 import org.mozilla.gecko.GeckoEvent;
    11 import org.mozilla.gecko.PrefsHelper;
    12 import org.mozilla.gecko.R;
    13 import org.mozilla.gecko.Tab;
    14 import org.mozilla.gecko.Tabs;
    15 import org.mozilla.gecko.TouchEventInterceptor;
    16 import org.mozilla.gecko.ZoomConstraints;
    17 import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
    18 import org.mozilla.gecko.mozglue.RobocopTarget;
    19 import org.mozilla.gecko.EventDispatcher;
    21 import android.content.Context;
    22 import android.graphics.Bitmap;
    23 import android.graphics.BitmapFactory;
    24 import android.graphics.Canvas;
    25 import android.graphics.Color;
    26 import android.graphics.Point;
    27 import android.graphics.PointF;
    28 import android.graphics.Rect;
    29 import android.graphics.SurfaceTexture;
    30 import android.os.Build;
    31 import android.os.Handler;
    32 import android.util.AttributeSet;
    33 import android.util.DisplayMetrics;
    34 import android.util.Log;
    35 import android.view.KeyEvent;
    36 import android.view.MotionEvent;
    37 import android.view.SurfaceHolder;
    38 import android.view.SurfaceView;
    39 import android.view.TextureView;
    40 import android.view.View;
    41 import android.view.ViewGroup;
    42 import android.view.inputmethod.EditorInfo;
    43 import android.view.inputmethod.InputConnection;
    44 import android.widget.FrameLayout;
    46 import java.nio.IntBuffer;
    47 import java.util.ArrayList;
    49 /**
    50  * A view rendered by the layer compositor.
    51  *
    52  * Note that LayerView is accessed by Robocop via reflection.
    53  */
    54 public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener {
    55     private static String LOGTAG = "GeckoLayerView";
    57     private GeckoLayerClient mLayerClient;
    58     private PanZoomController mPanZoomController;
    59     private LayerMarginsAnimator mMarginsAnimator;
    60     private GLController mGLController;
    61     private InputConnectionHandler mInputConnectionHandler;
    62     private LayerRenderer mRenderer;
    63     /* Must be a PAINT_xxx constant */
    64     private int mPaintState;
    65     private int mBackgroundColor;
    66     private boolean mFullScreen;
    68     private SurfaceView mSurfaceView;
    69     private TextureView mTextureView;
    71     private Listener mListener;
    73     /* This should only be modified on the Java UI thread. */
    74     private final ArrayList<TouchEventInterceptor> mTouchInterceptors;
    75     private final Overscroll mOverscroll;
    77     /* Flags used to determine when to show the painted surface. */
    78     public static final int PAINT_START = 0;
    79     public static final int PAINT_BEFORE_FIRST = 1;
    80     public static final int PAINT_AFTER_FIRST = 2;
    82     public boolean shouldUseTextureView() {
    83         // Disable TextureView support for now as it causes panning/zooming
    84         // performance regressions (see bug 792259). Uncomment the code below
    85         // once this bug is fixed.
    86         return false;
    88         /*
    89         // we can only use TextureView on ICS or higher
    90         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    91             Log.i(LOGTAG, "Not using TextureView: not on ICS+");
    92             return false;
    93         }
    95         try {
    96             // and then we can only use it if we have a hardware accelerated window
    97             Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
    98             return (Boolean) m.invoke(this);
    99         } catch (Exception e) {
   100             Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
   101             return false;
   102         } */
   103     }
   105     public LayerView(Context context, AttributeSet attrs) {
   106         super(context, attrs);
   108         mGLController = GLController.getInstance(this);
   109         mPaintState = PAINT_START;
   110         mBackgroundColor = Color.WHITE;
   112         mTouchInterceptors = new ArrayList<TouchEventInterceptor>();
   113         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
   114             mOverscroll = new OverscrollEdgeEffect(this);
   115         } else {
   116             mOverscroll = null;
   117         }
   118         Tabs.registerOnTabsChangedListener(this);
   119     }
   121     public LayerView(Context context) {
   122         this(context, null);
   123     }
   125     public void initializeView(EventDispatcher eventDispatcher) {
   126         mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
   127         if (mOverscroll != null) {
   128             mLayerClient.setOverscrollHandler(mOverscroll);
   129         }
   131         mPanZoomController = mLayerClient.getPanZoomController();
   132         mMarginsAnimator = mLayerClient.getLayerMarginsAnimator();
   134         mRenderer = new LayerRenderer(this);
   135         mInputConnectionHandler = null;
   137         setFocusable(true);
   138         setFocusableInTouchMode(true);
   140         GeckoAccessibility.setDelegate(this);
   141     }
   143     private Point getEventRadius(MotionEvent event) {
   144         if (Build.VERSION.SDK_INT >= 9) {
   145             return new Point((int)event.getToolMajor()/2,
   146                              (int)event.getToolMinor()/2);
   147         }
   149         float size = event.getSize();
   150         DisplayMetrics displaymetrics = getContext().getResources().getDisplayMetrics();
   151         size = size * Math.min(displaymetrics.heightPixels, displaymetrics.widthPixels);
   152         return new Point((int)size, (int)size);
   153     }
   155     public void geckoConnected() {
   156         // See if we want to force 16-bit colour before doing anything
   157         PrefsHelper.getPref("gfx.android.rgb16.force", new PrefsHelper.PrefHandlerBase() {
   158             @Override public void prefValue(String pref, boolean force16bit) {
   159                 if (force16bit) {
   160                     GeckoAppShell.setScreenDepthOverride(16);
   161                 }
   162             }
   163         });
   165         mLayerClient.notifyGeckoReady();
   166         addTouchInterceptor(new TouchEventInterceptor() {
   167             private PointF mInitialTouchPoint = null;
   169             @Override
   170             public boolean onInterceptTouchEvent(View view, MotionEvent event) {
   171                 return false;
   172             }
   174             @Override
   175             public boolean onTouch(View view, MotionEvent event) {
   176                 if (event == null) {
   177                     return true;
   178                 }
   180                 int action = event.getActionMasked();
   181                 PointF point = new PointF(event.getX(), event.getY());
   182                 if (action == MotionEvent.ACTION_DOWN) {
   183                     mInitialTouchPoint = point;
   184                 }
   186                 if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
   187                     Point p = getEventRadius(event);
   189                     if (PointUtils.subtract(point, mInitialTouchPoint).length() <
   190                         Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
   191                         // Don't send the touchmove event if if the users finger hasn't moved far.
   192                         // Necessary for Google Maps to work correctly. See bug 771099.
   193                         return true;
   194                     } else {
   195                         mInitialTouchPoint = null;
   196                     }
   197                 }
   199                 GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event, false));
   200                 return true;
   201             }
   202         });
   203     }
   205     public void showSurface() {
   206         // Fix this if TextureView support is turned back on above
   207         mSurfaceView.setVisibility(View.VISIBLE);
   208     }
   210     public void hideSurface() {
   211         // Fix this if TextureView support is turned back on above
   212         mSurfaceView.setVisibility(View.INVISIBLE);
   213     }
   215     public void destroy() {
   216         if (mLayerClient != null) {
   217             mLayerClient.destroy();
   218         }
   219         if (mRenderer != null) {
   220             mRenderer.destroy();
   221         }
   222         Tabs.unregisterOnTabsChangedListener(this);
   223     }
   225     public void addTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) {
   226         post(new Runnable() {
   227             @Override
   228             public void run() {
   229                 mTouchInterceptors.add(aTouchInterceptor);
   230             }
   231         });
   232     }
   234     public void removeTouchInterceptor(final TouchEventInterceptor aTouchInterceptor) {
   235         post(new Runnable() {
   236             @Override
   237             public void run() {
   238                 mTouchInterceptors.remove(aTouchInterceptor);
   239             }
   240         });
   241     }
   243     private boolean runTouchInterceptors(MotionEvent event, boolean aOnTouch) {
   244         boolean result = false;
   245         for (TouchEventInterceptor i : mTouchInterceptors) {
   246             if (aOnTouch) {
   247                 result |= i.onTouch(this, event);
   248             } else {
   249                 result |= i.onInterceptTouchEvent(this, event);
   250             }
   251         }
   253         return result;
   254     }
   256     @Override
   257     public void dispatchDraw(final Canvas canvas) {
   258         super.dispatchDraw(canvas);
   260         // We must have a layer client to get valid viewport metrics
   261         if (mLayerClient != null && mOverscroll != null) {
   262             mOverscroll.draw(canvas, getViewportMetrics());
   263         }
   264     }
   266     @Override
   267     public boolean onTouchEvent(MotionEvent event) {
   268         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
   269             requestFocus();
   270         }
   272         if (runTouchInterceptors(event, false)) {
   273             return true;
   274         }
   275         if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
   276             return true;
   277         }
   278         if (runTouchInterceptors(event, true)) {
   279             return true;
   280         }
   281         return false;
   282     }
   284     @Override
   285     public boolean onHoverEvent(MotionEvent event) {
   286         if (runTouchInterceptors(event, true)) {
   287             return true;
   288         }
   289         return false;
   290     }
   292     @Override
   293     public boolean onGenericMotionEvent(MotionEvent event) {
   294         if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
   295             return true;
   296         }
   297         return false;
   298     }
   300     @Override
   301     protected void onAttachedToWindow() {
   302         // This check should not be done before the view is attached to a window
   303         // as hardware acceleration will not be enabled at that point.
   304         // We must create and add the SurfaceView instance before the view tree
   305         // is fully created to avoid flickering (see bug 801477).
   306         if (shouldUseTextureView()) {
   307             mTextureView = new TextureView(getContext());
   308             mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
   310             // The background is set to this color when the LayerView is
   311             // created, and it will be shown immediately at startup. Shortly
   312             // after, the tab's background color will be used before any content
   313             // is shown.
   314             mTextureView.setBackgroundColor(Color.WHITE);
   315             addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
   316         } else {
   317             // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap)
   318             // from a SurfaceView, which is just not possible (the bitmap will be transparent).
   319             setWillNotCacheDrawing(false);
   321             mSurfaceView = new LayerSurfaceView(getContext(), this);
   322             mSurfaceView.setBackgroundColor(Color.WHITE);
   323             addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
   325             SurfaceHolder holder = mSurfaceView.getHolder();
   326             holder.addCallback(new SurfaceListener());
   327         }
   328     }
   330     // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
   331     GeckoLayerClient getLayerClient() { return mLayerClient; }
   332     public Object getLayerClientObject() { return mLayerClient; }
   334     public PanZoomController getPanZoomController() { return mPanZoomController; }
   335     public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; }
   337     public ImmutableViewportMetrics getViewportMetrics() {
   338         return mLayerClient.getViewportMetrics();
   339     }
   341     public void abortPanning() {
   342         if (mPanZoomController != null) {
   343             mPanZoomController.abortPanning();
   344         }
   345     }
   347     public PointF convertViewPointToLayerPoint(PointF viewPoint) {
   348         return mLayerClient.convertViewPointToLayerPoint(viewPoint);
   349     }
   351     int getBackgroundColor() {
   352         return mBackgroundColor;
   353     }
   355     @Override
   356     public void setBackgroundColor(int newColor) {
   357         mBackgroundColor = newColor;
   358         requestRender();
   359     }
   361     public void setZoomConstraints(ZoomConstraints constraints) {
   362         mLayerClient.setZoomConstraints(constraints);
   363     }
   365     public void setIsRTL(boolean aIsRTL) {
   366         mLayerClient.setIsRTL(aIsRTL);
   367     }
   369     public void setInputConnectionHandler(InputConnectionHandler inputConnectionHandler) {
   370         mInputConnectionHandler = inputConnectionHandler;
   371         mLayerClient.forceRedraw(null);
   372     }
   374     @Override
   375     public Handler getHandler() {
   376         if (mInputConnectionHandler != null)
   377             return mInputConnectionHandler.getHandler(super.getHandler());
   378         return super.getHandler();
   379     }
   381     @Override
   382     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
   383         if (mInputConnectionHandler != null)
   384             return mInputConnectionHandler.onCreateInputConnection(outAttrs);
   385         return null;
   386     }
   388     @Override
   389     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
   390         if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyPreIme(keyCode, event)) {
   391             return true;
   392         }
   393         return false;
   394     }
   396     @Override
   397     public boolean onKeyDown(int keyCode, KeyEvent event) {
   398         if (mPanZoomController != null && mPanZoomController.onKeyEvent(event)) {
   399             return true;
   400         }
   401         if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyDown(keyCode, event)) {
   402             return true;
   403         }
   404         return false;
   405     }
   407     @Override
   408     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
   409         if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyLongPress(keyCode, event)) {
   410             return true;
   411         }
   412         return false;
   413     }
   415     @Override
   416     public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
   417         if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event)) {
   418             return true;
   419         }
   420         return false;
   421     }
   423     @Override
   424     public boolean onKeyUp(int keyCode, KeyEvent event) {
   425         if (mInputConnectionHandler != null && mInputConnectionHandler.onKeyUp(keyCode, event)) {
   426             return true;
   427         }
   428         return false;
   429     }
   431     public boolean isIMEEnabled() {
   432         if (mInputConnectionHandler != null) {
   433             return mInputConnectionHandler.isIMEEnabled();
   434         }
   435         return false;
   436     }
   438     public void requestRender() {
   439         if (mListener != null) {
   440             mListener.renderRequested();
   441         }
   442     }
   444     public void addLayer(Layer layer) {
   445         mRenderer.addLayer(layer);
   446     }
   448     public void removeLayer(Layer layer) {
   449         mRenderer.removeLayer(layer);
   450     }
   452     public void postRenderTask(RenderTask task) {
   453         mRenderer.postRenderTask(task);
   454     }
   456     public void removeRenderTask(RenderTask task) {
   457         mRenderer.removeRenderTask(task);
   458     }
   460     public int getMaxTextureSize() {
   461         return mRenderer.getMaxTextureSize();
   462     }
   464     /** Used by robocop for testing purposes. Not for production use! */
   465     @RobocopTarget
   466     public IntBuffer getPixels() {
   467         return mRenderer.getPixels();
   468     }
   470     /* paintState must be a PAINT_xxx constant. */
   471     public void setPaintState(int paintState) {
   472         mPaintState = paintState;
   473     }
   475     public int getPaintState() {
   476         return mPaintState;
   477     }
   479     public LayerRenderer getRenderer() {
   480         return mRenderer;
   481     }
   483     public void setListener(Listener listener) {
   484         mListener = listener;
   485     }
   487     Listener getListener() {
   488         return mListener;
   489     }
   491     public GLController getGLController() {
   492         return mGLController;
   493     }
   495     private Bitmap getDrawable(String name) {
   496         BitmapFactory.Options options = new BitmapFactory.Options();
   497         options.inScaled = false;
   498         Context context = getContext();
   499         int resId = context.getResources().getIdentifier(name, "drawable", context.getPackageName());
   500         return BitmapUtils.decodeResource(context, resId, options);
   501     }
   503     Bitmap getScrollbarImage() {
   504         return getDrawable("scrollbar");
   505     }
   507     /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
   508      * phases. First, the LayerView changes size, then, often some frames later,
   509      * the SurfaceView changes size. Because of this, we need to split the
   510      * resize into two phases to avoid jittering.
   511      *
   512      * The first phase is the LayerView size change. mListener is notified so
   513      * that a synchronous draw can be performed (otherwise a blank frame will
   514      * appear).
   515      *
   516      * The second phase is the SurfaceView size change. At this point, the
   517      * backing GL surface is resized and another synchronous draw is performed.
   518      * Gecko is also sent the new window size, and this will likely cause an
   519      * extra draw a few frames later, after it's re-rendered and caught up.
   520      *
   521      * In the case that there is no valid GL surface (for example, when
   522      * resuming, or when coming back from the awesomescreen), or we're using a
   523      * TextureView instead of a SurfaceView, the first phase is skipped.
   524      */
   525     private void onSizeChanged(int width, int height) {
   526         if (!mGLController.isCompositorCreated()) {
   527             return;
   528         }
   530         surfaceChanged(width, height);
   532         if (mSurfaceView == null) {
   533             return;
   534         }
   536         if (mListener != null) {
   537             mListener.sizeChanged(width, height);
   538         }
   540         if (mOverscroll != null) {
   541             mOverscroll.setSize(width, height);
   542         }
   543     }
   545     private void surfaceChanged(int width, int height) {
   546         mGLController.serverSurfaceChanged(width, height);
   548         if (mListener != null) {
   549             mListener.surfaceChanged(width, height);
   550         }
   552         if (mOverscroll != null) {
   553             mOverscroll.setSize(width, height);
   554         }
   555     }
   557     private void onDestroyed() {
   558         mGLController.serverSurfaceDestroyed();
   559     }
   561     public Object getNativeWindow() {
   562         if (mSurfaceView != null)
   563             return mSurfaceView.getHolder();
   565         return mTextureView.getSurfaceTexture();
   566     }
   568     @WrapElementForJNI(allowMultithread = true, stubName = "RegisterCompositorWrapper")
   569     public static GLController registerCxxCompositor() {
   570         try {
   571             LayerView layerView = GeckoAppShell.getLayerView();
   572             GLController controller = layerView.getGLController();
   573             controller.compositorCreated();
   574             return controller;
   575         } catch (Exception e) {
   576             Log.e(LOGTAG, "Error registering compositor!", e);
   577             return null;
   578         }
   579     }
   581     public interface Listener {
   582         void renderRequested();
   583         void sizeChanged(int width, int height);
   584         void surfaceChanged(int width, int height);
   585     }
   587     private class SurfaceListener implements SurfaceHolder.Callback {
   588         @Override
   589         public void surfaceChanged(SurfaceHolder holder, int format, int width,
   590                                                 int height) {
   591             onSizeChanged(width, height);
   592         }
   594         @Override
   595         public void surfaceCreated(SurfaceHolder holder) {
   596         }
   598         @Override
   599         public void surfaceDestroyed(SurfaceHolder holder) {
   600             onDestroyed();
   601         }
   602     }
   604     /* A subclass of SurfaceView to listen to layout changes, as
   605      * View.OnLayoutChangeListener requires API level 11.
   606      */
   607     private class LayerSurfaceView extends SurfaceView {
   608         LayerView mParent;
   610         public LayerSurfaceView(Context aContext, LayerView aParent) {
   611             super(aContext);
   612             mParent = aParent;
   613         }
   615         @Override
   616         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   617             if (changed) {
   618                 mParent.surfaceChanged(right - left, bottom - top);
   619             }
   620         }
   621     }
   623     private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
   624         @Override
   625         public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
   626             // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
   627             // but that is not the case here.
   628             onSizeChanged(width, height);
   629         }
   631         @Override
   632         public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
   633             onDestroyed();
   634             return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
   635         }
   637         @Override
   638         public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
   639             onSizeChanged(width, height);
   640         }
   642         @Override
   643         public void onSurfaceTextureUpdated(SurfaceTexture surface) {
   645         }
   646     }
   648     @RobocopTarget
   649     public void addDrawListener(DrawListener listener) {
   650         mLayerClient.addDrawListener(listener);
   651     }
   653     @RobocopTarget
   654     public void removeDrawListener(DrawListener listener) {
   655         mLayerClient.removeDrawListener(listener);
   656     }
   658     @RobocopTarget
   659     public static interface DrawListener {
   660         public void drawFinished();
   661     }
   663     @Override
   664     public void setOverScrollMode(int overscrollMode) {
   665         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
   666             super.setOverScrollMode(overscrollMode);
   667         }
   668         if (mPanZoomController != null) {
   669             mPanZoomController.setOverScrollMode(overscrollMode);
   670         }
   671     }
   673     @Override
   674     public int getOverScrollMode() {
   675         if (mPanZoomController != null) {
   676             return mPanZoomController.getOverScrollMode();
   677         }
   679         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
   680             return super.getOverScrollMode();
   681         }
   682         return View.OVER_SCROLL_ALWAYS;
   683     }
   685     @Override
   686     public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
   687         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
   688         GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
   689     }
   691     public void setFullScreen(boolean fullScreen) {
   692         mFullScreen = fullScreen;
   693     }
   695     public boolean isFullScreen() {
   696         return mFullScreen;
   697     }
   699     @Override
   700     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
   701         if (msg == Tabs.TabEvents.VIEWPORT_CHANGE && Tabs.getInstance().isSelectedTab(tab) && mLayerClient != null) {
   702             setZoomConstraints(tab.getZoomConstraints());
   703             setIsRTL(tab.getIsRTL());
   704         }
   705     }
   707     // Public hooks for listening to metrics changing
   709     public interface OnMetricsChangedListener {
   710         public void onMetricsChanged(ImmutableViewportMetrics viewport);
   711         public void onPanZoomStopped();
   712     }
   714     public void setOnMetricsChangedListener(OnMetricsChangedListener listener) {
   715         mLayerClient.setOnMetricsChangedListener(listener);
   716     }
   717 }

mercurial