2e6da1539b
-> Pulling out the parts of device profile which can (and need to be) initialized and accessed without access to an Activity context, ie. the invariant bits. -> The invariant bits are stored in InvariantDeviceProfile which is initialized statically from LauncherAppState. -> The DeviceProfile contains the Activity context-dependent bits, and we will create one of these for each Activity instance, and this instance is accessed through the Launcher activity. -> It's possible that we can continue to refactor this such that all appropriate dimensions can be computed without an Activity context (by only specifying orientation). This would be an extension of this CL and allow us to know exactly how launcher will look in both orientations from any context. Sets the stage for some improvements around b/19514688 Change-Id: Ia7daccf14d8ca2b9cb340b8780b684769e9f1892
533 lines
18 KiB
Java
533 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2008 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;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.content.res.ColorStateList;
|
|
import android.content.res.Resources;
|
|
import android.content.res.Resources.Theme;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Region;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.util.TypedValue;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewParent;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.launcher3.IconCache.IconLoadRequest;
|
|
import com.android.launcher3.model.PackageItemInfo;
|
|
|
|
/**
|
|
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
|
|
* because we want to make the bubble taller than the text and TextView's clip is
|
|
* too aggressive.
|
|
*/
|
|
public class BubbleTextView extends TextView {
|
|
|
|
private static SparseArray<Theme> sPreloaderThemes = new SparseArray<Theme>(2);
|
|
|
|
private static final float SHADOW_LARGE_RADIUS = 4.0f;
|
|
private static final float SHADOW_SMALL_RADIUS = 1.75f;
|
|
private static final float SHADOW_Y_OFFSET = 2.0f;
|
|
private static final int SHADOW_LARGE_COLOUR = 0xDD000000;
|
|
private static final int SHADOW_SMALL_COLOUR = 0xCC000000;
|
|
static final float PADDING_V = 3.0f;
|
|
|
|
private static final int DISPLAY_WORKSPACE = 0;
|
|
private static final int DISPLAY_ALL_APPS = 1;
|
|
|
|
private Drawable mIcon;
|
|
private final Drawable mBackground;
|
|
private final CheckLongPressHelper mLongPressHelper;
|
|
private final HolographicOutlineHelper mOutlineHelper;
|
|
|
|
private boolean mBackgroundSizeChanged;
|
|
|
|
private Bitmap mPressedBackground;
|
|
|
|
private float mSlop;
|
|
|
|
private final boolean mDeferShadowGenerationOnTouch;
|
|
private final boolean mCustomShadowsEnabled;
|
|
private final boolean mLayoutHorizontal;
|
|
private final int mIconSize;
|
|
private int mTextColor;
|
|
|
|
private boolean mStayPressed;
|
|
private boolean mIgnorePressedStateChange;
|
|
private boolean mDisableRelayout = false;
|
|
|
|
private IconLoadRequest mIconLoadRequest;
|
|
|
|
public BubbleTextView(Context context) {
|
|
this(context, null, 0);
|
|
}
|
|
|
|
public BubbleTextView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
LauncherAppState app = LauncherAppState.getInstance();
|
|
DeviceProfile grid = ((Launcher) context).getDeviceProfile();
|
|
|
|
TypedArray a = context.obtainStyledAttributes(attrs,
|
|
R.styleable.BubbleTextView, defStyle, 0);
|
|
mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true);
|
|
mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
|
|
mDeferShadowGenerationOnTouch =
|
|
a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false);
|
|
|
|
int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
|
|
int defaultIconSize = grid.iconSizePx;
|
|
if (display == DISPLAY_WORKSPACE) {
|
|
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
|
|
} else if (display == DISPLAY_ALL_APPS) {
|
|
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
|
|
defaultIconSize = grid.allAppsIconSizePx;
|
|
}
|
|
|
|
mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
|
|
defaultIconSize);
|
|
|
|
a.recycle();
|
|
|
|
if (mCustomShadowsEnabled) {
|
|
// Draw the background itself as the parent is drawn twice.
|
|
mBackground = getBackground();
|
|
setBackground(null);
|
|
} else {
|
|
mBackground = null;
|
|
}
|
|
|
|
mLongPressHelper = new CheckLongPressHelper(this);
|
|
|
|
mOutlineHelper = HolographicOutlineHelper.obtain(getContext());
|
|
if (mCustomShadowsEnabled) {
|
|
setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
|
|
}
|
|
|
|
setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
|
|
}
|
|
|
|
public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {
|
|
applyFromShortcutInfo(info, iconCache, false);
|
|
}
|
|
|
|
public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,
|
|
boolean promiseStateChanged) {
|
|
Bitmap b = info.getIcon(iconCache);
|
|
|
|
FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b);
|
|
iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
|
|
|
|
setIcon(iconDrawable, mIconSize);
|
|
if (info.contentDescription != null) {
|
|
setContentDescription(info.contentDescription);
|
|
}
|
|
setText(info.title);
|
|
setTag(info);
|
|
|
|
if (promiseStateChanged || info.isPromise()) {
|
|
applyState(promiseStateChanged);
|
|
}
|
|
}
|
|
|
|
public void applyFromApplicationInfo(AppInfo info) {
|
|
setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize);
|
|
setText(info.title);
|
|
if (info.contentDescription != null) {
|
|
setContentDescription(info.contentDescription);
|
|
}
|
|
// We don't need to check the info since it's not a ShortcutInfo
|
|
super.setTag(info);
|
|
|
|
// Verify high res immediately
|
|
verifyHighRes();
|
|
}
|
|
|
|
public void applyFromPackageItemInfo(PackageItemInfo info) {
|
|
setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize);
|
|
setText(info.title);
|
|
if (info.contentDescription != null) {
|
|
setContentDescription(info.contentDescription);
|
|
}
|
|
// We don't need to check the info since it's not a ShortcutInfo
|
|
super.setTag(info);
|
|
|
|
// Verify high res immediately
|
|
verifyHighRes();
|
|
}
|
|
|
|
|
|
@Override
|
|
protected boolean setFrame(int left, int top, int right, int bottom) {
|
|
if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) {
|
|
mBackgroundSizeChanged = true;
|
|
}
|
|
return super.setFrame(left, top, right, bottom);
|
|
}
|
|
|
|
@Override
|
|
protected boolean verifyDrawable(Drawable who) {
|
|
return who == mBackground || super.verifyDrawable(who);
|
|
}
|
|
|
|
@Override
|
|
public void setTag(Object tag) {
|
|
if (tag != null) {
|
|
LauncherModel.checkItemInfo((ItemInfo) tag);
|
|
}
|
|
super.setTag(tag);
|
|
}
|
|
|
|
@Override
|
|
public void setPressed(boolean pressed) {
|
|
super.setPressed(pressed);
|
|
|
|
if (!mIgnorePressedStateChange) {
|
|
updateIconState();
|
|
}
|
|
}
|
|
|
|
/** Returns the icon for this view. */
|
|
public Drawable getIcon() {
|
|
return mIcon;
|
|
}
|
|
|
|
/** Returns whether the layout is horizontal. */
|
|
public boolean isLayoutHorizontal() {
|
|
return mLayoutHorizontal;
|
|
}
|
|
|
|
private void updateIconState() {
|
|
if (mIcon instanceof FastBitmapDrawable) {
|
|
((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
// Call the superclass onTouchEvent first, because sometimes it changes the state to
|
|
// isPressed() on an ACTION_UP
|
|
boolean result = super.onTouchEvent(event);
|
|
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
// So that the pressed outline is visible immediately on setStayPressed(),
|
|
// we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
|
|
// to create it)
|
|
if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) {
|
|
mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
|
|
}
|
|
|
|
mLongPressHelper.postCheckForLongPress();
|
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_UP:
|
|
// If we've touched down and up on an item, and it's still not "pressed", then
|
|
// destroy the pressed outline
|
|
if (!isPressed()) {
|
|
mPressedBackground = null;
|
|
}
|
|
|
|
mLongPressHelper.cancelLongPress();
|
|
break;
|
|
case MotionEvent.ACTION_MOVE:
|
|
if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
|
|
mLongPressHelper.cancelLongPress();
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void setStayPressed(boolean stayPressed) {
|
|
mStayPressed = stayPressed;
|
|
if (!stayPressed) {
|
|
mPressedBackground = null;
|
|
} else {
|
|
if (mPressedBackground == null) {
|
|
mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
|
|
}
|
|
}
|
|
|
|
// Only show the shadow effect when persistent pressed state is set.
|
|
ViewParent parent = getParent();
|
|
if (parent != null && parent.getParent() instanceof BubbleTextShadowHandler) {
|
|
((BubbleTextShadowHandler) parent.getParent()).setPressedIcon(
|
|
this, mPressedBackground);
|
|
}
|
|
|
|
updateIconState();
|
|
}
|
|
|
|
void clearPressedBackground() {
|
|
setPressed(false);
|
|
setStayPressed(false);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
if (super.onKeyDown(keyCode, event)) {
|
|
// Pre-create shadow so show immediately on click.
|
|
if (mPressedBackground == null) {
|
|
mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
// Unlike touch events, keypress event propagate pressed state change immediately,
|
|
// without waiting for onClickHandler to execute. Disable pressed state changes here
|
|
// to avoid flickering.
|
|
mIgnorePressedStateChange = true;
|
|
boolean result = super.onKeyUp(keyCode, event);
|
|
|
|
mPressedBackground = null;
|
|
mIgnorePressedStateChange = false;
|
|
updateIconState();
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
if (!mCustomShadowsEnabled) {
|
|
super.draw(canvas);
|
|
return;
|
|
}
|
|
|
|
final Drawable background = mBackground;
|
|
if (background != null) {
|
|
final int scrollX = getScrollX();
|
|
final int scrollY = getScrollY();
|
|
|
|
if (mBackgroundSizeChanged) {
|
|
background.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
|
|
mBackgroundSizeChanged = false;
|
|
}
|
|
|
|
if ((scrollX | scrollY) == 0) {
|
|
background.draw(canvas);
|
|
} else {
|
|
canvas.translate(scrollX, scrollY);
|
|
background.draw(canvas);
|
|
canvas.translate(-scrollX, -scrollY);
|
|
}
|
|
}
|
|
|
|
// If text is transparent, don't draw any shadow
|
|
if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) {
|
|
getPaint().clearShadowLayer();
|
|
super.draw(canvas);
|
|
return;
|
|
}
|
|
|
|
// We enhance the shadow by drawing the shadow twice
|
|
getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
|
|
super.draw(canvas);
|
|
canvas.save(Canvas.CLIP_SAVE_FLAG);
|
|
canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(),
|
|
getScrollX() + getWidth(),
|
|
getScrollY() + getHeight(), Region.Op.INTERSECT);
|
|
getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR);
|
|
super.draw(canvas);
|
|
canvas.restore();
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
|
|
if (mBackground != null) mBackground.setCallback(this);
|
|
|
|
if (mIcon instanceof PreloadIconDrawable) {
|
|
((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme());
|
|
}
|
|
mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
if (mBackground != null) mBackground.setCallback(null);
|
|
}
|
|
|
|
@Override
|
|
public void setTextColor(int color) {
|
|
mTextColor = color;
|
|
super.setTextColor(color);
|
|
}
|
|
|
|
@Override
|
|
public void setTextColor(ColorStateList colors) {
|
|
mTextColor = colors.getDefaultColor();
|
|
super.setTextColor(colors);
|
|
}
|
|
|
|
public void setTextVisibility(boolean visible) {
|
|
Resources res = getResources();
|
|
if (visible) {
|
|
super.setTextColor(mTextColor);
|
|
} else {
|
|
super.setTextColor(res.getColor(android.R.color.transparent));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void cancelLongPress() {
|
|
super.cancelLongPress();
|
|
|
|
mLongPressHelper.cancelLongPress();
|
|
}
|
|
|
|
public void applyState(boolean promiseStateChanged) {
|
|
if (getTag() instanceof ShortcutInfo) {
|
|
ShortcutInfo info = (ShortcutInfo) getTag();
|
|
final boolean isPromise = info.isPromise();
|
|
final int progressLevel = isPromise ?
|
|
((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ?
|
|
info.getInstallProgress() : 0)) : 100;
|
|
|
|
if (mIcon != null) {
|
|
final PreloadIconDrawable preloadDrawable;
|
|
if (mIcon instanceof PreloadIconDrawable) {
|
|
preloadDrawable = (PreloadIconDrawable) mIcon;
|
|
} else {
|
|
preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme());
|
|
setIcon(preloadDrawable, mIconSize);
|
|
}
|
|
|
|
preloadDrawable.setLevel(progressLevel);
|
|
if (promiseStateChanged) {
|
|
preloadDrawable.maybePerformFinishedAnimation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Theme getPreloaderTheme() {
|
|
Object tag = getTag();
|
|
int style = ((tag != null) && (tag instanceof ShortcutInfo) &&
|
|
(((ShortcutInfo) tag).container >= 0)) ? R.style.PreloadIcon_Folder
|
|
: R.style.PreloadIcon;
|
|
Theme theme = sPreloaderThemes.get(style);
|
|
if (theme == null) {
|
|
theme = getResources().newTheme();
|
|
theme.applyStyle(style, true);
|
|
sPreloaderThemes.put(style, theme);
|
|
}
|
|
return theme;
|
|
}
|
|
|
|
/**
|
|
* Sets the icon for this view based on the layout direction.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
private Drawable setIcon(Drawable icon, int iconSize) {
|
|
mIcon = icon;
|
|
if (iconSize != -1) {
|
|
mIcon.setBounds(0, 0, iconSize, iconSize);
|
|
}
|
|
if (mLayoutHorizontal) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
setCompoundDrawablesRelative(mIcon, null, null, null);
|
|
} else {
|
|
setCompoundDrawables(mIcon, null, null, null);
|
|
}
|
|
} else {
|
|
setCompoundDrawables(null, mIcon, null, null);
|
|
}
|
|
return icon;
|
|
}
|
|
|
|
@Override
|
|
public void requestLayout() {
|
|
if (!mDisableRelayout) {
|
|
super.requestLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies the item info if it is same as what the view is pointing to currently.
|
|
*/
|
|
public void reapplyItemInfo(final ItemInfo info) {
|
|
if (getTag() == info) {
|
|
mIconLoadRequest = null;
|
|
mDisableRelayout = true;
|
|
if (info instanceof AppInfo) {
|
|
applyFromApplicationInfo((AppInfo) info);
|
|
} else if (info instanceof ShortcutInfo) {
|
|
applyFromShortcutInfo((ShortcutInfo) info,
|
|
LauncherAppState.getInstance().getIconCache());
|
|
} else if (info instanceof PackageItemInfo) {
|
|
applyFromPackageItemInfo((PackageItemInfo) info);
|
|
}
|
|
mDisableRelayout = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies that the current icon is high-res otherwise posts a request to load the icon.
|
|
*/
|
|
public void verifyHighRes() {
|
|
if (mIconLoadRequest != null) {
|
|
mIconLoadRequest.cancel();
|
|
mIconLoadRequest = null;
|
|
}
|
|
if (getTag() instanceof AppInfo) {
|
|
AppInfo info = (AppInfo) getTag();
|
|
if (info.usingLowResIcon) {
|
|
mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
|
|
.updateIconInBackground(BubbleTextView.this, info);
|
|
}
|
|
} else if (getTag() instanceof ShortcutInfo) {
|
|
ShortcutInfo info = (ShortcutInfo) getTag();
|
|
if (info.usingLowResIcon) {
|
|
mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
|
|
.updateIconInBackground(BubbleTextView.this, info);
|
|
}
|
|
} else if (getTag() instanceof PackageItemInfo) {
|
|
PackageItemInfo info = (PackageItemInfo) getTag();
|
|
if (info.usingLowResIcon) {
|
|
mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
|
|
.updateIconInBackground(BubbleTextView.this, info);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface to be implemented by the grand parent to allow click shadow effect.
|
|
*/
|
|
public static interface BubbleTextShadowHandler {
|
|
void setPressedIcon(BubbleTextView icon, Bitmap background);
|
|
}
|
|
}
|