Chromium Code Reviews| 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..c424a9c29dca9c1c9d37371fb955fc72b2d38244 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/ChromeFullscreenManager.java |
| @@ -0,0 +1,723 @@ |
| +// 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. |
| + * @param fullscreenApiNotification A string for fullscreen api notification. |
| + */ |
| + public ChromeFullscreenManager(Activity activity, View controlContainer, boolean enabled, |
| + boolean persistentFullscreenSupported, TabModelSelector modelSelector, |
| + int resControlContainerHeight, String fullscreenApiNotification) { |
|
Ted C
2014/12/17 17:52:00
Actually...one question I had was why we need to p
Jaekyun Seok (inactive)
2014/12/17 22:03:33
Done.
|
| + super(activity.getWindow(), modelSelector, enabled, persistentFullscreenSupported, |
| + fullscreenApiNotification); |
| + |
| + 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(); |
| + } |
| +} |