blob: 8ee9b616d0c466f793694d1016f8120d2f7fafc0 [file] [log] [blame]
/*
* Copyright (C) 2012 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.keyguard;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
public class KeyguardWidgetFrame extends FrameLayout {
private final static PorterDuffXfermode sAddBlendMode =
new PorterDuffXfermode(PorterDuff.Mode.ADD);
static final float OUTLINE_ALPHA_MULTIPLIER = 0.6f;
static final int HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR = 0x99FF0000;
// Temporarily disable this for the time being until we know why the gfx is messing up
static final boolean ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY = true;
private int mGradientColor;
private LinearGradient mForegroundGradient;
private LinearGradient mLeftToRightGradient;
private LinearGradient mRightToLeftGradient;
private Paint mGradientPaint = new Paint();
boolean mLeftToRight = true;
private float mOverScrollAmount = 0f;
private final Rect mForegroundRect = new Rect();
private int mForegroundAlpha = 0;
private CheckLongPressHelper mLongPressHelper;
private Animator mFrameFade;
private boolean mIsSmall = false;
private Handler mWorkerHandler;
private float mBackgroundAlpha;
private float mContentAlpha;
private float mBackgroundAlphaMultiplier = 1.0f;
private Drawable mBackgroundDrawable;
private Rect mBackgroundRect = new Rect();
// These variables are all needed in order to size things properly before we're actually
// measured.
private int mSmallWidgetHeight;
private int mSmallFrameHeight;
private boolean mWidgetLockedSmall = false;
private int mMaxChallengeTop = -1;
private int mFrameStrokeAdjustment;
private boolean mPerformAppWidgetSizeUpdateOnBootComplete;
// This will hold the width value before we've actually been measured
private int mFrameHeight;
private boolean mIsHoveringOverDeleteDropTarget;
// Multiple callers may try and adjust the alpha of the frame. When a caller shows
// the outlines, we give that caller control, and nobody else can fade them out.
// This prevents animation conflicts.
private Object mBgAlphaController;
public KeyguardWidgetFrame(Context context) {
this(context, null, 0);
}
public KeyguardWidgetFrame(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyguardWidgetFrame(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLongPressHelper = new CheckLongPressHelper(this);
Resources res = context.getResources();
// TODO: this padding should really correspond to the padding embedded in the background
// drawable (ie. outlines).
float density = res.getDisplayMetrics().density;
int padding = (int) (res.getDisplayMetrics().density * 8);
setPadding(padding, padding, padding, padding);
mFrameStrokeAdjustment = 2 + (int) (2 * density);
// This will be overriden on phones based on the current security mode, however on tablets
// we need to specify a height.
mSmallWidgetHeight =
res.getDimensionPixelSize(R.dimen.kg_small_widget_height);
mBackgroundDrawable = res.getDrawable(R.drawable.kg_widget_bg_padded);
mGradientColor = res.getColor(R.color.kg_widget_pager_gradient);
mGradientPaint.setXfermode(sAddBlendMode);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
cancelLongPress();
KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallbacks);
}
private KeyguardUpdateMonitorCallback mUpdateMonitorCallbacks =
new KeyguardUpdateMonitorCallback() {
@Override
public void onBootCompleted() {
if (mPerformAppWidgetSizeUpdateOnBootComplete) {
performAppWidgetSizeCallbacksIfNecessary();
mPerformAppWidgetSizeUpdateOnBootComplete = false;
}
}
};
void setIsHoveringOverDeleteDropTarget(boolean isHovering) {
if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
if (mIsHoveringOverDeleteDropTarget != isHovering) {
mIsHoveringOverDeleteDropTarget = isHovering;
int resId = isHovering ? R.string.keyguard_accessibility_delete_widget_start
: R.string.keyguard_accessibility_delete_widget_end;
String text = getContext().getResources().getString(resId, getContentDescription());
announceForAccessibility(text);
invalidate();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLongPressHelper.postCheckForLongPress(ev);
break;
case MotionEvent.ACTION_MOVE:
mLongPressHelper.onMove(ev);
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mLongPressHelper.cancelLongPress();
break;
}
// Otherwise continue letting touch events fall through to children
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Watch for longpress events at this level to make sure
// users can always pick up this widget
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
mLongPressHelper.onMove(ev);
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mLongPressHelper.cancelLongPress();
break;
}
// We return true here to ensure that we will get cancel / up signal
// even if none of our children have requested touch.
return true;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
cancelLongPress();
}
@Override
public void cancelLongPress() {
super.cancelLongPress();
mLongPressHelper.cancelLongPress();
}
private void drawGradientOverlay(Canvas c) {
mGradientPaint.setShader(mForegroundGradient);
mGradientPaint.setAlpha(mForegroundAlpha);
c.drawRect(mForegroundRect, mGradientPaint);
}
private void drawHoveringOverDeleteOverlay(Canvas c) {
if (mIsHoveringOverDeleteDropTarget) {
c.drawColor(HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR);
}
}
protected void drawBg(Canvas canvas) {
if (mBackgroundAlpha > 0.0f) {
Drawable bg = mBackgroundDrawable;
bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255));
bg.setBounds(mBackgroundRect);
bg.draw(canvas);
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
canvas.save();
}
drawBg(canvas);
super.dispatchDraw(canvas);
drawGradientOverlay(canvas);
if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) {
drawHoveringOverDeleteOverlay(canvas);
canvas.restore();
}
}
/**
* Because this view has fading outlines, it is essential that we enable hardware
* layers on the content (child) so that updating the alpha of the outlines doesn't
* result in the content layer being recreated.
*/
public void enableHardwareLayersForContent() {
View widget = getContent();
if (widget != null && widget.isHardwareAccelerated()) {
widget.setLayerType(LAYER_TYPE_HARDWARE, null);
}
}
/**
* Because this view has fading outlines, it is essential that we enable hardware
* layers on the content (child) so that updating the alpha of the outlines doesn't
* result in the content layer being recreated.
*/
public void disableHardwareLayersForContent() {
View widget = getContent();
if (widget != null) {
widget.setLayerType(LAYER_TYPE_NONE, null);
}
}
public View getContent() {
return getChildAt(0);
}
public int getContentAppWidgetId() {
View content = getContent();
if (content instanceof AppWidgetHostView) {
return ((AppWidgetHostView) content).getAppWidgetId();
} else if (content instanceof KeyguardStatusView) {
return ((KeyguardStatusView) content).getAppWidgetId();
} else {
return AppWidgetManager.INVALID_APPWIDGET_ID;
}
}
public float getBackgroundAlpha() {
return mBackgroundAlpha;
}
public void setBackgroundAlphaMultiplier(float multiplier) {
if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) {
mBackgroundAlphaMultiplier = multiplier;
invalidate();
}
}
public float getBackgroundAlphaMultiplier() {
return mBackgroundAlphaMultiplier;
}
public void setBackgroundAlpha(float alpha) {
if (Float.compare(mBackgroundAlpha, alpha) != 0) {
mBackgroundAlpha = alpha;
invalidate();
}
}
public float getContentAlpha() {
return mContentAlpha;
}
public void setContentAlpha(float alpha) {
mContentAlpha = alpha;
View content = getContent();
if (content != null) {
content.setAlpha(alpha);
}
}
/**
* Depending on whether the security is up, the widget size needs to change
*
* @param height The height of the widget, -1 for full height
*/
private void setWidgetHeight(int height) {
boolean needLayout = false;
View widget = getContent();
if (widget != null) {
LayoutParams lp = (LayoutParams) widget.getLayoutParams();
if (lp.height != height) {
needLayout = true;
lp.height = height;
}
}
if (needLayout) {
requestLayout();
}
}
public void setMaxChallengeTop(int top) {
boolean dirty = mMaxChallengeTop != top;
mMaxChallengeTop = top;
mSmallWidgetHeight = top - getPaddingTop();
mSmallFrameHeight = top + getPaddingBottom();
if (dirty && mIsSmall) {
setWidgetHeight(mSmallWidgetHeight);
setFrameHeight(mSmallFrameHeight);
} else if (dirty && mWidgetLockedSmall) {
setWidgetHeight(mSmallWidgetHeight);
}
}
public boolean isSmall() {
return mIsSmall;
}
public void adjustFrame(int challengeTop) {
int frameHeight = challengeTop + getPaddingBottom();
setFrameHeight(frameHeight);
}
public void shrinkWidget(boolean alsoShrinkFrame) {
mIsSmall = true;
setWidgetHeight(mSmallWidgetHeight);
if (alsoShrinkFrame) {
setFrameHeight(mSmallFrameHeight);
}
}
public int getSmallFrameHeight() {
return mSmallFrameHeight;
}
public void setWidgetLockedSmall(boolean locked) {
if (locked) {
setWidgetHeight(mSmallWidgetHeight);
}
mWidgetLockedSmall = locked;
}
public void resetSize() {
mIsSmall = false;
if (!mWidgetLockedSmall) {
setWidgetHeight(LayoutParams.MATCH_PARENT);
}
setFrameHeight(getMeasuredHeight());
}
public void setFrameHeight(int height) {
mFrameHeight = height;
mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(mFrameHeight, getMeasuredHeight()));
mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,getMeasuredWidth() -
mFrameStrokeAdjustment, Math.min(getMeasuredHeight(), mFrameHeight) -
mFrameStrokeAdjustment);
updateGradient();
invalidate();
}
public void hideFrame(Object caller) {
fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION);
}
public void showFrame(Object caller) {
fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER,
KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION);
}
public void fadeFrame(Object caller, boolean takeControl, float alpha, int duration) {
if (takeControl) {
mBgAlphaController = caller;
}
if (mBgAlphaController != caller && mBgAlphaController != null) {
return;
}
if (mFrameFade != null) {
mFrameFade.cancel();
mFrameFade = null;
}
PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", alpha);
mFrameFade = ObjectAnimator.ofPropertyValuesHolder(this, bgAlpha);
mFrameFade.setDuration(duration);
mFrameFade.start();
}
private void updateGradient() {
float x0 = mLeftToRight ? 0 : mForegroundRect.width();
float x1 = mLeftToRight ? mForegroundRect.width(): 0;
mLeftToRightGradient = new LinearGradient(x0, 0f, x1, 0f,
mGradientColor, 0, Shader.TileMode.CLAMP);
mRightToLeftGradient = new LinearGradient(x1, 0f, x0, 0f,
mGradientColor, 0, Shader.TileMode.CLAMP);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (!mIsSmall) {
mFrameHeight = h;
}
// mFrameStrokeAdjustment is a cludge to prevent the overlay from drawing outside the
// rounded rect background.
mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,
w - mFrameStrokeAdjustment, Math.min(h, mFrameHeight) - mFrameStrokeAdjustment);
mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(h, mFrameHeight));
updateGradient();
invalidate();
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
performAppWidgetSizeCallbacksIfNecessary();
}
private void performAppWidgetSizeCallbacksIfNecessary() {
View content = getContent();
if (!(content instanceof AppWidgetHostView)) return;
if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) {
mPerformAppWidgetSizeUpdateOnBootComplete = true;
return;
}
// TODO: there's no reason to force the AppWidgetHostView to catch duplicate size calls.
// We can do that even more cheaply here. It's not an issue right now since we're in the
// system process and hence no binder calls.
AppWidgetHostView awhv = (AppWidgetHostView) content;
float density = getResources().getDisplayMetrics().density;
int width = (int) (content.getMeasuredWidth() / density);
int height = (int) (content.getMeasuredHeight() / density);
awhv.updateAppWidgetSize(null, width, height, width, height, true);
}
void setOverScrollAmount(float r, boolean left) {
if (Float.compare(mOverScrollAmount, r) != 0) {
mOverScrollAmount = r;
mForegroundGradient = left ? mLeftToRightGradient : mRightToLeftGradient;
mForegroundAlpha = (int) Math.round((0.5f * r * 255));
// We bump up the alpha of the outline to hide the fact that the overlay is drawing
// over the rounded part of the frame.
float bgAlpha = Math.min(OUTLINE_ALPHA_MULTIPLIER + r * (1 - OUTLINE_ALPHA_MULTIPLIER),
1f);
setBackgroundAlpha(bgAlpha);
invalidate();
}
}
public void onActive(boolean isActive) {
// hook for subclasses
}
public boolean onUserInteraction(MotionEvent event) {
// hook for subclasses
return false;
}
public void onBouncerShowing(boolean showing) {
// hook for subclasses
}
public void setWorkerHandler(Handler workerHandler) {
mWorkerHandler = workerHandler;
}
public Handler getWorkerHandler() {
return mWorkerHandler;
}
}