// Copyright 2015 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.compositor.overlays.strip; import static org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.AnimatableAnimation.createAnimation; import android.content.Context; import android.graphics.RectF; import org.chromium.base.ObserverList; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.R; import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation; import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable; import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animation; import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost; import org.chromium.chrome.browser.compositor.layouts.components.CompositorButton; import org.chromium.chrome.browser.compositor.layouts.components.VirtualView; import org.chromium.chrome.browser.compositor.overlays.strip.TabLoadTracker.TabLoadTrackerCallback; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.util.MathUtils; import org.chromium.ui.base.LocalizationUtils; import org.chromium.ui.resources.AndroidResourceType; import org.chromium.ui.resources.LayoutResource; import org.chromium.ui.resources.ResourceManager; import java.util.List; /** * {@link StripLayoutTab} is used to keep track of the strip position and rendering information for * a particular tab so it can draw itself onto the GL canvas. */ public class StripLayoutTab implements ChromeAnimation.Animatable<StripLayoutTab.Property>, VirtualView { /** An observer interface for StripLayoutTab. */ public interface Observer { /** @param visible Whether the StripLayoutTab is visible. */ public void onVisibilityChanged(boolean visible); } /** * Animatable properties that can be used with a {@link ChromeAnimation.Animatable} on a * {@link StripLayoutTab}. */ enum Property { X_OFFSET, Y_OFFSET, WIDTH, } // Behavior Constants private static final float VISIBILITY_FADE_CLOSE_BUTTON_PERCENTAGE = 0.99f; // Animation/Timer Constants private static final int ANIM_TAB_CLOSE_BUTTON_FADE_MS = 150; // Close button width private static final int CLOSE_BUTTON_WIDTH_DP = 36; private int mId = Tab.INVALID_TAB_ID; private final TabLoadTracker mLoadTracker; private final LayoutRenderHost mRenderHost; private boolean mVisible = true; private boolean mIsDying = false; private boolean mCanShowCloseButton = true; private final boolean mIncognito; private float mContentOffsetX; private float mVisiblePercentage = 1.f; private String mAccessibilityDescription; // Ideal intermediate parameters private float mIdealX; private float mTabOffsetX; private float mTabOffsetY; // Actual draw parameters private float mDrawX; private float mDrawY; private float mWidth; private float mHeight; private final RectF mTouchTarget = new RectF(); private boolean mShowingCloseButton = true; private final CompositorButton mCloseButton; // Content Animations private ChromeAnimation<Animatable<?>> mContentAnimations; private float mLoadingSpinnerRotationDegrees; // Preallocated private final RectF mClosePlacement = new RectF(); private ObserverList<Observer> mObservers = new ObserverList<>(); /** * Create a {@link StripLayoutTab} that represents the {@link Tab} with an id of * {@code id}. * * @param context An Android context for accessing system resources. * @param id The id of the {@link Tab} to visually represent. * @param loadTrackerCallback The {@link TabLoadTrackerCallback} to be notified of loading state * changes. * @param renderHost The {@link LayoutRenderHost}. * @param incognito Whether or not this layout tab is incognito. */ public StripLayoutTab(Context context, int id, TabLoadTrackerCallback loadTrackerCallback, LayoutRenderHost renderHost, boolean incognito) { mId = id; mLoadTracker = new TabLoadTracker(id, loadTrackerCallback); mRenderHost = renderHost; mIncognito = incognito; mCloseButton = new CompositorButton(context, 0, 0); mCloseButton.setResources(R.drawable.btn_tab_close_normal, R.drawable.btn_tab_close_pressed, R.drawable.btn_tab_close_white_normal, R.drawable.btn_tab_close_white_pressed); mCloseButton.setIncognito(mIncognito); mCloseButton.setBounds(getCloseRect()); mCloseButton.setClickSlop(0.f); String description = context.getResources().getString(R.string.accessibility_tabstrip_btn_close_tab); mCloseButton.setAccessibilityDescription(description, description); } /** @param observer The observer to add. */ @VisibleForTesting public void addObserver(Observer observer) { mObservers.addObserver(observer); } /** @param observer The observer to remove. */ public void removeObserver(Observer observer) { mObservers.removeObserver(observer); } /** * Get a list of virtual views for accessibility events. * * @param views A List to populate with virtual views. */ public void getVirtualViews(List<VirtualView> views) { views.add(this); if (mShowingCloseButton) views.add(mCloseButton); } /** * @param description A description for accessibility events. */ public void setAccessibilityDescription(String description) { mAccessibilityDescription = description; } @Override public String getAccessibilityDescription() { return mAccessibilityDescription; } @Override public void getTouchTarget(RectF target) { target.set(mTouchTarget); } @Override public boolean checkClicked(float x, float y) { // Since both the close button as well as the tab inhabit the same coordinates, the tab // should not consider itself hit if the close button is also hit, since it is on top. if (mShowingCloseButton && mCloseButton.checkClicked(x, y)) return false; return mTouchTarget.contains(x, y); } /** * @return The id of the {@link Tab} this {@link StripLayoutTab} represents. */ public int getId() { return mId; } /** * @param foreground Whether or not this tab is a foreground tab. * @return The Android resource that represents the tab background. */ public int getResourceId(boolean foreground) { if (foreground) { return mIncognito ? R.drawable.bg_tabstrip_incognito_tab : R.drawable.bg_tabstrip_tab; } return mIncognito ? R.drawable.bg_tabstrip_incognito_background_tab : R.drawable.bg_tabstrip_background_tab; } /** * @param visible Whether or not this {@link StripLayoutTab} should be drawn. */ public void setVisible(boolean visible) { mVisible = visible; for (Observer observer : mObservers) { observer.onVisibilityChanged(mVisible); } } /** * @return Whether or not this {@link StripLayoutTab} should be drawn. */ public boolean isVisible() { return mVisible; } /** * Mark this tab as in the process of dying. This lets us track which tabs are dead after * animations. * @param isDying Whether or not the tab is dying. */ public void setIsDying(boolean isDying) { mIsDying = isDying; } /** * @return Whether or not the tab is dying. */ public boolean isDying() { return mIsDying; } /** * @return Whether or not this tab should be visually represented as loading. */ public boolean isLoading() { return mLoadTracker.isLoading(); } /** * @return The rotation of the loading spinner in degrees. */ public float getLoadingSpinnerRotation() { return mLoadingSpinnerRotationDegrees; } /** * Additive spinner rotation update. * @param rotation The amount to rotate the spinner by in degrees. */ public void addLoadingSpinnerRotation(float rotation) { mLoadingSpinnerRotationDegrees = (mLoadingSpinnerRotationDegrees + rotation) % 1080; } /** * Called when this tab has started loading. */ public void pageLoadingStarted() { mLoadTracker.pageLoadingStarted(); } /** * Called when this tab has finished loading. */ public void pageLoadingFinished() { mLoadTracker.pageLoadingFinished(); } /** * Called when this tab has started loading resources. */ public void loadingStarted() { mLoadTracker.loadingStarted(); } /** * Called when this tab has finished loading resources. */ public void loadingFinished() { mLoadTracker.loadingFinished(); } /** * @param offsetX How far to offset the tab content (favicons and title). */ public void setContentOffsetX(float offsetX) { mContentOffsetX = MathUtils.clamp(offsetX, 0.f, mWidth); } /** * @return How far to offset the tab content (favicons and title). */ public float getContentOffsetX() { return mContentOffsetX; } /** * @param visiblePercentage How much of the tab is visible (not overlapped by other tabs). */ public void setVisiblePercentage(float visiblePercentage) { mVisiblePercentage = visiblePercentage; checkCloseButtonVisibility(true); } /** * @return How much of the tab is visible (not overlapped by other tabs). */ @VisibleForTesting public float getVisiblePercentage() { return mVisiblePercentage; } /** * @param show Whether or not the close button is allowed to be shown. */ public void setCanShowCloseButton(boolean show) { mCanShowCloseButton = show; checkCloseButtonVisibility(true); } /** * @param x The actual position in the strip, taking into account stacking, scrolling, etc. */ public void setDrawX(float x) { mCloseButton.setX(mCloseButton.getX() + (x - mDrawX)); mDrawX = x; mTouchTarget.left = mDrawX; mTouchTarget.right = mDrawX + mWidth; } /** * @return The actual position in the strip, taking into account stacking, scrolling, etc. */ public float getDrawX() { return mDrawX; } /** * @param y The vertical position for the tab. */ public void setDrawY(float y) { mCloseButton.setY(mCloseButton.getY() + (y - mDrawY)); mDrawY = y; mTouchTarget.top = mDrawY; mTouchTarget.bottom = mDrawY + mHeight; } /** * @return The vertical position for the tab. */ public float getDrawY() { return mDrawY; } /** * @param width The width of the tab. */ public void setWidth(float width) { mWidth = width; resetCloseRect(); mTouchTarget.right = mDrawX + mWidth; } /** * @return The width of the tab. */ public float getWidth() { return mWidth; } /** * @param height The height of the tab. */ public void setHeight(float height) { mHeight = height; resetCloseRect(); mTouchTarget.bottom = mDrawY + mHeight; } /** * @return The height of the tab. */ public float getHeight() { return mHeight; } /** * @param closePressed The current pressed state of the attached button. */ public void setClosePressed(boolean closePressed) { mCloseButton.setPressed(closePressed); } /** * @return The current pressed state of the close button. */ public boolean getClosePressed() { return mCloseButton.isPressed(); } /** * @return The close button for this tab. */ public CompositorButton getCloseButton() { return mCloseButton; } /** * This represents how much this tab's width should be counted when positioning tabs in the * stack. As tabs close or open, their width weight is increased. They visually take up * the same amount of space but the other tabs will smoothly move out of the way to make room. * @return The weight from 0 to 1 that the width of this tab should have on the stack. */ public float getWidthWeight() { return MathUtils.clamp(1.f - mDrawY / mHeight, 0.f, 1.f); } /** * @param x The x position of the position to test. * @param y The y position of the position to test. * @return Whether or not {@code x} and {@code y} is over the close button for this tab and * if the button can be clicked. */ public boolean checkCloseHitTest(float x, float y) { return mShowingCloseButton ? mCloseButton.checkClicked(x, y) : false; } /** * This is used to help calculate the tab's position and is not used for rendering. * @param offsetX The offset of the tab (used for drag and drop, slide animating, etc). */ public void setOffsetX(float offsetX) { mTabOffsetX = offsetX; } /** * This is used to help calculate the tab's position and is not used for rendering. * @return The offset of the tab (used for drag and drop, slide animating, etc). */ public float getOffsetX() { return mTabOffsetX; } /** * This is used to help calculate the tab's position and is not used for rendering. * @param x The ideal position, in an infinitely long strip, of this tab. */ public void setIdealX(float x) { mIdealX = x; } /** * This is used to help calculate the tab's position and is not used for rendering. * @return The ideal position, in an infinitely long strip, of this tab. */ public float getIdealX() { return mIdealX; } /** * This is used to help calculate the tab's position and is not used for rendering. * @param offsetY The vertical offset of the tab. */ public void setOffsetY(float offsetY) { mTabOffsetY = offsetY; } /** * This is used to help calculate the tab's position and is not used for rendering. * @return The vertical offset of the tab. */ public float getOffsetY() { return mTabOffsetY; } private void startAnimation(Animation<Animatable<?>> animation, boolean finishPrevious) { if (finishPrevious) finishAnimation(); if (mContentAnimations == null) { mContentAnimations = new ChromeAnimation<Animatable<?>>(); } mContentAnimations.add(animation); } /** * Finishes any content animations currently owned and running on this StripLayoutTab. */ public void finishAnimation() { if (mContentAnimations == null) return; mContentAnimations.updateAndFinish(); mContentAnimations = null; } /** * @return Whether or not there are any content animations running on this StripLayoutTab. */ public boolean isAnimating() { return mContentAnimations != null; } /** * Updates any content animations on this StripLayoutTab. * @param time The current time of the app in ms. * @param jumpToEnd Whether or not to force any current animations to end. * @return Whether or not animations are done. */ public boolean onUpdateAnimation(long time, boolean jumpToEnd) { if (mContentAnimations == null) return true; boolean finished = true; if (jumpToEnd) { finished = mContentAnimations.finished(); } else { finished = mContentAnimations.update(time); } if (jumpToEnd || finished) finishAnimation(); return finished; } @Override public void setProperty(Property prop, float val) { switch (prop) { case X_OFFSET: setOffsetX(val); break; case Y_OFFSET: setOffsetY(val); break; case WIDTH: setWidth(val); break; } } @Override public void onPropertyAnimationFinished(Property prop) {} private void resetCloseRect() { RectF closeRect = getCloseRect(); mCloseButton.setWidth(closeRect.width()); mCloseButton.setHeight(closeRect.height()); mCloseButton.setX(closeRect.left); mCloseButton.setY(closeRect.top); } private RectF getCloseRect() { if (!LocalizationUtils.isLayoutRtl()) { mClosePlacement.left = getWidth() - CLOSE_BUTTON_WIDTH_DP; mClosePlacement.right = mClosePlacement.left + CLOSE_BUTTON_WIDTH_DP; } else { mClosePlacement.left = 0; mClosePlacement.right = CLOSE_BUTTON_WIDTH_DP; } mClosePlacement.top = 0; mClosePlacement.bottom = getHeight(); float xOffset = 0; ResourceManager manager = mRenderHost.getResourceManager(); if (manager != null) { LayoutResource resource = manager.getResource(AndroidResourceType.STATIC, getResourceId(false)); if (resource != null) { xOffset = LocalizationUtils.isLayoutRtl() ? resource.getPadding().left : -(resource.getBitmapSize().width() - resource.getPadding().right); } } mClosePlacement.offset(getDrawX() + xOffset, getDrawY()); return mClosePlacement; } // TODO(dtrainor): Don't animate this if we're selecting or deselecting this tab. private void checkCloseButtonVisibility(boolean animate) { boolean shouldShow = mCanShowCloseButton && mVisiblePercentage > VISIBILITY_FADE_CLOSE_BUTTON_PERCENTAGE; if (shouldShow != mShowingCloseButton) { float opacity = shouldShow ? 1.f : 0.f; if (animate) { startAnimation(buildCloseButtonOpacityAnimation(opacity), true); } else { mCloseButton.setOpacity(opacity); } mShowingCloseButton = shouldShow; if (!mShowingCloseButton) mCloseButton.setPressed(false); } } private Animation<Animatable<?>> buildCloseButtonOpacityAnimation(float finalOpacity) { return createAnimation(mCloseButton, CompositorButton.Property.OPACITY, mCloseButton.getOpacity(), finalOpacity, ANIM_TAB_CLOSE_BUTTON_FADE_MS, 0, false, ChromeAnimation.getLinearInterpolator()); } }