mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-20 03:08:19 +00:00
> Inflating taskbar views with a non-launcher window context > Removing separate taskbar view in Launcher > Handling taskbar icon clicks directly instead of going via launcher > Fixing some relayouts during show/hide animation Bug: 187353581 Test: Manual Change-Id: Ia7d44acbb770bf0345922234b3daa5cc040a5843
440 lines
17 KiB
Java
440 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2021 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.launcher3.taskbar;
|
|
|
|
import static android.view.View.MeasureSpec.EXACTLY;
|
|
import static android.view.View.MeasureSpec.makeMeasureSpec;
|
|
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.util.AttributeSet;
|
|
import android.view.DragEvent;
|
|
import android.view.Gravity;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewConfiguration;
|
|
import android.widget.LinearLayout;
|
|
|
|
import androidx.annotation.LayoutRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.android.launcher3.AbstractFloatingView;
|
|
import com.android.launcher3.BubbleTextView;
|
|
import com.android.launcher3.Insettable;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.folder.FolderIcon;
|
|
import com.android.launcher3.model.data.FolderInfo;
|
|
import com.android.launcher3.model.data.ItemInfo;
|
|
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
|
import com.android.launcher3.views.ActivityContext;
|
|
|
|
/**
|
|
* Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
|
|
*/
|
|
public class TaskbarView extends LinearLayout implements FolderIcon.FolderIconParent, Insettable {
|
|
|
|
private final int mIconTouchSize;
|
|
private final boolean mIsRtl;
|
|
private final int mTouchSlop;
|
|
private final RectF mTempDelegateBounds = new RectF();
|
|
private final RectF mDelegateSlopBounds = new RectF();
|
|
private final int[] mTempOutLocation = new int[2];
|
|
|
|
private final int mItemMarginLeftRight;
|
|
|
|
private final TaskbarActivityContext mActivityContext;
|
|
|
|
// Initialized in TaskbarController constructor.
|
|
private View.OnClickListener mIconClickListener;
|
|
private View.OnLongClickListener mIconLongClickListener;
|
|
|
|
LinearLayout mSystemButtonContainer;
|
|
LinearLayout mHotseatIconsContainer;
|
|
|
|
// Delegate touches to the closest view if within mIconTouchSize.
|
|
private boolean mDelegateTargeted;
|
|
private View mDelegateView;
|
|
// Prevents dispatching touches to children if true
|
|
private boolean mTouchEnabled = true;
|
|
|
|
private boolean mIsDraggingItem;
|
|
// Only non-null when the corresponding Folder is open.
|
|
private @Nullable FolderIcon mLeaveBehindFolderIcon;
|
|
|
|
/** Provider of buttons added to taskbar in 3 button nav */
|
|
private ButtonProvider mButtonProvider;
|
|
|
|
private boolean mDisableRelayout;
|
|
private boolean mAreHolesAllowed;
|
|
|
|
public TaskbarView(@NonNull Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs,
|
|
int defStyleAttr) {
|
|
this(context, attrs, defStyleAttr, 0);
|
|
}
|
|
|
|
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
mActivityContext = ActivityContext.lookupContext(context);
|
|
|
|
Resources resources = getResources();
|
|
mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
|
|
mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
|
|
|
|
mIsRtl = Utilities.isRtl(resources);
|
|
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
mSystemButtonContainer = findViewById(R.id.system_button_layout);
|
|
mHotseatIconsContainer = findViewById(R.id.hotseat_icons_layout);
|
|
}
|
|
|
|
protected void construct(OnClickListener clickListener, OnLongClickListener longClickListener,
|
|
ButtonProvider buttonProvider) {
|
|
mIconClickListener = clickListener;
|
|
mIconLongClickListener = longClickListener;
|
|
mButtonProvider = buttonProvider;
|
|
|
|
if (mActivityContext.canShowNavButtons()) {
|
|
createNavButtons();
|
|
} else {
|
|
mSystemButtonContainer.setVisibility(GONE);
|
|
}
|
|
|
|
int numHotseatIcons = mActivityContext.getDeviceProfile().numShownHotseatIcons;
|
|
updateHotseatItems(new ItemInfo[numHotseatIcons]);
|
|
}
|
|
|
|
/**
|
|
* Enables/disables empty icons in taskbar so that the layout matches with Launcher
|
|
*/
|
|
public void setHolesAllowedInLayout(boolean areHolesAllowed) {
|
|
if (mAreHolesAllowed != areHolesAllowed) {
|
|
mAreHolesAllowed = areHolesAllowed;
|
|
updateHotseatItemsVisibility();
|
|
// TODO: Add animation
|
|
}
|
|
}
|
|
|
|
private void setHolesAllowedInLayoutNoAnimation(boolean areHolesAllowed) {
|
|
if (mAreHolesAllowed != areHolesAllowed) {
|
|
mAreHolesAllowed = areHolesAllowed;
|
|
updateHotseatItemsVisibility();
|
|
onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
|
|
makeMeasureSpec(getMeasuredHeight(), EXACTLY));
|
|
onLayout(false, getLeft(), getTop(), getRight(), getBottom());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
|
|
*/
|
|
protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
|
|
for (int i = 0; i < hotseatItemInfos.length; i++) {
|
|
ItemInfo hotseatItemInfo = hotseatItemInfos[
|
|
!mIsRtl ? i : hotseatItemInfos.length - i - 1];
|
|
View hotseatView = mHotseatIconsContainer.getChildAt(i);
|
|
|
|
// Replace any Hotseat views with the appropriate type if it's not already that type.
|
|
final int expectedLayoutResId;
|
|
boolean isFolder = false;
|
|
boolean needsReinflate = false;
|
|
if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
|
|
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
|
|
} else if (hotseatItemInfo instanceof FolderInfo) {
|
|
expectedLayoutResId = R.layout.folder_icon;
|
|
isFolder = true;
|
|
// Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, so
|
|
// if the info changes we need to reinflate. This should only happen if a new folder
|
|
// is dragged to the position that another folder previously existed.
|
|
needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
|
|
} else {
|
|
expectedLayoutResId = R.layout.taskbar_app_icon;
|
|
}
|
|
if (hotseatView == null
|
|
|| hotseatView.getSourceLayoutResId() != expectedLayoutResId
|
|
|| needsReinflate) {
|
|
mHotseatIconsContainer.removeView(hotseatView);
|
|
if (isFolder) {
|
|
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
|
|
FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
|
|
mActivityContext, this, folderInfo);
|
|
folderIcon.setTextVisible(false);
|
|
hotseatView = folderIcon;
|
|
} else {
|
|
hotseatView = inflate(expectedLayoutResId);
|
|
}
|
|
int iconSize = mActivityContext.getDeviceProfile().iconSizePx;
|
|
LayoutParams lp = new LayoutParams(iconSize, iconSize);
|
|
lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
|
|
mHotseatIconsContainer.addView(hotseatView, i, lp);
|
|
}
|
|
|
|
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
|
|
if (hotseatView instanceof BubbleTextView
|
|
&& hotseatItemInfo instanceof WorkspaceItemInfo) {
|
|
((BubbleTextView) hotseatView).applyFromWorkspaceItem(
|
|
(WorkspaceItemInfo) hotseatItemInfo);
|
|
hotseatView.setOnClickListener(mIconClickListener);
|
|
hotseatView.setOnLongClickListener(mIconLongClickListener);
|
|
} else if (isFolder) {
|
|
hotseatView.setOnClickListener(mIconClickListener);
|
|
hotseatView.setOnLongClickListener(mIconLongClickListener);
|
|
} else {
|
|
hotseatView.setOnClickListener(null);
|
|
hotseatView.setOnLongClickListener(null);
|
|
hotseatView.setTag(null);
|
|
}
|
|
updateHotseatItemVisibility(hotseatView);
|
|
}
|
|
}
|
|
|
|
protected void updateHotseatItemsVisibility() {
|
|
for (int i = mHotseatIconsContainer.getChildCount() - 1; i >= 0; i--) {
|
|
updateHotseatItemVisibility(mHotseatIconsContainer.getChildAt(i));
|
|
}
|
|
}
|
|
|
|
private void updateHotseatItemVisibility(View hotseatView) {
|
|
hotseatView.setVisibility(
|
|
hotseatView.getTag() != null ? VISIBLE : (mAreHolesAllowed ? INVISIBLE : GONE));
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchTouchEvent(MotionEvent ev) {
|
|
if (!mTouchEnabled) {
|
|
return true;
|
|
}
|
|
return super.dispatchTouchEvent(ev);
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
boolean handled = delegateTouchIfNecessary(event);
|
|
return super.onTouchEvent(event) || handled;
|
|
}
|
|
|
|
public void setTouchesEnabled(boolean touchEnabled) {
|
|
this.mTouchEnabled = touchEnabled;
|
|
}
|
|
|
|
/**
|
|
* User touched the Taskbar background. Determine whether the touch is close enough to a view
|
|
* that we should forward the touches to it.
|
|
* @return Whether a delegate view was chosen and it handled the touch event.
|
|
*/
|
|
private boolean delegateTouchIfNecessary(MotionEvent event) {
|
|
final float x = event.getX();
|
|
final float y = event.getY();
|
|
if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
|
|
View delegateView = findDelegateView(x, y);
|
|
if (delegateView != null) {
|
|
mDelegateTargeted = true;
|
|
mDelegateView = delegateView;
|
|
mDelegateSlopBounds.set(mTempDelegateBounds);
|
|
mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
|
|
}
|
|
}
|
|
|
|
boolean sendToDelegate = mDelegateTargeted;
|
|
boolean inBounds = true;
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_MOVE:
|
|
inBounds = mDelegateSlopBounds.contains(x, y);
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
mDelegateTargeted = false;
|
|
break;
|
|
}
|
|
|
|
boolean handled = false;
|
|
if (sendToDelegate) {
|
|
if (inBounds) {
|
|
// Offset event coordinates to be inside the target view
|
|
event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
|
|
} else {
|
|
// Offset event coordinates to be outside the target view (in case it does
|
|
// something like tracking pressed state)
|
|
event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
|
|
}
|
|
handled = mDelegateView.dispatchTouchEvent(event);
|
|
// Cleanup if this was the last event to send to the delegate.
|
|
if (!mDelegateTargeted) {
|
|
mDelegateView = null;
|
|
}
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
/**
|
|
* Return an item whose touch bounds contain the given coordinates,
|
|
* or null if no such item exists.
|
|
*
|
|
* Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view.
|
|
*/
|
|
private @Nullable View findDelegateView(float x, float y) {
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
View child = getChildAt(i);
|
|
if (!child.isShown() || !child.isClickable()) {
|
|
continue;
|
|
}
|
|
int childCenterX = child.getLeft() + child.getWidth() / 2;
|
|
int childCenterY = child.getTop() + child.getHeight() / 2;
|
|
mTempDelegateBounds.set(
|
|
childCenterX - mIconTouchSize / 2f,
|
|
childCenterY - mIconTouchSize / 2f,
|
|
childCenterX + mIconTouchSize / 2f,
|
|
childCenterY + mIconTouchSize / 2f);
|
|
if (mTempDelegateBounds.contains(x, y)) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
|
|
* touch bounds.
|
|
*/
|
|
public boolean isEventOverAnyItem(MotionEvent ev) {
|
|
getLocationOnScreen(mTempOutLocation);
|
|
float xInOurCoordinates = ev.getX() - mTempOutLocation[0];
|
|
float yInOurCoorindates = ev.getY() - mTempOutLocation[1];
|
|
return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null;
|
|
}
|
|
|
|
/**
|
|
* Add back/home/recents buttons into a single ViewGroup that will be inserted at
|
|
* {@param navButtonStartIndex}
|
|
*/
|
|
private void createNavButtons() {
|
|
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
|
mActivityContext.getDeviceProfile().iconSizePx,
|
|
mActivityContext.getDeviceProfile().iconSizePx
|
|
);
|
|
buttonParams.gravity = Gravity.CENTER;
|
|
|
|
mSystemButtonContainer.addView(mButtonProvider.getBack(), buttonParams);
|
|
mSystemButtonContainer.addView(mButtonProvider.getHome(), buttonParams);
|
|
mSystemButtonContainer.addView(mButtonProvider.getRecents(), buttonParams);
|
|
}
|
|
|
|
@Override
|
|
public boolean onDragEvent(DragEvent event) {
|
|
switch (event.getAction()) {
|
|
case DragEvent.ACTION_DRAG_STARTED:
|
|
mIsDraggingItem = true;
|
|
AbstractFloatingView.closeAllOpenViews(mActivityContext);
|
|
return true;
|
|
case DragEvent.ACTION_DRAG_ENDED:
|
|
mIsDraggingItem = false;
|
|
break;
|
|
}
|
|
return super.onDragEvent(event);
|
|
}
|
|
|
|
public boolean isDraggingItem() {
|
|
return mIsDraggingItem;
|
|
}
|
|
|
|
/**
|
|
* @return The bounding box of where the hotseat elements are relative to this TaskbarView.
|
|
*/
|
|
protected RectF getHotseatBounds() {
|
|
RectF result;
|
|
mDisableRelayout = true;
|
|
boolean wereHolesAllowed = mAreHolesAllowed;
|
|
setHolesAllowedInLayoutNoAnimation(true);
|
|
result = new RectF(
|
|
mHotseatIconsContainer.getLeft(),
|
|
mHotseatIconsContainer.getTop(),
|
|
mHotseatIconsContainer.getRight(),
|
|
mHotseatIconsContainer.getBottom());
|
|
setHolesAllowedInLayoutNoAnimation(wereHolesAllowed);
|
|
mDisableRelayout = false;
|
|
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public void requestLayout() {
|
|
if (!mDisableRelayout) {
|
|
super.requestLayout();
|
|
}
|
|
}
|
|
|
|
// FolderIconParent implemented methods.
|
|
|
|
@Override
|
|
public void drawFolderLeaveBehindForIcon(FolderIcon child) {
|
|
mLeaveBehindFolderIcon = child;
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
public void clearFolderLeaveBehind(FolderIcon child) {
|
|
mLeaveBehindFolderIcon = null;
|
|
invalidate();
|
|
}
|
|
|
|
// End FolderIconParent implemented methods.
|
|
|
|
@Override
|
|
protected void onDraw(Canvas canvas) {
|
|
super.onDraw(canvas);
|
|
if (mLeaveBehindFolderIcon != null) {
|
|
canvas.save();
|
|
canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
|
|
mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
|
|
canvas.restore();
|
|
}
|
|
}
|
|
|
|
private View inflate(@LayoutRes int layoutResId) {
|
|
return mActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
|
|
}
|
|
|
|
@Override
|
|
public void setInsets(Rect insets) {
|
|
// Ignore, we just implement Insettable to draw behind system insets.
|
|
}
|
|
|
|
public void setIconsVisibility(boolean isVisible) {
|
|
mHotseatIconsContainer.setVisibility(isVisible ? VISIBLE : INVISIBLE);
|
|
}
|
|
|
|
public boolean areIconsVisible() {
|
|
return mHotseatIconsContainer.getVisibility() == VISIBLE;
|
|
}
|
|
}
|