// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser; import android.content.Context; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ZoomButtonsController; /** * The ZoomManager is responsible for maintaining the ContentView's current zoom * level state. It is also responsible for managing the on-screen zoom controls. */ class ZoomManager { private static final String TAG = "ContentViewZoom"; private ContentViewCore mContentViewCore; private ZoomButtonsController mZoomButtonsController; private class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { // Completely silence scaling events. Used in WebView when zoom support // is turned off. private boolean mPermanentlyIgnoreDetectorEvents = false; // Bypass events through the detector to maintain its state. Used when // renderes already handles the touch event. private boolean mTemporarilyIgnoreDetectorEvents = false; // Whether any pinch zoom event has been sent to native. private boolean mPinchEventSent; boolean getPermanentlyIgnoreDetectorEvents() { return mPermanentlyIgnoreDetectorEvents; } void setPermanentlyIgnoreDetectorEvents(boolean value) { // Note that returning false from onScaleBegin / onScale makes the // gesture detector not to emit further scaling notifications // related to this gesture. Thus, if detector events are enabled in // the middle of the gesture, we don't need to do anything. mPermanentlyIgnoreDetectorEvents = value; } void setTemporarilyIgnoreDetectorEvents(boolean value) { mTemporarilyIgnoreDetectorEvents = value; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { if (ignoreDetectorEvents()) return false; mPinchEventSent = false; mContentViewCore.getContentViewGestureHandler().setIgnoreSingleTap(true); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { if (!mPinchEventSent || !mContentViewCore.isAlive()) return; mContentViewCore.getContentViewGestureHandler().pinchEnd(detector.getEventTime()); mPinchEventSent = false; } @Override public boolean onScale(ScaleGestureDetector detector) { if (ignoreDetectorEvents()) return false; // It is possible that pinchBegin() was never called when we reach here. // This happens when webkit handles the 2nd touch down event. That causes // ContentView to ignore the onScaleBegin() call. And if webkit does not // handle the touch move events afterwards, we will face a situation // that pinchBy() is called without any pinchBegin(). // To solve this problem, we call pinchBegin() here if it is never called. if (!mPinchEventSent) { mContentViewCore.getContentViewGestureHandler().pinchBegin(detector.getEventTime(), (int) detector.getFocusX(), (int) detector.getFocusY()); mPinchEventSent = true; } mContentViewCore.getContentViewGestureHandler().pinchBy( detector.getEventTime(), (int) detector.getFocusX(), (int) detector.getFocusY(), detector.getScaleFactor()); return true; } private boolean ignoreDetectorEvents() { return mPermanentlyIgnoreDetectorEvents || mTemporarilyIgnoreDetectorEvents || !mContentViewCore.isAlive(); } } private ScaleGestureDetector mMultiTouchDetector; private ScaleGestureListener mMultiTouchListener; ZoomManager(final Context context, ContentViewCore contentViewCore) { mContentViewCore = contentViewCore; mMultiTouchListener = new ScaleGestureListener(); mMultiTouchDetector = new ScaleGestureDetector(context, mMultiTouchListener); } void invokeZoomPicker() { ZoomButtonsController zoomControls = getZoomControls(); if (zoomControls != null && !zoomControls.isVisible()) { zoomControls.setVisible(true); } } void dismissZoomPicker() { ZoomButtonsController zoomControls = getZoomControls(); if (zoomControls != null && zoomControls.isVisible()) { zoomControls.setVisible(false); } } boolean isMultiTouchZoomSupported() { return !mMultiTouchListener.getPermanentlyIgnoreDetectorEvents(); } boolean isScaleGestureDetectionInProgress() { return isMultiTouchZoomSupported() && mMultiTouchDetector.isInProgress(); } // Passes the touch event to ScaleGestureDetector so that its internal // state won't go wrong, but instructs the listener to ignore the result // of processing, if any. void passTouchEventThrough(MotionEvent event) { mMultiTouchListener.setTemporarilyIgnoreDetectorEvents(true); try { mMultiTouchDetector.onTouchEvent(event); } catch (Exception e) { Log.e(TAG, "ScaleGestureDetector got into a bad state!", e); assert(false); } } // Passes the touch event to ScaleGestureDetector so that its internal state // won't go wrong. ScaleGestureDetector needs two pointers in a MotionEvent // to recognize a zoom gesture. boolean processTouchEvent(MotionEvent event) { // TODO: Need to deal with multi-touch transition mMultiTouchListener.setTemporarilyIgnoreDetectorEvents(false); try { boolean inGesture = isScaleGestureDetectionInProgress(); boolean retVal = mMultiTouchDetector.onTouchEvent(event); if (event.getActionMasked() == MotionEvent.ACTION_UP && !inGesture) return false; return retVal; } catch (Exception e) { Log.e(TAG, "ScaleGestureDetector got into a bad state!", e); assert(false); } return false; } void updateMultiTouchSupport() { mMultiTouchListener.setPermanentlyIgnoreDetectorEvents( !mContentViewCore.getContentSettings().supportsMultiTouchZoom()); } private ZoomButtonsController getZoomControls() { if (mZoomButtonsController == null && mContentViewCore.getContentSettings().shouldDisplayZoomControls()) { mZoomButtonsController = new ZoomButtonsController( mContentViewCore.getContainerView()); mZoomButtonsController.setOnZoomListener(new ZoomListener()); // ZoomButtonsController positions the buttons at the bottom, but in // the middle. Change their layout parameters so they appear on the // right. View controls = mZoomButtonsController.getZoomControls(); ViewGroup.LayoutParams params = controls.getLayoutParams(); if (params instanceof FrameLayout.LayoutParams) { ((FrameLayout.LayoutParams) params).gravity = Gravity.RIGHT; } } return mZoomButtonsController; } // This method is used in tests. It doesn't modify the state of zoom controls. View getZoomControlsViewForTest() { return mZoomButtonsController != null ? mZoomButtonsController.getZoomControls() : null; } void updateZoomControls() { if (mZoomButtonsController == null) return; boolean canZoomIn = mContentViewCore.canZoomIn(); boolean canZoomOut = mContentViewCore.canZoomOut(); if (!canZoomIn && !canZoomOut) { // Hide the zoom in and out buttons if the page cannot zoom mZoomButtonsController.getZoomControls().setVisibility(View.GONE); } else { // Set each one individually, as a page may be able to zoom in or out mZoomButtonsController.setZoomInEnabled(canZoomIn); mZoomButtonsController.setZoomOutEnabled(canZoomOut); } } private class ZoomListener implements ZoomButtonsController.OnZoomListener { @Override public void onVisibilityChanged(boolean visible) { if (visible) { // Bring back the hidden zoom controls. mZoomButtonsController.getZoomControls().setVisibility(View.VISIBLE); updateZoomControls(); } } @Override public void onZoom(boolean zoomIn) { if (zoomIn) { mContentViewCore.zoomIn(); } else { mContentViewCore.zoomOut(); } // ContentView will call updateZoomControls after its current page scale // is got updated from the native code. } } }