Index: chrome/android/java/src/org/chromium/chrome/browser/fullscreen/ChromeFullscreenManager.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/ChromeFullscreenManager.java b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/ChromeFullscreenManager.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..974ff2037ff4793325de81dfde61a45289f14e5c |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/ChromeFullscreenManager.java |
@@ -0,0 +1,721 @@ |
+// Copyright 2014 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.chrome.browser.fullscreen; |
+ |
+import android.animation.Animator; |
+import android.animation.AnimatorListenerAdapter; |
+import android.animation.ObjectAnimator; |
+import android.app.Activity; |
+import android.content.res.Resources; |
+import android.os.Build; |
+import android.os.Handler; |
+import android.os.Message; |
+import android.os.SystemClock; |
+import android.util.Property; |
+import android.view.Gravity; |
+import android.view.MotionEvent; |
+import android.view.View; |
+import android.view.ViewGroup; |
+import android.view.ViewGroup.LayoutParams; |
+import android.view.Window; |
+import android.widget.FrameLayout; |
+ |
+import org.chromium.base.ActivityState; |
+import org.chromium.base.ApiCompatibilityUtils; |
+import org.chromium.base.ApplicationStatus; |
+import org.chromium.base.ApplicationStatus.ActivityStateListener; |
+import org.chromium.base.BaseChromiumApplication; |
+import org.chromium.base.BaseChromiumApplication.WindowFocusChangedListener; |
+import org.chromium.base.TraceEvent; |
+import org.chromium.base.VisibleForTesting; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.chrome.browser.fullscreen.FullscreenHtmlApiHandler.FullscreenHtmlApiDelegate; |
+import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
+import org.chromium.content.browser.ContentViewCore; |
+ |
+import java.util.ArrayList; |
+import java.util.HashSet; |
+ |
+/** |
+ * A class that manages control and content views to create the fullscreen mode. |
+ */ |
+public class ChromeFullscreenManager |
+ extends FullscreenManager implements ActivityStateListener, WindowFocusChangedListener { |
+ // Minimum showtime of the toolbar (in ms). |
+ private static final long MINIMUM_SHOW_DURATION_MS = 3000; |
+ |
+ // Maximum length of the slide in/out animation of the toolbar (in ms). |
+ private static final long MAX_ANIMATION_DURATION_MS = 500; |
+ |
+ private static final int MSG_ID_CONTROLS_REQUEST_LAYOUT = 1; |
+ private static final int MSG_ID_HIDE_CONTROLS = 2; |
+ |
+ private final HashSet<Integer> mPersistentControlTokens = new HashSet<Integer>(); |
+ |
+ private final Activity mActivity; |
+ private final Window mWindow; |
+ private final Handler mHandler; |
+ private final int mControlContainerHeight; |
+ |
+ private View mControlContainer; |
+ |
+ private long mMinShowNotificationMs = MINIMUM_SHOW_DURATION_MS; |
+ private long mMaxAnimationDurationMs = MAX_ANIMATION_DURATION_MS; |
+ |
+ private final boolean mEnabled; |
+ |
+ private float mBrowserControlOffset = Float.NaN; |
+ private float mRendererControlOffset = Float.NaN; |
+ private float mRendererContentOffset; |
+ private float mPreviousContentOffset; |
+ private float mControlOffset; |
+ private float mPreviousControlOffset; |
+ private boolean mIsEnteringPersistentModeState; |
+ |
+ private boolean mInGesture; |
+ private boolean mContentViewScrolling; |
+ |
+ private int mPersistentControlsCurrentToken; |
+ private long mCurrentShowTime; |
+ |
+ private ObjectAnimator mControlAnimation; |
+ private boolean mCurrentAnimationIsShowing; |
+ |
+ private boolean mDisableBrowserOverride; |
+ |
+ private boolean mTopControlsPermanentlyHidden; |
+ private boolean mTopControlsAndroidViewHidden; |
+ |
+ private final ArrayList<FullscreenListener> mListeners = new ArrayList<FullscreenListener>(); |
+ |
+ /** |
+ * A listener that gets notified of changes to the fullscreen state. |
+ */ |
+ public interface FullscreenListener { |
+ /** |
+ * Called whenever the content's offset changes. |
+ * @param offset The new offset of the content from the top of the screen. |
+ */ |
+ public void onContentOffsetChanged(float offset); |
+ |
+ /** |
+ * Called whenever the content's visible offset changes. |
+ * @param offset The new offset of the visible content from the top of the screen. |
+ */ |
+ public void onVisibleContentOffsetChanged(float offset); |
+ |
+ /** |
+ * Called when a ContentVideoView is created/destroyed. |
+ * @param enabled Whether to enter or leave overlay video mode. |
+ */ |
+ public void onToggleOverlayVideoMode(boolean enabled); |
+ } |
+ |
+ private class ControlsOffsetProperty extends Property<ChromeFullscreenManager, Float> { |
+ public ControlsOffsetProperty() { |
+ super(Float.class, "controlsOffset"); |
+ } |
+ |
+ @Override |
+ public Float get(ChromeFullscreenManager object) { |
+ return getControlOffset(); |
+ } |
+ |
+ @Override |
+ public void set(ChromeFullscreenManager manager, Float offset) { |
+ if (mDisableBrowserOverride) return; |
+ float browserOffset = offset.floatValue(); |
+ if (Float.compare(mBrowserControlOffset, browserOffset) == 0) return; |
+ mBrowserControlOffset = browserOffset; |
+ manager.updateControlOffset(); |
+ manager.updateVisuals(); |
+ } |
+ } |
+ |
+ private final Runnable mUpdateVisibilityRunnable = new Runnable() { |
+ @Override |
+ public void run() { |
+ int visibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE; |
+ if (mControlContainer.getVisibility() == visibility) return; |
+ // requestLayout is required to trigger a new gatherTransparentRegion(), which |
+ // only occurs together with a layout and let's SurfaceFlinger trim overlays. |
+ // This may be almost equivalent to using View.GONE, but we still use View.INVISIBLE |
+ // since drawing caches etc. won't be destroyed, and the layout may be less expensive. |
+ mControlContainer.setVisibility(visibility); |
+ mControlContainer.requestLayout(); |
+ } |
+ }; |
+ |
+ /** |
+ * Creates an instance of the fullscreen mode manager. |
+ * @param activity The activity that supports fullscreen. |
+ * @param controlContainer Container holding the controls (Toolbar). |
+ * @param enabled Whether fullscreen is globally enabled. |
+ * @param modelSelector The model selector providing access to the current tab. |
+ */ |
+ public ChromeFullscreenManager(Activity activity, View controlContainer, boolean enabled, |
+ boolean persistentFullscreenSupported, TabModelSelector modelSelector, |
+ int resControlContainerHeight) { |
+ super(activity.getWindow(), modelSelector, enabled, persistentFullscreenSupported); |
+ |
+ mActivity = activity; |
+ ApplicationStatus.registerStateListenerForActivity(this, activity); |
+ ((BaseChromiumApplication) activity.getApplication()) |
+ .registerWindowFocusChangedListener(this); |
+ |
+ mWindow = activity.getWindow(); |
+ mHandler = new Handler() { |
+ @Override |
+ public void handleMessage(Message msg) { |
+ if (msg == null) return; |
+ switch (msg.what) { |
+ case MSG_ID_CONTROLS_REQUEST_LAYOUT: |
+ mControlContainer.requestLayout(); |
+ break; |
+ case MSG_ID_HIDE_CONTROLS: |
+ update(false); |
+ break; |
+ default: |
+ assert false : "Unexpected message for ID: " + msg.what; |
+ break; |
+ } |
+ } |
+ }; |
+ setControlContainer(controlContainer); |
+ Resources resources = mWindow.getContext().getResources(); |
+ mControlContainerHeight = resources.getDimensionPixelSize(resControlContainerHeight); |
+ mRendererContentOffset = mControlContainerHeight; |
+ mEnabled = enabled; |
+ updateControlOffset(); |
+ } |
+ |
+ /** |
+ * @return Whether or not fullscreen is enabled. |
+ */ |
+ public boolean isEnabled() { |
+ return mEnabled; |
+ } |
+ |
+ /** |
+ * Set the control container that is being hidden and shown when manipulating the fullscreen |
+ * state. |
+ * @param controlContainer The container at the top of the screen that contains the controls. |
+ */ |
+ public void setControlContainer(View controlContainer) { |
+ assert controlContainer != null; |
+ mControlContainer = controlContainer; |
+ } |
+ |
+ @Override |
+ public void onActivityStateChange(Activity activity, int newState) { |
+ if (newState == ActivityState.STOPPED) { |
+ // Exit fullscreen in onStop to ensure the system UI flags are set correctly when |
+ // showing again (on JB MR2+ builds, the omnibox would be covered by the |
+ // notification bar when this was done in onStart()). |
+ setPersistentFullscreenMode(false); |
+ } else if (newState == ActivityState.STARTED) { |
+ showControlsTransient(); |
+ } else if (newState == ActivityState.DESTROYED) { |
+ ApplicationStatus.unregisterActivityStateListener(this); |
+ ((BaseChromiumApplication) mWindow.getContext().getApplicationContext()) |
+ .unregisterWindowFocusChangedListener(this); |
+ } |
+ } |
+ |
+ @Override |
+ public void onWindowFocusChanged(Activity activity, boolean hasFocus) { |
+ if (mActivity == activity) onWindowFocusChanged(hasFocus); |
+ } |
+ |
+ @Override |
+ protected FullscreenHtmlApiDelegate createApiDelegate() { |
+ return new FullscreenHtmlApiDelegate() { |
+ @Override |
+ public View getNotificationAnchorView() { |
+ return mControlContainer; |
+ } |
+ |
+ @Override |
+ public int getNotificationOffsetY() { |
+ return (int) getControlOffset(); |
+ } |
+ |
+ @Override |
+ public void onEnterFullscreen() { |
+ mIsEnteringPersistentModeState = true; |
+ } |
+ |
+ @Override |
+ public boolean cancelPendingEnterFullscreen() { |
+ boolean wasPending = mIsEnteringPersistentModeState; |
+ mIsEnteringPersistentModeState = false; |
+ return wasPending; |
+ } |
+ |
+ @Override |
+ public void onFullscreenExited(ContentViewCore contentViewCore) { |
+ contentViewCore.getWebContents().updateTopControlsState(false, true, true); |
+ contentViewCore.getWebContents().updateTopControlsState(true, true, false); |
+ } |
+ |
+ @Override |
+ public boolean shouldShowNotificationBubble() { |
+ return !isOverlayVideoMode(); |
+ } |
+ }; |
+ } |
+ |
+ /** |
+ * Disables the ability for the browser to override the renderer provided top controls |
+ * position for testing. |
+ */ |
+ @VisibleForTesting |
+ public void disableBrowserOverrideForTest() { |
+ mDisableBrowserOverride = true; |
+ mPersistentControlTokens.clear(); |
+ mHandler.removeMessages(MSG_ID_HIDE_CONTROLS); |
+ if (mControlAnimation != null) { |
+ mControlAnimation.cancel(); |
+ mControlAnimation = null; |
+ } |
+ mBrowserControlOffset = Float.NaN; |
+ updateVisuals(); |
+ } |
+ |
+ /** |
+ * Allows tests to override the animation durations for faster tests. |
+ * @param minShowDuration The minimum time the controls must be shown. |
+ * @param maxAnimationDuration The maximum animation time to show/hide the controls. |
+ */ |
+ @VisibleForTesting |
+ public void setAnimationDurationsForTest(long minShowDuration, long maxAnimationDuration) { |
+ mMinShowNotificationMs = minShowDuration; |
+ mMaxAnimationDurationMs = maxAnimationDuration; |
+ } |
+ |
+ @Override |
+ public void showControlsTransient() { |
+ if (mPersistentControlTokens.isEmpty()) update(true); |
+ } |
+ |
+ @Override |
+ public int showControlsPersistent() { |
+ int token = mPersistentControlsCurrentToken++; |
+ mPersistentControlTokens.add(token); |
+ if (mPersistentControlTokens.size() == 1) update(true); |
+ return token; |
+ } |
+ |
+ @Override |
+ public int showControlsPersistentAndClearOldToken(int oldToken) { |
+ if (oldToken != INVALID_TOKEN) mPersistentControlTokens.remove(oldToken); |
+ return showControlsPersistent(); |
+ } |
+ |
+ @Override |
+ public void hideControlsPersistent(int token) { |
+ if (mPersistentControlTokens.remove(token) && mPersistentControlTokens.isEmpty()) { |
+ update(false); |
+ } |
+ } |
+ |
+ /** |
+ * @param remove Whether or not to forcefully remove the toolbar. |
+ */ |
+ public void setTopControlsPermamentlyHidden(boolean remove) { |
+ if (remove == mTopControlsPermanentlyHidden) return; |
+ mTopControlsPermanentlyHidden = remove; |
+ updateVisuals(); |
+ } |
+ |
+ /** |
+ * @return Whether or not the toolbar is forcefully being removed. |
+ */ |
+ public boolean areTopControlsPermanentlyHidden() { |
+ return mTopControlsPermanentlyHidden; |
+ } |
+ |
+ /** |
+ * @return Whether the top controls should be drawn as a texture. |
+ */ |
+ public boolean drawControlsAsTexture() { |
+ return getControlOffset() > -mControlContainerHeight; |
+ } |
+ |
+ /** |
+ * @return The height of the top controls in pixels. |
+ */ |
+ public int getTopControlsHeight() { |
+ return mEnabled ? mControlContainerHeight : 0; |
+ } |
+ |
+ @Override |
+ public float getContentOffset() { |
+ if (!mEnabled || mTopControlsPermanentlyHidden) return 0; |
+ return rendererContentOffset(); |
+ } |
+ |
+ /** |
+ * @return The offset of the controls from the top of the screen. |
+ */ |
+ public float getControlOffset() { |
+ if (!mEnabled) return 0; |
+ if (mTopControlsPermanentlyHidden) return -getTopControlsHeight(); |
+ return mControlOffset; |
+ } |
+ |
+ @SuppressWarnings("SelfEquality") |
+ private void updateControlOffset() { |
+ float offset = 0; |
+ // Inline Float.isNan with "x != x": |
+ final boolean isNaNBrowserControlOffset = mBrowserControlOffset != mBrowserControlOffset; |
+ final float rendererControlOffset = rendererControlOffset(); |
+ final boolean isNaNRendererControlOffset = rendererControlOffset != rendererControlOffset; |
+ if (!isNaNBrowserControlOffset || !isNaNRendererControlOffset) { |
+ offset = Math.max( |
+ isNaNBrowserControlOffset ? -mControlContainerHeight : mBrowserControlOffset, |
+ isNaNRendererControlOffset ? -mControlContainerHeight : rendererControlOffset); |
+ } |
+ mControlOffset = offset; |
+ } |
+ |
+ @Override |
+ public void setOverlayVideoMode(boolean enabled) { |
+ super.setOverlayVideoMode(enabled); |
+ |
+ for (int i = 0; i < mListeners.size(); i++) { |
+ mListeners.get(i).onToggleOverlayVideoMode(enabled); |
+ } |
+ } |
+ |
+ /** |
+ * @return Whether the browser has a control offset override. |
+ */ |
+ @VisibleForTesting |
+ public boolean hasBrowserControlOffsetOverride() { |
+ return !Float.isNaN(mBrowserControlOffset) || mControlAnimation != null |
+ || !mPersistentControlTokens.isEmpty(); |
+ } |
+ |
+ /** |
+ * Returns how tall the opaque portion of the control container is. |
+ */ |
+ public float controlContainerHeight() { |
+ return mControlContainerHeight; |
+ } |
+ |
+ private float rendererContentOffset() { |
+ if (!mEnabled) return mControlContainerHeight; |
+ return mRendererContentOffset; |
+ } |
+ |
+ private float rendererControlOffset() { |
+ if (!mEnabled) return 0; |
+ return mRendererControlOffset; |
+ } |
+ |
+ /** |
+ * @return The visible offset of the content from the top of the screen. |
+ */ |
+ public float getVisibleContentOffset() { |
+ if (!mEnabled) return 0; |
+ return mControlContainerHeight + getControlOffset(); |
+ } |
+ |
+ /** |
+ * @param listener The {@link FullscreenListener} to be notified of fullscreen changes. |
+ */ |
+ public void addListener(FullscreenListener listener) { |
+ if (!mListeners.contains(listener)) mListeners.add(listener); |
+ } |
+ |
+ /** |
+ * @param listener The {@link FullscreenListener} to no longer be notified of fullscreen |
+ * changes. |
+ */ |
+ public void removeListener(FullscreenListener listener) { |
+ mListeners.remove(listener); |
+ } |
+ |
+ /** |
+ * Updates the content view's viewport size to have it render the content correctly. |
+ * |
+ * @param viewCore The ContentViewCore to update. |
+ */ |
+ public void updateContentViewViewportSize(ContentViewCore viewCore) { |
+ if (!mEnabled || viewCore == null) return; |
+ if (mInGesture || mContentViewScrolling) return; |
+ |
+ // Update content viewport size only when the top controls are not animating. |
+ int contentOffset = (int) rendererContentOffset(); |
+ if (contentOffset != 0 && contentOffset != mControlContainerHeight) return; |
+ viewCore.setTopControlsHeight(mControlContainerHeight, contentOffset > 0); |
+ } |
+ |
+ @Override |
+ public void updateContentViewChildrenState() { |
+ ContentViewCore contentViewCore = getActiveContentViewCore(); |
+ if (contentViewCore == null || !mEnabled) return; |
+ ViewGroup view = contentViewCore.getContainerView(); |
+ |
+ float topViewsTranslation = (getControlOffset() + mControlContainerHeight); |
+ applyTranslationToTopChildViews(view, topViewsTranslation); |
+ applyMarginToFullChildViews(view, topViewsTranslation); |
+ updateContentViewViewportSize(contentViewCore); |
+ } |
+ |
+ /** |
+ * Utility routine for ensuring visibility updates are synchronized with |
+ * animation, preventing message loop stalls due to untimely invalidation. |
+ */ |
+ private void scheduleVisibilityUpdate() { |
+ final int desiredVisibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE; |
+ if (mControlContainer.getVisibility() == desiredVisibility) return; |
+ mControlContainer.removeCallbacks(mUpdateVisibilityRunnable); |
+ ApiCompatibilityUtils.postOnAnimation(mControlContainer, mUpdateVisibilityRunnable); |
+ } |
+ |
+ private void updateVisuals() { |
+ if (!mEnabled) return; |
+ |
+ TraceEvent.begin("FullscreenManager:updateVisuals"); |
+ |
+ float offset = getControlOffset(); |
+ if (Float.compare(mPreviousControlOffset, offset) != 0) { |
+ mPreviousControlOffset = offset; |
+ getHtmlApiHandler().updateBubblePosition(); |
+ |
+ scheduleVisibilityUpdate(); |
+ if (shouldShowAndroidControls()) mControlContainer.setTranslationY(getControlOffset()); |
+ |
+ // In ICS, the toolbar can appear clipped when compositor content is not being drawn |
+ // beneath it (at the top of the page, during side swipe). Requesting a layout clears |
+ // up the issue (see crbug.com/172631). |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { |
+ if (!mHandler.hasMessages(MSG_ID_CONTROLS_REQUEST_LAYOUT)) { |
+ mHandler.sendEmptyMessage(MSG_ID_CONTROLS_REQUEST_LAYOUT); |
+ } |
+ } |
+ for (int i = 0; i < mListeners.size(); i++) { |
+ mListeners.get(i).onVisibleContentOffsetChanged(getVisibleContentOffset()); |
+ } |
+ } |
+ |
+ final ContentViewCore contentViewCore = getActiveContentViewCore(); |
+ if (contentViewCore != null && offset == -mControlContainerHeight |
+ && mIsEnteringPersistentModeState) { |
+ getHtmlApiHandler().enterFullscreen(contentViewCore); |
+ mIsEnteringPersistentModeState = false; |
+ } |
+ |
+ updateContentViewChildrenState(); |
+ |
+ float contentOffset = getContentOffset(); |
+ if (Float.compare(mPreviousContentOffset, contentOffset) != 0) { |
+ for (int i = 0; i < mListeners.size(); i++) { |
+ mListeners.get(i).onContentOffsetChanged(contentOffset); |
+ } |
+ mPreviousContentOffset = contentOffset; |
+ } |
+ |
+ TraceEvent.end("FullscreenManager:updateVisuals"); |
+ } |
+ |
+ /** |
+ * @param hide Whether or not to force the top controls Android view to hide. If this is |
+ * {@code false} the top controls Android view will show/hide based on position, if |
+ * it is {@code true} the top controls Android view will always be hidden. |
+ */ |
+ public void setHideTopControlsAndroidView(boolean hide) { |
+ if (mTopControlsAndroidViewHidden == hide) return; |
+ mTopControlsAndroidViewHidden = hide; |
+ scheduleVisibilityUpdate(); |
+ } |
+ |
+ private boolean shouldShowAndroidControls() { |
+ if (mTopControlsAndroidViewHidden) return false; |
+ |
+ boolean showControls = getControlOffset() == 0; |
+ ContentViewCore contentViewCore = getActiveContentViewCore(); |
+ if (contentViewCore == null) return showControls; |
+ ViewGroup contentView = contentViewCore.getContainerView(); |
+ |
+ for (int i = 0; i < contentView.getChildCount(); i++) { |
+ View child = contentView.getChildAt(i); |
+ if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) continue; |
+ |
+ FrameLayout.LayoutParams layoutParams = |
+ (FrameLayout.LayoutParams) child.getLayoutParams(); |
+ if (Gravity.TOP == (layoutParams.gravity & Gravity.FILL_VERTICAL)) { |
+ showControls = true; |
+ break; |
+ } |
+ } |
+ |
+ showControls |= !mPersistentControlTokens.isEmpty(); |
+ |
+ return showControls; |
+ } |
+ |
+ private void applyMarginToFullChildViews(ViewGroup contentView, float margin) { |
+ for (int i = 0; i < contentView.getChildCount(); i++) { |
+ View child = contentView.getChildAt(i); |
+ if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) continue; |
+ FrameLayout.LayoutParams layoutParams = |
+ (FrameLayout.LayoutParams) child.getLayoutParams(); |
+ |
+ if (layoutParams.height == LayoutParams.MATCH_PARENT |
+ && layoutParams.topMargin != (int) margin) { |
+ layoutParams.topMargin = (int) margin; |
+ child.requestLayout(); |
+ TraceEvent.instant("FullscreenManager:child.requestLayout()"); |
+ } |
+ } |
+ } |
+ |
+ private void applyTranslationToTopChildViews(ViewGroup contentView, float translation) { |
+ for (int i = 0; i < contentView.getChildCount(); i++) { |
+ View child = contentView.getChildAt(i); |
+ if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) continue; |
+ |
+ FrameLayout.LayoutParams layoutParams = |
+ (FrameLayout.LayoutParams) child.getLayoutParams(); |
+ if (Gravity.TOP == (layoutParams.gravity & Gravity.FILL_VERTICAL)) { |
+ child.setTranslationY(translation); |
+ TraceEvent.instant("FullscreenManager:child.setTranslationY()"); |
+ } |
+ } |
+ } |
+ |
+ private ContentViewCore getActiveContentViewCore() { |
+ Tab tab = getTabModelSelector().getCurrentTab(); |
+ return tab != null ? tab.getContentViewCore() : null; |
+ } |
+ |
+ @Override |
+ public void setPositionsForTabToNonFullscreen() { |
+ setPositionsForTab(0, mControlContainerHeight); |
+ } |
+ |
+ @Override |
+ public void setPositionsForTab(float controlsOffset, float contentOffset) { |
+ if (!mEnabled) return; |
+ float rendererControlOffset = |
+ Math.round(Math.max(controlsOffset, -mControlContainerHeight)); |
+ float rendererContentOffset = Math.min( |
+ Math.round(contentOffset), rendererControlOffset + mControlContainerHeight); |
+ |
+ if (Float.compare(rendererControlOffset, mRendererControlOffset) == 0 |
+ && Float.compare(rendererContentOffset, mRendererContentOffset) == 0) { |
+ return; |
+ } |
+ |
+ mRendererControlOffset = rendererControlOffset; |
+ mRendererContentOffset = rendererContentOffset; |
+ updateControlOffset(); |
+ |
+ if (mControlAnimation == null) updateVisuals(); |
+ } |
+ |
+ /** |
+ * @param e The dispatched motion event |
+ * @return Whether or not this motion event is in the top control container area and should be |
+ * consumed. |
+ */ |
+ public boolean onInterceptMotionEvent(MotionEvent e) { |
+ return mEnabled && e.getY() < getControlOffset() + mControlContainerHeight |
+ && !mTopControlsAndroidViewHidden; |
+ } |
+ |
+ /** |
+ * Notifies the fullscreen manager that a motion event has occurred. |
+ * @param e The dispatched motion event. |
+ */ |
+ public void onMotionEvent(MotionEvent e) { |
+ if (!mEnabled) return; |
+ int eventAction = e.getActionMasked(); |
+ if (eventAction == MotionEvent.ACTION_DOWN |
+ || eventAction == MotionEvent.ACTION_POINTER_DOWN) { |
+ mInGesture = true; |
+ getHtmlApiHandler().hideNotificationBubble(); |
+ } else if (eventAction == MotionEvent.ACTION_CANCEL |
+ || eventAction == MotionEvent.ACTION_UP) { |
+ mInGesture = false; |
+ updateVisuals(); |
+ } |
+ } |
+ |
+ private void update(boolean show) { |
+ // On forced show/hide, reset the flags that may suppress ContentView resize. |
+ // As this method is also called when tab is switched, this also cleanup the scrolling |
+ // flag set based on the previous ContentView's scrolling state. |
+ mInGesture = false; |
+ mContentViewScrolling = false; |
+ |
+ if (!mEnabled) return; |
+ |
+ if (show) mCurrentShowTime = SystemClock.uptimeMillis(); |
+ |
+ boolean postHideMessage = false; |
+ if (!show) { |
+ if (mControlAnimation != null && mCurrentAnimationIsShowing) { |
+ postHideMessage = true; |
+ } else { |
+ long timeDelta = SystemClock.uptimeMillis() - mCurrentShowTime; |
+ animateIfNecessary(false, Math.max(mMinShowNotificationMs - timeDelta, 0)); |
+ } |
+ } else { |
+ animateIfNecessary(true, 0); |
+ if (mPersistentControlTokens.isEmpty()) postHideMessage = true; |
+ } |
+ |
+ mHandler.removeMessages(MSG_ID_HIDE_CONTROLS); |
+ if (postHideMessage) { |
+ long timeDelta = SystemClock.uptimeMillis() - mCurrentShowTime; |
+ mHandler.sendEmptyMessageDelayed( |
+ MSG_ID_HIDE_CONTROLS, Math.max(mMinShowNotificationMs - timeDelta, 0)); |
+ } |
+ } |
+ |
+ private void animateIfNecessary(final boolean show, long startDelay) { |
+ if (mControlAnimation != null) { |
+ if (!mControlAnimation.isRunning() || mCurrentAnimationIsShowing != show) { |
+ mControlAnimation.cancel(); |
+ mControlAnimation = null; |
+ } else { |
+ return; |
+ } |
+ } |
+ |
+ float destination = show ? 0 : -mControlContainerHeight; |
+ long duration = (long) (mMaxAnimationDurationMs |
+ * Math.abs((destination - getControlOffset()) / mControlContainerHeight)); |
+ mControlAnimation = ObjectAnimator.ofFloat(this, new ControlsOffsetProperty(), destination); |
+ mControlAnimation.addListener(new AnimatorListenerAdapter() { |
+ private boolean mCanceled = false; |
+ |
+ @Override |
+ public void onAnimationCancel(Animator anim) { |
+ mCanceled = true; |
+ } |
+ |
+ @Override |
+ public void onAnimationEnd(Animator animation) { |
+ if (!show && !mCanceled) mBrowserControlOffset = Float.NaN; |
+ mControlAnimation = null; |
+ } |
+ }); |
+ mControlAnimation.setStartDelay(startDelay); |
+ mControlAnimation.setDuration(duration); |
+ mControlAnimation.start(); |
+ mCurrentAnimationIsShowing = show; |
+ } |
+ |
+ @Override |
+ public void onContentViewScrollingStateChanged(boolean scrolling) { |
+ mContentViewScrolling = scrolling; |
+ if (!scrolling) updateVisuals(); |
+ } |
+} |