blob: c75911d66c9553639ae11b1a973b45b06c150589 [file] [log] [blame]
// Copyright 2019 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.weblayer_private;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.widget.FrameLayout;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.EventOffsetHandler;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;
/**
* TopControlsContainerView is responsible for holding the top-view from the client. Further, it
* has a ViewResourceAdapter that is kept in sync with the contents of the top-view.
* ViewResourceAdapter is used to keep a bitmap in sync with the contents of the top-view. The
* bitmap is placed in a cc::Layer and the layer is shown while scrolling the top-view.
* ViewResourceAdapter is always kept in sync, as to do otherwise results in a noticeable delay
* between when the scroll starts the content is available.
*
* There are many parts involved in orchestrating top-controls scrolling. The key things to know
* are:
* . TopControlsContainerView (in native code) keeps a cc::Layer that shows a bitmap rendered by
* the top-view. The bitmap is updated anytime the top-view changes. This is done as otherwise
* there is a noticable delay between when the scroll starts and the bitmap is available.
* . When scrolling, the cc::Layer for the WebContents and TopControlsContainerView is moved.
* . The size of the WebContents is only changed after the user releases a touch point. Otherwise
* the scrollbar bounces around.
* . WebContentsDelegate::DoBrowserControlsShrinkRendererSize() only changes when the WebContents
* size change.
* . WebContentsGestureStateTracker is responsible for determining when a scroll/touch is underway.
* . ContentViewRenderView.Delegate is used to adjust the size of the webcontents when the
* top-controls are fully visible (and a scroll is not underway).
*
* The flow of this code is roughly:
* . WebContentsGestureStateTracker generally detects a touch first
* . BrowserControllerImpl is notified and caches state.
* . onTopControlsChanged() is called. This triggers hiding the real view and calling to native code
* to move the cc::Layers.
* . the move continues.
* . when the move completes and both WebContentsGestureStateTracker and TopControlsContainerView
* no longer believe a move/gesture/scroll is underway the size of the WebContents is adjusted
* (if necessary).
*/
@JNINamespace("weblayer")
class TopControlsContainerView extends FrameLayout {
// ID used with ViewResourceAdapter.
private static final int TOP_CONTROLS_ID = 1001;
private static final long SYSTEM_UI_VIEWPORT_UPDATE_DELAY_MS = 500;
private long mNativeTopControlsContainerView;
private ViewResourceAdapter mViewResourceAdapter;
// Last width/height of mView as sent to the native side.
private int mLastWidth;
private int mLastHeight;
// view from the client.
private View mView;
private ContentViewRenderView mContentViewRenderView;
private WebContents mWebContents;
private EventOffsetHandler mEventOffsetHandler;
private int mTopContentOffset;
// Set to true if |mView| is hidden because the user has scrolled or triggered some action such
// that mView is not visible. While |mView| is not visible if this is true, the bitmap from
// |mView| may be partially visible.
private boolean mInTopControlsScroll;
private boolean mIsFullscreen;
// Used to delay processing fullscreen requests.
private Runnable mSystemUiFullscreenResizeRunnable;
private final Listener mListener;
public interface Listener {
/**
* Called when the top-controls are either completely showing, or completely hiding.
*/
public void onTopControlsCompletelyShownOrHidden();
}
// Used to delay updating the image for the layer.
private final Runnable mRefreshResourceIdRunnable = () -> {
if (mView == null) return;
TopControlsContainerViewJni.get().updateTopControlsResource(
mNativeTopControlsContainerView, TopControlsContainerView.this);
};
TopControlsContainerView(Context context, WebContents webContents,
ContentViewRenderView contentViewRenderView, Listener listener) {
super(context);
mContentViewRenderView = contentViewRenderView;
mWebContents = webContents;
mEventOffsetHandler =
new EventOffsetHandler(new EventOffsetHandler.EventOffsetHandlerDelegate() {
@Override
public float getTop() {
return mTopContentOffset;
}
@Override
public void setCurrentTouchEventOffsets(float top) {
mWebContents.getEventForwarder().setCurrentTouchEventOffsets(0, top);
}
});
mNativeTopControlsContainerView =
TopControlsContainerViewJni.get().createTopControlsContainerView(
this, webContents, contentViewRenderView.getNativeHandle());
mListener = listener;
}
public void destroy() {
setView(null);
TopControlsContainerViewJni.get().deleteTopControlsContainerView(
mNativeTopControlsContainerView, TopControlsContainerView.this);
}
public long getNativeHandle() {
return mNativeTopControlsContainerView;
}
public EventOffsetHandler getEventOffsetHandler() {
return mEventOffsetHandler;
}
/**
* Returns the vertical offset for the WebContents.
*/
public int getTopContentOffset() {
return mView == null ? 0 : mTopContentOffset;
}
/**
* Returns true if the top control is visible to the user.
*/
public boolean isTopControlVisible() {
// Don't check the visibility of the View itself as it's hidden while scrolling.
return mView != null && mTopContentOffset != 0;
}
/**
* Sets the view from the client.
*/
public void setView(View view) {
if (mView == view) return;
if (mView != null) {
if (mView.getParent() == this) removeView(mView);
// TODO: need some sort of destroy to drop reference.
mViewResourceAdapter = null;
TopControlsContainerViewJni.get().deleteTopControlsLayer(
mNativeTopControlsContainerView, TopControlsContainerView.this);
mContentViewRenderView.getResourceManager()
.getDynamicResourceLoader()
.unregisterResource(TOP_CONTROLS_ID);
}
mView = view;
if (mView == null) return;
addView(view,
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY));
if (getWidth() > 0 && getHeight() > 0) {
view.layout(0, 0, getWidth(), getHeight());
createAdapterAndLayer();
}
if (mIsFullscreen) hideTopControls();
}
public View getView() {
return mView;
}
/**
* Called from ViewAndroidDelegate, see it for details.
*/
public void onTopControlsChanged(int topControlsOffsetY, int topContentOffsetY) {
if (mView == null) return;
if (mIsFullscreen) return;
if (topContentOffsetY == getHeight()) {
finishTopControlsScroll(topContentOffsetY);
return;
}
if (!mInTopControlsScroll) prepareForTopControlsScroll();
setTopControlsOffset(topControlsOffsetY, topContentOffsetY);
}
@SuppressLint("NewApi") // Used on O+, invalidateChildInParent used for previous versions.
@Override
public void onDescendantInvalidated(View child, View target) {
super.onDescendantInvalidated(child, target);
invalidateViewResourceAdapter();
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidateViewResourceAdapter();
return super.invalidateChildInParent(location, dirty);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mView == null) return;
int width = right - left;
int height = bottom - top;
if (height != mLastHeight || width != mLastWidth) {
mLastWidth = width;
mLastHeight = height;
if (mLastWidth > 0 && mLastHeight > 0) {
if (mViewResourceAdapter == null) {
createAdapterAndLayer();
} else {
TopControlsContainerViewJni.get().setTopControlsSize(
mNativeTopControlsContainerView, TopControlsContainerView.this,
mLastWidth, mLastHeight);
}
}
}
}
/**
* Triggers copying the contents of mView to the offscreen buffer.
*/
private void invalidateViewResourceAdapter() {
if (mViewResourceAdapter == null || mView.getVisibility() != View.VISIBLE) return;
mViewResourceAdapter.invalidate(null);
removeCallbacks(mRefreshResourceIdRunnable);
postOnAnimation(mRefreshResourceIdRunnable);
}
/**
* Creates mViewResourceAdapter and the layer showing a copy of mView.
*/
private void createAdapterAndLayer() {
assert mViewResourceAdapter == null;
assert mView != null;
mViewResourceAdapter = new ViewResourceAdapter(mView);
mContentViewRenderView.getResourceManager().getDynamicResourceLoader().registerResource(
TOP_CONTROLS_ID, mViewResourceAdapter);
// It's important that the layer is created immediately and always kept in sync with the
// View. Creating the layer only when needed results in a noticeable delay between when
// the layer is created and actually shown. Chrome for Android does the same thing.
TopControlsContainerViewJni.get().createTopControlsLayer(
mNativeTopControlsContainerView, TopControlsContainerView.this, TOP_CONTROLS_ID);
mLastWidth = getWidth();
mLastHeight = getHeight();
TopControlsContainerViewJni.get().setTopControlsSize(mNativeTopControlsContainerView,
TopControlsContainerView.this, mLastWidth, mLastHeight);
}
private void finishTopControlsScroll(int topContentOffsetY) {
mInTopControlsScroll = false;
setTopControlsOffset(0, topContentOffsetY);
mContentViewRenderView.postOnAnimation(() -> showTopControls());
}
/**
* Returns true if the top-controls are completely shown or completely hidden. A return value
* of false indicates the top-controls are being moved.
*/
public boolean isTopControlsCompletelyShownOrHidden() {
return mTopContentOffset == 0 || mTopContentOffset == getHeight();
}
private void setTopControlsOffset(int topControlsOffsetY, int topContentOffsetY) {
mTopContentOffset = topContentOffsetY;
if (isTopControlsCompletelyShownOrHidden()) {
mListener.onTopControlsCompletelyShownOrHidden();
}
TopControlsContainerViewJni.get().setTopControlsOffset(mNativeTopControlsContainerView,
TopControlsContainerView.this, topControlsOffsetY, topContentOffsetY);
}
private void prepareForTopControlsScroll() {
mInTopControlsScroll = true;
mContentViewRenderView.postOnAnimation(() -> hideTopControls());
}
private void hideTopControls() {
if (mView != null) mView.setVisibility(View.INVISIBLE);
}
private void showTopControls() {
if (mView != null) mView.setVisibility(View.VISIBLE);
}
@CalledByNative
private void didToggleFullscreenModeForTab(final boolean isFullscreen) {
// Delay hiding until after the animation. This comes from Chrome code.
if (mSystemUiFullscreenResizeRunnable != null) {
getHandler().removeCallbacks(mSystemUiFullscreenResizeRunnable);
}
mSystemUiFullscreenResizeRunnable = () -> processFullscreenChanged(isFullscreen);
long delay = isFullscreen ? SYSTEM_UI_VIEWPORT_UPDATE_DELAY_MS : 0;
postDelayed(mSystemUiFullscreenResizeRunnable, delay);
}
private void processFullscreenChanged(boolean isFullscreen) {
mSystemUiFullscreenResizeRunnable = null;
if (mIsFullscreen == isFullscreen) return;
mIsFullscreen = isFullscreen;
if (mView == null) return;
if (mIsFullscreen) {
hideTopControls();
setTopControlsOffset(-mLastHeight, 0);
} else {
showTopControls();
setTopControlsOffset(0, mLastHeight);
}
}
@NativeMethods
interface Natives {
long createTopControlsContainerView(TopControlsContainerView view, WebContents webContents,
long nativeContentViewRenderView);
void deleteTopControlsContainerView(
long nativeTopControlsContainerView, TopControlsContainerView caller);
void createTopControlsLayer(
long nativeTopControlsContainerView, TopControlsContainerView caller, int id);
void deleteTopControlsLayer(
long nativeTopControlsContainerView, TopControlsContainerView caller);
void setTopControlsOffset(long nativeTopControlsContainerView,
TopControlsContainerView caller, int topControlsOffsetY, int topContentOffsetY);
void setTopControlsSize(long nativeTopControlsContainerView,
TopControlsContainerView caller, int width, int height);
void updateTopControlsResource(
long nativeTopControlsContainerView, TopControlsContainerView caller);
}
}