blob: 84f1b6161866633604bd140034c3b49ff4dd191a [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.portraitlauncher.panel;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import androidx.annotation.Nullable;
import com.android.car.carlauncher.CarTaskView;
import com.android.car.portraitlauncher.R;
import com.android.car.portraitlauncher.panel.animation.ClosePanelAnimator;
import com.android.car.portraitlauncher.panel.animation.ExpandPanelAnimator;
import com.android.car.portraitlauncher.panel.animation.FadeInPanelAnimator;
import com.android.car.portraitlauncher.panel.animation.FadeOutPanelAnimator;
import com.android.car.portraitlauncher.panel.animation.FullScreenPanelAnimator;
import com.android.car.portraitlauncher.panel.animation.PanelAnimator;
/**
* A view container used to display CarTaskViews.
*
* This panel can transition between various states, e.g. open, close and full screen.
* When panel is in open state it shows a grab bar to the users which can be dragged to transition
* to the other states.
*/
public class TaskViewPanel extends RelativeLayout {
private static final String TAG = TaskViewPanel.class.getSimpleName();
private static final boolean DBG = Build.IS_DEBUGGABLE;
/** The properties of each valid state of the panel. */
public static class State {
/** The bounds used for the panel when put in this state. */
Rect mBounds = new Rect();
/** The insets used for the panel. */
Insets mInsets = Insets.NONE;
/** Whether or not the panel should display the grip bar. */
private final boolean mHasGripBar;
/** Whether the panel is visible when put in this state. */
private final boolean mIsVisible;
/** Whether the panel is considered full screen when put in this state. */
private final boolean mIsFullScreen;
public State(boolean hasGripBar, boolean isVisible, boolean isFullScreen) {
mHasGripBar = hasGripBar;
mIsVisible = isVisible;
mIsFullScreen = isFullScreen;
}
boolean hasGripBar() {
return mHasGripBar;
}
/** Whether the panel in this state has any visible parts. */
public boolean isVisible() {
return mIsVisible;
}
/** Is this state considered full screen or not. */
public boolean isFullScreen() {
return mIsFullScreen;
}
/** The string representation of the state. Used for debugging */
public String toString() {
return "(visible: " + isVisible() + ", fullscreen: " + isFullScreen() + ", bounds: "
+ mBounds + ")";
}
}
/** Notifies the listener when the panel state changes. */
public interface OnStateChangeListener{
/**
* Called right before the panel state changes.
*
* @param oldState The state from which the transition is about to start.
* @param newState The final state of the panel after the transition.
* @param animated If the transition is animated.
*/
void onStateChangeStart(State oldState, State newState, boolean animated);
/**
* Called right after the panel state changes.
*
* If the transition is animated, this method would be called after the animation.
* @param oldState The state from which the transition started.
* @param newState The final state of the panel after the transition.
* @param animated If the transition is animated.
*/
void onStateChangeEnd(State oldState, State newState, boolean animated);
}
private static void logIfDebuggable(String message) {
if (DBG) {
Log.d(TAG, message);
}
}
/** The properties of the panel when in {@code open} state. */
private final State mOpenState;
/** The properties of the panel when in {@code close} state. */
private final State mCloseState;
/** The properties of the panel when in {@code full screen} state. */
private final State mFullScreenState;
/**
* The current state of the panel.
*
* When transitioning from an state to another, this value will show the final state of the
* animation.
*/
private State mActiveState;
/** An optional listener to observe when the panel state changes. */
private OnStateChangeListener mOnStateChangeListener;
/** The drag threshold after which the panel transitions to the close mode. */
private final int mDragThreshold;
/** The height of the grip bar. */
private int mGripBarHeight;
/** The grip bar used to drag the panel. */
private GripBarView mGripBar;
/** Internal container of the {@code CarTaskView}. */
private ViewGroup mTaskViewContainer;
/** A view that is shown on top of the task view and used to fake the fade effect. */
private View mTaskViewOverlay;
/** The {@code CarTaskView} embedded in this panel. This is the main content of the panel. */
private CarTaskView mTaskView;
/** Shows whether the panel is animating or there is no ongoing animation. */
private boolean mIsAnimating;
/** The last reported window bounds of the task view. */
private Rect mTaskViewWindowBounds;
/** The surface view showed on the back of the panel. */
private BackgroundSurfaceView mBackgroundSurfaceView;
/** The flag indicating if the task view on the panel is ready. */
private boolean mIsReady;
public TaskViewPanel(Context context) {
this(context, null);
}
public TaskViewPanel(Context context,
@Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskViewPanel(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TaskViewPanel(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mDragThreshold = (int) getResources().getDimension(R.dimen.panel_drag_threshold);
mOpenState = new State(/* hasGripBar = */ true, /* isVisible = */ true,
/* isFullScreen */false);
mCloseState = new State(/* hasGripBar = */ true, /* isVisible = */ false,
/* isFullScreen */false);
mFullScreenState = new State(/* hasGripBar = */ false, /* isVisible = */ true,
/* isFullScreen */true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mGripBar = findViewById(R.id.grip_bar);
mTaskViewContainer = findViewById(R.id.task_view_container);
mTaskViewOverlay = findViewById(R.id.task_view_overlay);
mBackgroundSurfaceView = findViewById(R.id.surface_view);
setupGrabBar();
setActiveState(mCloseState, /* animator = */ null);
}
/** Whether the panel is in the open state. */
public boolean isOpen() {
return mActiveState == mOpenState;
}
/** Whether the panel is actively animating. */
public boolean isAnimating() {
return mIsAnimating;
}
/** Transitions the panel into the open state. */
public void openPanel() {
openPanel(/* animated= */ true);
}
/** Transitions the panel into the open state. */
public void openPanel(boolean animated) {
PanelAnimator animator =
animated ? new ClosePanelAnimator(this, mOpenState.mBounds) : null;
setActiveState(mOpenState, animator);
}
/** Transitions the panel into the close state. */
public void closePanel() {
closePanel(/* animated= */ true);
}
/** Transitions the panel into the close state. */
public void closePanel(boolean animated) {
PanelAnimator animator =
animated ? new ClosePanelAnimator(this, mCloseState.mBounds) : null;
setActiveState(mCloseState, animator);
}
/** Transitions the panel into the open state using the expand animation. */
public void expandPanel() {
Point origin = new Point(mOpenState.mBounds.centerX(), mOpenState.mBounds.centerY());
PanelAnimator animator =
new ExpandPanelAnimator(this, origin, mOpenState.mBounds, mGripBar);
setActiveState(mOpenState, animator);
}
/** Transitions the panel into the open state using the fade-in animation. */
public void fadeInPanel() {
setActiveState(mOpenState, new FadeInPanelAnimator(this, mTaskView, mOpenState.mBounds));
}
/** Transitions the panel into the close state using the fade-out animation. */
public void fadeOutPanel() {
PanelAnimator animator =
new FadeOutPanelAnimator(this, mBackgroundSurfaceView, mTaskViewOverlay,
mTaskView, mOpenState.mBounds);
setActiveState(mCloseState, animator);
}
/** Transitions the panel into the full screen state. */
public void openFullScreenPanel(boolean animated) {
PanelAnimator animator = null;
if (animated) {
Point offset = new Point(mOpenState.mBounds.left, mOpenState.mBounds.top);
Rect bounds = mFullScreenState.mBounds;
animator = new FullScreenPanelAnimator(this, bounds, offset);
}
setActiveState(mFullScreenState, animator);
}
/** Sets the state change listener for the panel. */
public void setOnStateChangeListener(OnStateChangeListener listener) {
mOnStateChangeListener = listener;
}
/**
* Returns the grip bar bounds of the current state.
*
* Note that the visual grip bar bounds might be different from the value here during
* transition animations.
*
* @param bounds The {@code Rect} that is used to return the data.
*/
public void getGripBarBounds(Rect bounds) {
if (mActiveState.hasGripBar()) {
bounds.set(mActiveState.mBounds);
bounds.bottom = mActiveState.mBounds.top + mGripBarHeight;
} else {
bounds.setEmpty();
}
}
/** Updates the {@code TaskView} used in the panel. */
public void setTaskView(CarTaskView taskView) {
mTaskView = taskView;
mTaskViewContainer.addView(mTaskView);
onParentDimensionChanged();
}
/** Updates the readiness state of the panel. */
public void setReady(boolean isReady) {
mIsReady = isReady;
}
/** Returns whether the panel is ready. */
public boolean isReady() {
return mIsReady;
}
/** Refreshes the panel according to the given {@code Theme}. */
public void refresh(Resources.Theme theme) {
int backgroundColor = getResources().getColor(R.color.car_background, theme);
mTaskViewContainer.setBackgroundColor(backgroundColor);
mTaskViewOverlay.setBackgroundColor(backgroundColor);
mGripBar.refresh(theme);
mBackgroundSurfaceView.refresh(theme);
}
/**
* Updates the Obscured touch region of the panel.
* This need to be called if there are areas that the task view should not receive a touch
* input due to other blocking views in the view hierarchy.
*/
public void setObscuredTouchRegion(Region region) {
if (mTaskView == null) {
return;
}
mTaskView.setObscuredTouchRegion(region);
}
@SuppressLint("ClickableViewAccessibility")
private void setupGrabBar() {
mGripBarHeight = (int) getResources().getDimension(R.dimen.panel_grip_bar_height);
mGripBar.setOnTouchListener(new OnPanelDragListener() {
private boolean mIsEnabled = true;
@Override
public void onDragBegin() {
mIsEnabled = true;
}
@Override
public void onDrag(int deltaX, int deltaY) {
if (!mIsEnabled) {
return;
}
deltaY = Math.max(0, deltaY);
// Close the panel and disable the drag as soon as we cross the threshold.
if (deltaY > mDragThreshold) {
mIsEnabled = false;
closePanel();
} else {
Rect rect = new Rect(mActiveState.mBounds);
rect.offset(/* dx= */ 0, deltaY);
updateBounds(rect);
}
}
@Override
public void onDragEnd(int deltaX, int deltaY) {
if (!mIsEnabled) {
return;
}
openPanel();
}
});
}
/** Returns the bounds of the task view once fully transitioned to the active state */
private Rect getTaskViewBounds(State state) {
Rect bounds = new Rect(state.mBounds);
bounds.inset(mActiveState.mInsets);
if (state.hasGripBar()) {
bounds.top += mGripBarHeight;
}
return bounds;
}
/** Updates the insets of the taskView for non-fullscreen states. */
public void setInsets(Insets insets) {
if (insets.equals(mOpenState.mInsets)) {
return;
}
mOpenState.mInsets = insets;
mCloseState.mInsets = insets;
updateInsets(mActiveState.mInsets);
recalculateBounds();
updateBounds(mActiveState.mBounds);
post(() -> {
if (mTaskView != null) {
mTaskView.onLocationChanged();
}
});
}
/** Sets a fixed background color for the task view. */
public void setTaskViewBackgroundColor(int color) {
mBackgroundSurfaceView.setFixedColor(color);
}
/** Should be called when the view is no longer in use. */
public void onDestroy() {
mTaskView = null;
}
/** Should be called when the parent dimension changes. */
public void onParentDimensionChanged() {
int parentWidth = ((ViewGroup) getParent()).getWidth();
int parentHeight = ((ViewGroup) getParent()).getHeight();
logIfDebuggable("onDimensionChanged: " + parentWidth + " " + parentHeight);
recalculateBounds();
post(() -> {
if (mTaskView != null) {
mTaskView.onLocationChanged();
}
});
updateBounds(mActiveState.mBounds);
}
private void recalculateBounds() {
int parentWidth = ((ViewGroup) getParent()).getWidth();
int parentHeight = ((ViewGroup) getParent()).getHeight();
int panelHeight = parentWidth + mOpenState.mInsets.bottom + mGripBarHeight + 1;
int panelTop = Math.max(0, parentHeight - panelHeight);
mOpenState.mBounds.set(0, panelTop, parentWidth, parentHeight);
mCloseState.mBounds.set(0, parentHeight, parentWidth,
parentHeight + mOpenState.mBounds.height());
mFullScreenState.mBounds.set(0, 0, parentWidth, parentHeight);
}
private void setActiveState(State toState, PanelAnimator animator) {
State fromState = mActiveState;
logIfDebuggable("Panel( " + getTag() + ") active state changes from " + fromState
+ " to " + toState);
boolean animated = animator != null;
onStateChangeStart(fromState, toState, animated);
mActiveState = toState;
updateInsets(mActiveState.mInsets);
updateTaskViewWindowBounds();
if (animated) {
post(() -> animator.animate(() -> {
mGripBar.setVisibility(toState.hasGripBar() ? VISIBLE : GONE);
updateBounds(mActiveState.mBounds);
onStateChangeEnd(fromState, toState, /* animated= */ true);
}));
} else {
mGripBar.setVisibility(toState.hasGripBar() ? VISIBLE : GONE);
updateBounds(mActiveState.mBounds);
onStateChangeEnd(fromState, toState, /* animated= */ false);
}
}
private void onStateChangeStart(State fromState, State toState, boolean animated) {
mIsAnimating = animated;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChangeStart(fromState, toState, animated);
}
}
private void onStateChangeEnd(State fromState, State toState, boolean animated) {
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChangeEnd(fromState, toState, animated);
}
mIsAnimating = false;
}
private void updateTaskViewWindowBounds() {
// Due to performance issues we only set the window bounds when the panel is transitioning
// to a visible state and only if the window bounds is not changed since the last visible
// state.
Rect taskViewBounds = getTaskViewBounds(mActiveState);
if (!mActiveState.isVisible() || taskViewBounds.equals(mTaskViewWindowBounds)) {
return;
}
mTaskViewWindowBounds = taskViewBounds;
logIfDebuggable("TaskView bounds: " + mTaskViewWindowBounds);
if (mTaskView != null) {
mTaskView.setWindowBounds(taskViewBounds);
}
}
private void updateInsets(Insets insets) {
mTaskViewContainer.setPadding(insets.left, insets.top, insets.right, insets.bottom);
}
private void updateBounds(Rect bounds) {
final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
layoutParams.topMargin = bounds.top;
layoutParams.rightMargin = bounds.right;
layoutParams.width = bounds.width();
layoutParams.height = bounds.height();
setLayoutParams(layoutParams);
}
}