diff options
author | Jorim Jaggi <jjaggi@google.com> | 2015-01-27 16:53:01 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2015-01-27 16:53:02 +0000 |
commit | 3cdb3b7d0da84be9d5dba73dc76245bd9f314854 (patch) | |
tree | 763517412fd5f1ab78c845636627a49d1ff3608d /core/java/com | |
parent | ca3e3b73b3db850dc6bdcd835bef27bcafa9e093 (diff) | |
parent | de47e3554d8e72509975d6ff475ede79e800743c (diff) | |
download | frameworks_base-3cdb3b7d0da84be9d5dba73dc76245bd9f314854.zip frameworks_base-3cdb3b7d0da84be9d5dba73dc76245bd9f314854.tar.gz frameworks_base-3cdb3b7d0da84be9d5dba73dc76245bd9f314854.tar.bz2 |
Merge "Revert "Remove dead code #11: Remove more unused classes""
Diffstat (limited to 'core/java/com')
8 files changed, 3318 insertions, 0 deletions
diff --git a/core/java/com/android/internal/widget/FaceUnlockView.java b/core/java/com/android/internal/widget/FaceUnlockView.java new file mode 100644 index 0000000..121e601 --- /dev/null +++ b/core/java/com/android/internal/widget/FaceUnlockView.java @@ -0,0 +1,67 @@ +/* + * 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.internal.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.RelativeLayout; + +public class FaceUnlockView extends RelativeLayout { + private static final String TAG = "FaceUnlockView"; + + public FaceUnlockView(Context context) { + this(context, null); + } + + public FaceUnlockView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private int resolveMeasured(int measureSpec, int desired) + { + int result = 0; + int specSize = MeasureSpec.getSize(measureSpec); + switch (MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.UNSPECIFIED: + result = desired; + break; + case MeasureSpec.AT_MOST: + result = Math.max(specSize, desired); + break; + case MeasureSpec.EXACTLY: + default: + result = specSize; + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int minimumWidth = getSuggestedMinimumWidth(); + final int minimumHeight = getSuggestedMinimumHeight(); + int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); + int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); + + final int chosenSize = Math.min(viewWidth, viewHeight); + final int newWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST); + final int newHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST); + + super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); + } +} diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java new file mode 100644 index 0000000..5f3c5f9 --- /dev/null +++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java @@ -0,0 +1,442 @@ +/* + * 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.internal.widget; + +import java.lang.Math; + +import com.android.internal.R; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.StateSet; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.widget.RemoteViews.RemoteView; + +/** + * A layout that switches between its children based on the requested layout height. + * Each child specifies its minimum and maximum valid height. Results are undefined + * if children specify overlapping ranges. A child may specify the maximum height + * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall. + * + * <p> + * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the + * layout parameters used by SizeAdaptiveLayout. + */ +@RemoteView +public class SizeAdaptiveLayout extends ViewGroup { + + private static final String TAG = "SizeAdaptiveLayout"; + private static final boolean DEBUG = false; + private static final boolean REPORT_BAD_BOUNDS = true; + private static final long CROSSFADE_TIME = 250; + + // TypedArray indices + private static final int MIN_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight; + private static final int MAX_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight; + + // view state + private View mActiveChild; + private View mLastActive; + + // animation state + private AnimatorSet mTransitionAnimation; + private AnimatorListener mAnimatorListener; + private ObjectAnimator mFadePanel; + private ObjectAnimator mFadeView; + private int mCanceledAnimationCount; + private View mEnteringView; + private View mLeavingView; + // View used to hide larger views under smaller ones to create a uniform crossfade + private View mModestyPanel; + private int mModestyPanelTop; + + public SizeAdaptiveLayout(Context context) { + this(context, null); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SizeAdaptiveLayout( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initialize(); + } + + private void initialize() { + mModestyPanel = new View(getContext()); + // If the SizeAdaptiveLayout has a solid background, use it as a transition hint. + Drawable background = getBackground(); + if (background instanceof StateListDrawable) { + StateListDrawable sld = (StateListDrawable) background; + sld.setState(StateSet.WILD_CARD); + background = sld.getCurrent(); + } + if (background instanceof ColorDrawable) { + mModestyPanel.setBackgroundDrawable(background); + } + SizeAdaptiveLayout.LayoutParams layout = + new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mModestyPanel.setLayoutParams(layout); + addView(mModestyPanel); + mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f); + mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f); + mAnimatorListener = new BringToFrontOnEnd(); + mTransitionAnimation = new AnimatorSet(); + mTransitionAnimation.play(mFadeView).with(mFadePanel); + mTransitionAnimation.setDuration(CROSSFADE_TIME); + mTransitionAnimation.addListener(mAnimatorListener); + } + + /** + * Visible for testing + * @hide + */ + public Animator getTransitionAnimation() { + return mTransitionAnimation; + } + + /** + * Visible for testing + * @hide + */ + public View getModestyPanel() { + return mModestyPanel; + } + + @Override + public void onAttachedToWindow() { + mLastActive = null; + // make sure all views start off invisible. + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setVisibility(View.GONE); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (DEBUG) Log.d(TAG, this + " measure spec: " + + MeasureSpec.toString(heightMeasureSpec)); + View model = selectActiveChild(heightMeasureSpec); + if (model == null) { + setMeasuredDimension(0, 0); + return; + } + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams(); + if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight); + measureChild(model, widthMeasureSpec, heightMeasureSpec); + int childHeight = model.getMeasuredHeight(); + int childWidth = model.getMeasuredHeight(); + int childState = combineMeasuredStates(0, model.getMeasuredState()); + if (DEBUG) Log.d(TAG, "measured child at: " + childHeight); + int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState); + int resolvedHeight = resolveSizeAndState(childHeight, heightMeasureSpec, childState); + if (DEBUG) Log.d(TAG, "resolved to: " + resolvedHeight); + int boundedHeight = clampSizeToBounds(resolvedHeight, model); + if (DEBUG) Log.d(TAG, "bounded to: " + boundedHeight); + setMeasuredDimension(resolvedWidth, boundedHeight); + } + + private int clampSizeToBounds(int measuredHeight, View child) { + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); + int heightIn = View.MEASURED_SIZE_MASK & measuredHeight; + int height = Math.max(heightIn, lp.minHeight); + if (lp.maxHeight != SizeAdaptiveLayout.LayoutParams.UNBOUNDED) { + height = Math.min(height, lp.maxHeight); + } + + if (REPORT_BAD_BOUNDS && heightIn != height) { + Log.d(TAG, this + "child view " + child + " " + + "measured out of bounds at " + heightIn +"px " + + "clamped to " + height + "px"); + } + + return height; + } + + //TODO extend to width and height + private View selectActiveChild(int heightMeasureSpec) { + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + View unboundedView = null; + View tallestView = null; + int tallestViewSize = 0; + View smallestView = null; + int smallestViewSize = Integer.MAX_VALUE; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != mModestyPanel) { + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); + if (DEBUG) Log.d(TAG, "looking at " + i + + " with min: " + lp.minHeight + + " max: " + lp.maxHeight); + if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED && + unboundedView == null) { + unboundedView = child; + } + if (lp.maxHeight > tallestViewSize) { + tallestViewSize = lp.maxHeight; + tallestView = child; + } + if (lp.minHeight < smallestViewSize) { + smallestViewSize = lp.minHeight; + smallestView = child; + } + if (heightMode != MeasureSpec.UNSPECIFIED && + heightSize >= lp.minHeight && heightSize <= lp.maxHeight) { + if (DEBUG) Log.d(TAG, " found exact match, finishing early"); + return child; + } + } + } + if (unboundedView != null) { + tallestView = unboundedView; + } + if (heightMode == MeasureSpec.UNSPECIFIED || heightSize > tallestViewSize) { + return tallestView; + } else { + return smallestView; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top)); + mLastActive = mActiveChild; + int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, + View.MeasureSpec.EXACTLY); + mActiveChild = selectActiveChild(measureSpec); + if (mActiveChild == null) return; + + mActiveChild.setVisibility(View.VISIBLE); + + if (mLastActive != mActiveChild && mLastActive != null) { + if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive + + " to: " + mActiveChild); + + mEnteringView = mActiveChild; + mLeavingView = mLastActive; + + mEnteringView.setAlpha(1f); + + mModestyPanel.setAlpha(1f); + mModestyPanel.bringToFront(); + mModestyPanelTop = mLeavingView.getHeight(); + mModestyPanel.setVisibility(View.VISIBLE); + // TODO: mModestyPanel background should be compatible with mLeavingView + + mLeavingView.bringToFront(); + + if (mTransitionAnimation.isRunning()) { + mTransitionAnimation.cancel(); + } + mFadeView.setTarget(mLeavingView); + mFadeView.setFloatValues(0f); + mFadePanel.setFloatValues(0f); + mTransitionAnimation.setupStartValues(); + mTransitionAnimation.start(); + } + final int childWidth = mActiveChild.getMeasuredWidth(); + final int childHeight = mActiveChild.getMeasuredHeight(); + // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive + mActiveChild.layout(0, 0, childWidth, childHeight); + + if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); + mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + if (DEBUG) Log.d(TAG, "generate layout from attrs"); + return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (DEBUG) Log.d(TAG, "generate default layout from viewgroup"); + return new SizeAdaptiveLayout.LayoutParams(p); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + if (DEBUG) Log.d(TAG, "generate default layout from null"); + return new SizeAdaptiveLayout.LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof SizeAdaptiveLayout.LayoutParams; + } + + /** + * Per-child layout information associated with ViewSizeAdaptiveLayout. + * + * TODO extend to width and height + * + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + + /** + * Indicates the minimum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int minHeight; + + /** + * Indicates the maximum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int maxHeight; + + /** + * Constant value for maxHeight that indicates there is not maximum height. + */ + public static final int UNBOUNDED = -1; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + if (DEBUG) { + Log.d(TAG, "construct layout from attrs"); + for (int i = 0; i < attrs.getAttributeCount(); i++) { + Log.d(TAG, " " + attrs.getAttributeName(i) + " = " + + attrs.getAttributeValue(i)); + } + } + TypedArray a = + c.obtainStyledAttributes(attrs, + R.styleable.SizeAdaptiveLayout_Layout); + + minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0); + if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight); + + try { + maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED); + if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight); + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e); + } + + a.recycle(); + } + + /** + * Creates a new set of layout parameters with the specified width, height + * and valid height bounds. + * + * @param width the width, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param minHeight the minimum height of this child + * @param maxHeight the maximum height of this child + * or {@link #UNBOUNDED} if the child can grow forever + */ + public LayoutParams(int width, int height, int minHeight, int maxHeight) { + super(width, height); + this.minHeight = minHeight; + this.maxHeight = maxHeight; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + this(width, height, UNBOUNDED, UNBOUNDED); + } + + /** + * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. + */ + public LayoutParams() { + this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + minHeight = UNBOUNDED; + maxHeight = UNBOUNDED; + } + + public String debug(String output) { + return output + "SizeAdaptiveLayout.LayoutParams={" + + ", max=" + maxHeight + + ", max=" + minHeight + "}"; + } + } + + class BringToFrontOnEnd implements AnimatorListener { + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceledAnimationCount == 0) { + mLeavingView.setVisibility(View.GONE); + mModestyPanel.setVisibility(View.GONE); + mEnteringView.bringToFront(); + mEnteringView = null; + mLeavingView = null; + } else { + mCanceledAnimationCount--; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceledAnimationCount++; + } + + @Override + public void onAnimationRepeat(Animator animation) { + if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen."); + assert(false); + } + + @Override + public void onAnimationStart(Animator animation) { + } + } +} diff --git a/core/java/com/android/internal/widget/WaveView.java b/core/java/com/android/internal/widget/WaveView.java new file mode 100644 index 0000000..9e7a649 --- /dev/null +++ b/core/java/com/android/internal/widget/WaveView.java @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2010 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.internal.widget; + +import java.util.ArrayList; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.media.AudioAttributes; +import android.os.UserHandle; +import android.os.Vibrator; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.R; + +/** + * A special widget containing a center and outer ring. Moving the center ring to the outer ring + * causes an event that can be caught by implementing OnTriggerListener. + */ +public class WaveView extends View implements ValueAnimator.AnimatorUpdateListener { + private static final String TAG = "WaveView"; + private static final boolean DBG = false; + private static final int WAVE_COUNT = 20; // default wave count + private static final long VIBRATE_SHORT = 20; // msec + private static final long VIBRATE_LONG = 20; // msec + + // Lock state machine states + private static final int STATE_RESET_LOCK = 0; + private static final int STATE_READY = 1; + private static final int STATE_START_ATTEMPT = 2; + private static final int STATE_ATTEMPTING = 3; + private static final int STATE_UNLOCK_ATTEMPT = 4; + private static final int STATE_UNLOCK_SUCCESS = 5; + + // Animation properties. + private static final long DURATION = 300; // duration of transitional animations + private static final long FINAL_DURATION = 200; // duration of final animations when unlocking + private static final long RING_DELAY = 1300; // when to start fading animated rings + private static final long FINAL_DELAY = 200; // delay for unlock success animation + private static final long SHORT_DELAY = 100; // for starting one animation after another. + private static final long WAVE_DURATION = 2000; // amount of time for way to expand/decay + private static final long RESET_TIMEOUT = 3000; // elapsed time of inactivity before we reset + private static final long DELAY_INCREMENT = 15; // increment per wave while tracking motion + private static final long DELAY_INCREMENT2 = 12; // increment per wave while not tracking + private static final long WAVE_DELAY = WAVE_DURATION / WAVE_COUNT; // initial propagation delay + + /** + * The scale by which to multiply the unlock handle width to compute the radius + * in which it can be grabbed when accessibility is disabled. + */ + private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED = 0.5f; + + /** + * The scale by which to multiply the unlock handle width to compute the radius + * in which it can be grabbed when accessibility is enabled (more generous). + */ + private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.0f; + + private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .build(); + + private Vibrator mVibrator; + private OnTriggerListener mOnTriggerListener; + private ArrayList<DrawableHolder> mDrawables = new ArrayList<DrawableHolder>(3); + private ArrayList<DrawableHolder> mLightWaves = new ArrayList<DrawableHolder>(WAVE_COUNT); + private boolean mFingerDown = false; + private float mRingRadius = 182.0f; // Radius of bitmap ring. Used to snap halo to it + private int mSnapRadius = 136; // minimum threshold for drag unlock + private int mWaveCount = WAVE_COUNT; // number of waves + private long mWaveTimerDelay = WAVE_DELAY; + private int mCurrentWave = 0; + private float mLockCenterX; // center of widget as dictated by widget size + private float mLockCenterY; + private float mMouseX; // current mouse position as of last touch event + private float mMouseY; + private DrawableHolder mUnlockRing; + private DrawableHolder mUnlockDefault; + private DrawableHolder mUnlockHalo; + private int mLockState = STATE_RESET_LOCK; + private int mGrabbedState = OnTriggerListener.NO_HANDLE; + private boolean mWavesRunning; + private boolean mFinishWaves; + + public WaveView(Context context) { + this(context, null); + } + + public WaveView(Context context, AttributeSet attrs) { + super(context, attrs); + + // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView); + // mOrientation = a.getInt(R.styleable.WaveView_orientation, HORIZONTAL); + // a.recycle(); + + initDrawables(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mLockCenterX = 0.5f * w; + mLockCenterY = 0.5f * h; + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + protected int getSuggestedMinimumWidth() { + // View should be large enough to contain the unlock ring + halo + return mUnlockRing.getWidth() + mUnlockHalo.getWidth(); + } + + @Override + protected int getSuggestedMinimumHeight() { + // View should be large enough to contain the unlock ring + halo + return mUnlockRing.getHeight() + mUnlockHalo.getHeight(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + int width; + int height; + + if (widthSpecMode == MeasureSpec.AT_MOST) { + width = Math.min(widthSpecSize, getSuggestedMinimumWidth()); + } else if (widthSpecMode == MeasureSpec.EXACTLY) { + width = widthSpecSize; + } else { + width = getSuggestedMinimumWidth(); + } + + if (heightSpecMode == MeasureSpec.AT_MOST) { + height = Math.min(heightSpecSize, getSuggestedMinimumWidth()); + } else if (heightSpecMode == MeasureSpec.EXACTLY) { + height = heightSpecSize; + } else { + height = getSuggestedMinimumHeight(); + } + + setMeasuredDimension(width, height); + } + + private void initDrawables() { + mUnlockRing = new DrawableHolder(createDrawable(R.drawable.unlock_ring)); + mUnlockRing.setX(mLockCenterX); + mUnlockRing.setY(mLockCenterY); + mUnlockRing.setScaleX(0.1f); + mUnlockRing.setScaleY(0.1f); + mUnlockRing.setAlpha(0.0f); + mDrawables.add(mUnlockRing); + + mUnlockDefault = new DrawableHolder(createDrawable(R.drawable.unlock_default)); + mUnlockDefault.setX(mLockCenterX); + mUnlockDefault.setY(mLockCenterY); + mUnlockDefault.setScaleX(0.1f); + mUnlockDefault.setScaleY(0.1f); + mUnlockDefault.setAlpha(0.0f); + mDrawables.add(mUnlockDefault); + + mUnlockHalo = new DrawableHolder(createDrawable(R.drawable.unlock_halo)); + mUnlockHalo.setX(mLockCenterX); + mUnlockHalo.setY(mLockCenterY); + mUnlockHalo.setScaleX(0.1f); + mUnlockHalo.setScaleY(0.1f); + mUnlockHalo.setAlpha(0.0f); + mDrawables.add(mUnlockHalo); + + BitmapDrawable wave = createDrawable(R.drawable.unlock_wave); + for (int i = 0; i < mWaveCount; i++) { + DrawableHolder holder = new DrawableHolder(wave); + mLightWaves.add(holder); + holder.setAlpha(0.0f); + } + } + + private void waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown) { + double distX = mouseX - mLockCenterX; + double distY = mouseY - mLockCenterY; + int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); + double touchA = Math.atan2(distX, distY); + float ringX = (float) (mLockCenterX + mRingRadius * Math.sin(touchA)); + float ringY = (float) (mLockCenterY + mRingRadius * Math.cos(touchA)); + + switch (mLockState) { + case STATE_RESET_LOCK: + if (DBG) Log.v(TAG, "State RESET_LOCK"); + mWaveTimerDelay = WAVE_DELAY; + for (int i = 0; i < mLightWaves.size(); i++) { + DrawableHolder holder = mLightWaves.get(i); + holder.addAnimTo(300, 0, "alpha", 0.0f, false); + } + for (int i = 0; i < mLightWaves.size(); i++) { + mLightWaves.get(i).startAnimations(this); + } + + mUnlockRing.addAnimTo(DURATION, 0, "x", mLockCenterX, true); + mUnlockRing.addAnimTo(DURATION, 0, "y", mLockCenterY, true); + mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 0.1f, true); + mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 0.1f, true); + mUnlockRing.addAnimTo(DURATION, 0, "alpha", 0.0f, true); + + mUnlockDefault.removeAnimationFor("x"); + mUnlockDefault.removeAnimationFor("y"); + mUnlockDefault.removeAnimationFor("scaleX"); + mUnlockDefault.removeAnimationFor("scaleY"); + mUnlockDefault.removeAnimationFor("alpha"); + mUnlockDefault.setX(mLockCenterX); + mUnlockDefault.setY(mLockCenterY); + mUnlockDefault.setScaleX(0.1f); + mUnlockDefault.setScaleY(0.1f); + mUnlockDefault.setAlpha(0.0f); + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); + + mUnlockHalo.removeAnimationFor("x"); + mUnlockHalo.removeAnimationFor("y"); + mUnlockHalo.removeAnimationFor("scaleX"); + mUnlockHalo.removeAnimationFor("scaleY"); + mUnlockHalo.removeAnimationFor("alpha"); + mUnlockHalo.setX(mLockCenterX); + mUnlockHalo.setY(mLockCenterY); + mUnlockHalo.setScaleX(0.1f); + mUnlockHalo.setScaleY(0.1f); + mUnlockHalo.setAlpha(0.0f); + mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "x", mLockCenterX, true); + mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "y", mLockCenterY, true); + mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); + mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); + mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); + + removeCallbacks(mLockTimerActions); + + mLockState = STATE_READY; + break; + + case STATE_READY: + if (DBG) Log.v(TAG, "State READY"); + mWaveTimerDelay = WAVE_DELAY; + break; + + case STATE_START_ATTEMPT: + if (DBG) Log.v(TAG, "State START_ATTEMPT"); + mUnlockDefault.removeAnimationFor("x"); + mUnlockDefault.removeAnimationFor("y"); + mUnlockDefault.removeAnimationFor("scaleX"); + mUnlockDefault.removeAnimationFor("scaleY"); + mUnlockDefault.removeAnimationFor("alpha"); + mUnlockDefault.setX(mLockCenterX + 182); + mUnlockDefault.setY(mLockCenterY); + mUnlockDefault.setScaleX(0.1f); + mUnlockDefault.setScaleY(0.1f); + mUnlockDefault.setAlpha(0.0f); + + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, false); + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, false); + mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, false); + + mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 1.0f, true); + mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 1.0f, true); + mUnlockRing.addAnimTo(DURATION, 0, "alpha", 1.0f, true); + + mLockState = STATE_ATTEMPTING; + break; + + case STATE_ATTEMPTING: + if (DBG) Log.v(TAG, "State ATTEMPTING (fingerDown = " + fingerDown + ")"); + if (dragDistance > mSnapRadius) { + mFinishWaves = true; // don't start any more waves. + if (fingerDown) { + mUnlockHalo.addAnimTo(0, 0, "x", ringX, true); + mUnlockHalo.addAnimTo(0, 0, "y", ringY, true); + mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); + mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); + mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); + } else { + if (DBG) Log.v(TAG, "up detected, moving to STATE_UNLOCK_ATTEMPT"); + mLockState = STATE_UNLOCK_ATTEMPT; + } + } else { + // If waves have stopped, we need to kick them off again... + if (!mWavesRunning) { + mWavesRunning = true; + mFinishWaves = false; + // mWaveTimerDelay = WAVE_DELAY; + postDelayed(mAddWaveAction, mWaveTimerDelay); + } + mUnlockHalo.addAnimTo(0, 0, "x", mouseX, true); + mUnlockHalo.addAnimTo(0, 0, "y", mouseY, true); + mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); + mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); + mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); + } + break; + + case STATE_UNLOCK_ATTEMPT: + if (DBG) Log.v(TAG, "State UNLOCK_ATTEMPT"); + if (dragDistance > mSnapRadius) { + for (int n = 0; n < mLightWaves.size(); n++) { + DrawableHolder wave = mLightWaves.get(n); + long delay = 1000L*(6 + n - mCurrentWave)/10L; + wave.addAnimTo(FINAL_DURATION, delay, "x", ringX, true); + wave.addAnimTo(FINAL_DURATION, delay, "y", ringY, true); + wave.addAnimTo(FINAL_DURATION, delay, "scaleX", 0.1f, true); + wave.addAnimTo(FINAL_DURATION, delay, "scaleY", 0.1f, true); + wave.addAnimTo(FINAL_DURATION, delay, "alpha", 0.0f, true); + } + for (int i = 0; i < mLightWaves.size(); i++) { + mLightWaves.get(i).startAnimations(this); + } + + mUnlockRing.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); + mUnlockRing.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); + mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleX", 0.1f, false); + mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleY", 0.1f, false); + mUnlockRing.addAnimTo(FINAL_DURATION, 0, "alpha", 0.0f, false); + + mUnlockRing.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); + + mUnlockDefault.removeAnimationFor("x"); + mUnlockDefault.removeAnimationFor("y"); + mUnlockDefault.removeAnimationFor("scaleX"); + mUnlockDefault.removeAnimationFor("scaleY"); + mUnlockDefault.removeAnimationFor("alpha"); + mUnlockDefault.setX(ringX); + mUnlockDefault.setY(ringY); + mUnlockDefault.setScaleX(0.1f); + mUnlockDefault.setScaleY(0.1f); + mUnlockDefault.setAlpha(0.0f); + + mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "x", ringX, true); + mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "y", ringY, true); + mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleX", 1.0f, true); + mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleY", 1.0f, true); + mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "alpha", 1.0f, true); + + mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); + mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); + mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); + + mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); + mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); + + mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); + mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); + mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); + + removeCallbacks(mLockTimerActions); + + postDelayed(mLockTimerActions, RESET_TIMEOUT); + + dispatchTriggerEvent(OnTriggerListener.CENTER_HANDLE); + mLockState = STATE_UNLOCK_SUCCESS; + } else { + mLockState = STATE_RESET_LOCK; + } + break; + + case STATE_UNLOCK_SUCCESS: + if (DBG) Log.v(TAG, "State UNLOCK_SUCCESS"); + removeCallbacks(mAddWaveAction); + break; + + default: + if (DBG) Log.v(TAG, "Unknown state " + mLockState); + break; + } + mUnlockDefault.startAnimations(this); + mUnlockHalo.startAnimations(this); + mUnlockRing.startAnimations(this); + } + + BitmapDrawable createDrawable(int resId) { + Resources res = getResources(); + Bitmap bitmap = BitmapFactory.decodeResource(res, resId); + return new BitmapDrawable(res, bitmap); + } + + @Override + protected void onDraw(Canvas canvas) { + waveUpdateFrame(mMouseX, mMouseY, mFingerDown); + for (int i = 0; i < mDrawables.size(); ++i) { + mDrawables.get(i).draw(canvas); + } + for (int i = 0; i < mLightWaves.size(); ++i) { + mLightWaves.get(i).draw(canvas); + } + } + + private final Runnable mLockTimerActions = new Runnable() { + public void run() { + if (DBG) Log.v(TAG, "LockTimerActions"); + // reset lock after inactivity + if (mLockState == STATE_ATTEMPTING) { + if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK"); + mLockState = STATE_RESET_LOCK; + } + // for prototype, reset after successful unlock + if (mLockState == STATE_UNLOCK_SUCCESS) { + if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK after success"); + mLockState = STATE_RESET_LOCK; + } + invalidate(); + } + }; + + private final Runnable mAddWaveAction = new Runnable() { + public void run() { + double distX = mMouseX - mLockCenterX; + double distY = mMouseY - mLockCenterY; + int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); + if (mLockState == STATE_ATTEMPTING && dragDistance < mSnapRadius + && mWaveTimerDelay >= WAVE_DELAY) { + mWaveTimerDelay = Math.min(WAVE_DURATION, mWaveTimerDelay + DELAY_INCREMENT); + + DrawableHolder wave = mLightWaves.get(mCurrentWave); + wave.setAlpha(0.0f); + wave.setScaleX(0.2f); + wave.setScaleY(0.2f); + wave.setX(mMouseX); + wave.setY(mMouseY); + + wave.addAnimTo(WAVE_DURATION, 0, "x", mLockCenterX, true); + wave.addAnimTo(WAVE_DURATION, 0, "y", mLockCenterY, true); + wave.addAnimTo(WAVE_DURATION*2/3, 0, "alpha", 1.0f, true); + wave.addAnimTo(WAVE_DURATION, 0, "scaleX", 1.0f, true); + wave.addAnimTo(WAVE_DURATION, 0, "scaleY", 1.0f, true); + + wave.addAnimTo(1000, RING_DELAY, "alpha", 0.0f, false); + wave.startAnimations(WaveView.this); + + mCurrentWave = (mCurrentWave+1) % mWaveCount; + if (DBG) Log.v(TAG, "WaveTimerDelay: start new wave in " + mWaveTimerDelay); + } else { + mWaveTimerDelay += DELAY_INCREMENT2; + } + if (mFinishWaves) { + // sentinel used to restart the waves after they've stopped + mWavesRunning = false; + } else { + postDelayed(mAddWaveAction, mWaveTimerDelay); + } + } + }; + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { + final int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + event.setAction(MotionEvent.ACTION_DOWN); + break; + case MotionEvent.ACTION_HOVER_MOVE: + event.setAction(MotionEvent.ACTION_MOVE); + break; + case MotionEvent.ACTION_HOVER_EXIT: + event.setAction(MotionEvent.ACTION_UP); + break; + } + onTouchEvent(event); + event.setAction(action); + } + return super.onHoverEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getAction(); + mMouseX = event.getX(); + mMouseY = event.getY(); + boolean handled = false; + switch (action) { + case MotionEvent.ACTION_DOWN: + removeCallbacks(mLockTimerActions); + mFingerDown = true; + tryTransitionToStartAttemptState(event); + handled = true; + break; + + case MotionEvent.ACTION_MOVE: + tryTransitionToStartAttemptState(event); + handled = true; + break; + + case MotionEvent.ACTION_UP: + if (DBG) Log.v(TAG, "ACTION_UP"); + mFingerDown = false; + postDelayed(mLockTimerActions, RESET_TIMEOUT); + setGrabbedState(OnTriggerListener.NO_HANDLE); + // Normally the state machine is driven by user interaction causing redraws. + // However, when there's no more user interaction and no running animations, + // the state machine stops advancing because onDraw() never gets called. + // The following ensures we advance to the next state in this case, + // either STATE_UNLOCK_ATTEMPT or STATE_RESET_LOCK. + waveUpdateFrame(mMouseX, mMouseY, mFingerDown); + handled = true; + break; + + case MotionEvent.ACTION_CANCEL: + mFingerDown = false; + handled = true; + break; + } + invalidate(); + return handled ? true : super.onTouchEvent(event); + } + + /** + * Tries to transition to start attempt state. + * + * @param event A motion event. + */ + private void tryTransitionToStartAttemptState(MotionEvent event) { + final float dx = event.getX() - mUnlockHalo.getX(); + final float dy = event.getY() - mUnlockHalo.getY(); + float dist = (float) Math.hypot(dx, dy); + if (dist <= getScaledGrabHandleRadius()) { + setGrabbedState(OnTriggerListener.CENTER_HANDLE); + if (mLockState == STATE_READY) { + mLockState = STATE_START_ATTEMPT; + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + announceUnlockHandle(); + } + } + } + } + + /** + * @return The radius in which the handle is grabbed scaled based on + * whether accessibility is enabled. + */ + private float getScaledGrabHandleRadius() { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mUnlockHalo.getWidth(); + } else { + return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED * mUnlockHalo.getWidth(); + } + } + + /** + * Announces the unlock handle if accessibility is enabled. + */ + private void announceUnlockHandle() { + setContentDescription(mContext.getString(R.string.description_target_unlock_tablet)); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + setContentDescription(null); + } + + /** + * Triggers haptic feedback. + */ + private synchronized void vibrate(long duration) { + final boolean hapticEnabled = Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, + UserHandle.USER_CURRENT) != 0; + if (hapticEnabled) { + if (mVibrator == null) { + mVibrator = (android.os.Vibrator) getContext() + .getSystemService(Context.VIBRATOR_SERVICE); + } + mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES); + } + } + + /** + * Registers a callback to be invoked when the user triggers an event. + * + * @param listener the OnDialTriggerListener to attach to this view + */ + public void setOnTriggerListener(OnTriggerListener listener) { + mOnTriggerListener = listener; + } + + /** + * Dispatches a trigger event to listener. Ignored if a listener is not set. + * @param whichHandle the handle that triggered the event. + */ + private void dispatchTriggerEvent(int whichHandle) { + vibrate(VIBRATE_LONG); + if (mOnTriggerListener != null) { + mOnTriggerListener.onTrigger(this, whichHandle); + } + } + + /** + * Sets the current grabbed state, and dispatches a grabbed state change + * event to our listener. + */ + private void setGrabbedState(int newState) { + if (newState != mGrabbedState) { + mGrabbedState = newState; + if (mOnTriggerListener != null) { + mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); + } + } + } + + public interface OnTriggerListener { + /** + * Sent when the user releases the handle. + */ + public static final int NO_HANDLE = 0; + + /** + * Sent when the user grabs the center handle + */ + public static final int CENTER_HANDLE = 10; + + /** + * Called when the user drags the center ring beyond a threshold. + */ + void onTrigger(View v, int whichHandle); + + /** + * Called when the "grabbed state" changes (i.e. when the user either grabs or releases + * one of the handles.) + * + * @param v the view that was triggered + * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #CENTER_HANDLE}, + */ + void onGrabbedStateChange(View v, int grabbedState); + } + + public void onAnimationUpdate(ValueAnimator animation) { + invalidate(); + } + + public void reset() { + if (DBG) Log.v(TAG, "reset() : resets state to STATE_RESET_LOCK"); + mLockState = STATE_RESET_LOCK; + invalidate(); + } +} diff --git a/core/java/com/android/internal/widget/multiwaveview/Ease.java b/core/java/com/android/internal/widget/multiwaveview/Ease.java new file mode 100644 index 0000000..7f90c44 --- /dev/null +++ b/core/java/com/android/internal/widget/multiwaveview/Ease.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 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.internal.widget.multiwaveview; + +import android.animation.TimeInterpolator; + +class Ease { + private static final float DOMAIN = 1.0f; + private static final float DURATION = 1.0f; + private static final float START = 0.0f; + + static class Linear { + public static final TimeInterpolator easeNone = new TimeInterpolator() { + public float getInterpolation(float input) { + return input; + } + }; + } + + static class Cubic { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN*(input/=DURATION)*input*input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input/=DURATION/2) < 1.0f) ? + (DOMAIN/2*input*input*input + START) + : (DOMAIN/2*((input-=2)*input*input + 2) + START); + } + }; + } + + static class Quad { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation (float input) { + return DOMAIN*(input/=DURATION)*input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN *(input/=DURATION)*(input-2) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input/=DURATION/2) < 1) ? + (DOMAIN/2*input*input + START) + : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START); + } + }; + } + + static class Quart { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN*(input/=DURATION)*input*input*input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input/=DURATION/2) < 1) ? + (DOMAIN/2*input*input*input*input + START) + : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START); + } + }; + } + + static class Quint { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN*(input/=DURATION)*input*input*input*input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input/=DURATION/2) < 1) ? + (DOMAIN/2*input*input*input*input*input + START) + : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START); + } + }; + } + + static class Sine { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START; + } + }; + } + +} diff --git a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java new file mode 100644 index 0000000..11ac19e --- /dev/null +++ b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java @@ -0,0 +1,1383 @@ +/* + * 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.internal.widget.multiwaveview; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.media.AudioAttributes; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.Vibrator; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.R; + +import java.util.ArrayList; + +/** + * A re-usable widget containing a center, outer ring and wave animation. + */ +public class GlowPadView extends View { + private static final String TAG = "GlowPadView"; + private static final boolean DEBUG = false; + + // Wave state machine + private static final int STATE_IDLE = 0; + private static final int STATE_START = 1; + private static final int STATE_FIRST_TOUCH = 2; + private static final int STATE_TRACKING = 3; + private static final int STATE_SNAP = 4; + private static final int STATE_FINISH = 5; + + // Animation properties. + private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it + + public interface OnTriggerListener { + int NO_HANDLE = 0; + int CENTER_HANDLE = 1; + public void onGrabbed(View v, int handle); + public void onReleased(View v, int handle); + public void onTrigger(View v, int target); + public void onGrabbedStateChange(View v, int handle); + public void onFinishFinalAnimation(); + } + + // Tuneable parameters for animation + private static final int WAVE_ANIMATION_DURATION = 1000; + private static final int RETURN_TO_HOME_DELAY = 1200; + private static final int RETURN_TO_HOME_DURATION = 200; + private static final int HIDE_ANIMATION_DELAY = 200; + private static final int HIDE_ANIMATION_DURATION = 200; + private static final int SHOW_ANIMATION_DURATION = 200; + private static final int SHOW_ANIMATION_DELAY = 50; + private static final int INITIAL_SHOW_HANDLE_DURATION = 200; + private static final int REVEAL_GLOW_DELAY = 0; + private static final int REVEAL_GLOW_DURATION = 0; + + private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; + private static final float TARGET_SCALE_EXPANDED = 1.0f; + private static final float TARGET_SCALE_COLLAPSED = 0.8f; + private static final float RING_SCALE_EXPANDED = 1.0f; + private static final float RING_SCALE_COLLAPSED = 0.5f; + + private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .build(); + + private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); + private AnimationBundle mWaveAnimations = new AnimationBundle(); + private AnimationBundle mTargetAnimations = new AnimationBundle(); + private AnimationBundle mGlowAnimations = new AnimationBundle(); + private ArrayList<String> mTargetDescriptions; + private ArrayList<String> mDirectionDescriptions; + private OnTriggerListener mOnTriggerListener; + private TargetDrawable mHandleDrawable; + private TargetDrawable mOuterRing; + private Vibrator mVibrator; + + private int mFeedbackCount = 3; + private int mVibrationDuration = 0; + private int mGrabbedState; + private int mActiveTarget = -1; + private float mGlowRadius; + private float mWaveCenterX; + private float mWaveCenterY; + private int mMaxTargetHeight; + private int mMaxTargetWidth; + private float mRingScaleFactor = 1f; + private boolean mAllowScaling; + + private float mOuterRadius = 0.0f; + private float mSnapMargin = 0.0f; + private float mFirstItemOffset = 0.0f; + private boolean mMagneticTargets = false; + private boolean mDragging; + private int mNewTargetResources; + + private class AnimationBundle extends ArrayList<Tweener> { + private static final long serialVersionUID = 0xA84D78726F127468L; + private boolean mSuspended; + + public void start() { + if (mSuspended) return; // ignore attempts to start animations + final int count = size(); + for (int i = 0; i < count; i++) { + Tweener anim = get(i); + anim.animator.start(); + } + } + + public void cancel() { + final int count = size(); + for (int i = 0; i < count; i++) { + Tweener anim = get(i); + anim.animator.cancel(); + } + clear(); + } + + public void stop() { + final int count = size(); + for (int i = 0; i < count; i++) { + Tweener anim = get(i); + anim.animator.end(); + } + clear(); + } + + public void setSuspended(boolean suspend) { + mSuspended = suspend; + } + }; + + private AnimatorListener mResetListener = new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); + dispatchOnFinishFinalAnimation(); + } + }; + + private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + ping(); + switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); + dispatchOnFinishFinalAnimation(); + } + }; + + private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + invalidate(); + } + }; + + private boolean mAnimatingTargets; + private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + if (mNewTargetResources != 0) { + internalSetTargetResources(mNewTargetResources); + mNewTargetResources = 0; + hideTargets(false, false); + } + mAnimatingTargets = false; + } + }; + private int mTargetResourceId; + private int mTargetDescriptionsResourceId; + private int mDirectionDescriptionsResourceId; + private boolean mAlwaysTrackFinger; + private int mHorizontalInset; + private int mVerticalInset; + private int mGravity = Gravity.TOP; + private boolean mInitialLayout = true; + private Tweener mBackgroundAnimator; + private PointCloud mPointCloud; + private float mInnerRadius; + private int mPointerId; + + public GlowPadView(Context context) { + this(context, null); + } + + public GlowPadView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView); + mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius); + mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius); + mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); + mFirstItemOffset = (float) Math.toRadians( + a.getFloat(R.styleable.GlowPadView_firstItemOffset, + (float) Math.toDegrees(mFirstItemOffset))); + mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, + mVibrationDuration); + mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, + mFeedbackCount); + mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false); + TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); + mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0); + mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); + mOuterRing = new TargetDrawable(res, + getResourceId(a, R.styleable.GlowPadView_outerRingDrawable)); + + mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false); + mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets); + + int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); + Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null; + mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); + + mPointCloud = new PointCloud(pointDrawable); + mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); + mPointCloud.glowManager.setRadius(mGlowRadius); + + TypedValue outValue = new TypedValue(); + + // Read array of target drawables + if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { + internalSetTargetResources(outValue.resourceId); + } + if (mTargetDrawables == null || mTargetDrawables.size() == 0) { + throw new IllegalStateException("Must specify at least one target drawable"); + } + + // Read array of target descriptions + if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) { + final int resourceId = outValue.resourceId; + if (resourceId == 0) { + throw new IllegalStateException("Must specify target descriptions"); + } + setTargetDescriptionsResourceId(resourceId); + } + + // Read array of direction descriptions + if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) { + final int resourceId = outValue.resourceId; + if (resourceId == 0) { + throw new IllegalStateException("Must specify direction descriptions"); + } + setDirectionDescriptionsResourceId(resourceId); + } + + mGravity = a.getInt(R.styleable.GlowPadView_gravity, Gravity.TOP); + + a.recycle(); + + setVibrateEnabled(mVibrationDuration > 0); + + assignDefaultsIfNeeded(); + } + + private int getResourceId(TypedArray a, int id) { + TypedValue tv = a.peekValue(id); + return tv == null ? 0 : tv.resourceId; + } + + private void dump() { + Log.v(TAG, "Outer Radius = " + mOuterRadius); + Log.v(TAG, "SnapMargin = " + mSnapMargin); + Log.v(TAG, "FeedbackCount = " + mFeedbackCount); + Log.v(TAG, "VibrationDuration = " + mVibrationDuration); + Log.v(TAG, "GlowRadius = " + mGlowRadius); + Log.v(TAG, "WaveCenterX = " + mWaveCenterX); + Log.v(TAG, "WaveCenterY = " + mWaveCenterY); + } + + public void suspendAnimations() { + mWaveAnimations.setSuspended(true); + mTargetAnimations.setSuspended(true); + mGlowAnimations.setSuspended(true); + } + + public void resumeAnimations() { + mWaveAnimations.setSuspended(false); + mTargetAnimations.setSuspended(false); + mGlowAnimations.setSuspended(false); + mWaveAnimations.start(); + mTargetAnimations.start(); + mGlowAnimations.start(); + } + + @Override + protected int getSuggestedMinimumWidth() { + // View should be large enough to contain the background + handle and + // target drawable on either edge. + return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); + } + + @Override + protected int getSuggestedMinimumHeight() { + // View should be large enough to contain the unlock ring + target and + // target drawable on either edge + return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); + } + + /** + * This gets the suggested width accounting for the ring's scale factor. + */ + protected int getScaledSuggestedMinimumWidth() { + return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + + mMaxTargetWidth); + } + + /** + * This gets the suggested height accounting for the ring's scale factor. + */ + protected int getScaledSuggestedMinimumHeight() { + return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + + mMaxTargetHeight); + } + + private int resolveMeasured(int measureSpec, int desired) + { + int result = 0; + int specSize = MeasureSpec.getSize(measureSpec); + switch (MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.UNSPECIFIED: + result = desired; + break; + case MeasureSpec.AT_MOST: + result = Math.min(specSize, desired); + break; + case MeasureSpec.EXACTLY: + default: + result = specSize; + } + return result; + } + + private void switchToState(int state, float x, float y) { + switch (state) { + case STATE_IDLE: + deactivateTargets(); + hideGlow(0, 0, 0.0f, null); + startBackgroundAnimation(0, 0.0f); + mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); + mHandleDrawable.setAlpha(1.0f); + break; + + case STATE_START: + startBackgroundAnimation(0, 0.0f); + break; + + case STATE_FIRST_TOUCH: + mHandleDrawable.setAlpha(0.0f); + deactivateTargets(); + showTargets(true); + startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); + setGrabbedState(OnTriggerListener.CENTER_HANDLE); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + announceTargets(); + } + break; + + case STATE_TRACKING: + mHandleDrawable.setAlpha(0.0f); + showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null); + break; + + case STATE_SNAP: + // TODO: Add transition states (see list_selector_background_transition.xml) + mHandleDrawable.setAlpha(0.0f); + showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null); + break; + + case STATE_FINISH: + doFinish(); + break; + } + } + + private void showGlow(int duration, int delay, float finalAlpha, + AnimatorListener finishListener) { + mGlowAnimations.cancel(); + mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, + "ease", Ease.Cubic.easeIn, + "delay", delay, + "alpha", finalAlpha, + "onUpdate", mUpdateListener, + "onComplete", finishListener)); + mGlowAnimations.start(); + } + + private void hideGlow(int duration, int delay, float finalAlpha, + AnimatorListener finishListener) { + mGlowAnimations.cancel(); + mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, + "ease", Ease.Quart.easeOut, + "delay", delay, + "alpha", finalAlpha, + "x", 0.0f, + "y", 0.0f, + "onUpdate", mUpdateListener, + "onComplete", finishListener)); + mGlowAnimations.start(); + } + + private void deactivateTargets() { + final int count = mTargetDrawables.size(); + for (int i = 0; i < count; i++) { + TargetDrawable target = mTargetDrawables.get(i); + target.setState(TargetDrawable.STATE_INACTIVE); + } + mActiveTarget = -1; + } + + /** + * Dispatches a trigger event to listener. Ignored if a listener is not set. + * @param whichTarget the target that was triggered. + */ + private void dispatchTriggerEvent(int whichTarget) { + vibrate(); + if (mOnTriggerListener != null) { + mOnTriggerListener.onTrigger(this, whichTarget); + } + } + + private void dispatchOnFinishFinalAnimation() { + if (mOnTriggerListener != null) { + mOnTriggerListener.onFinishFinalAnimation(); + } + } + + private void doFinish() { + final int activeTarget = mActiveTarget; + final boolean targetHit = activeTarget != -1; + + if (targetHit) { + if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); + + highlightSelected(activeTarget); + + // Inform listener of any active targets. Typically only one will be active. + hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); + dispatchTriggerEvent(activeTarget); + if (!mAlwaysTrackFinger) { + // Force ring and targets to finish animation to final expanded state + mTargetAnimations.stop(); + } + } else { + // Animate handle back to the center based on current state. + hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing); + hideTargets(true, false); + } + + setGrabbedState(OnTriggerListener.NO_HANDLE); + } + + private void highlightSelected(int activeTarget) { + // Highlight the given target and fade others + mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); + hideUnselected(activeTarget); + } + + private void hideUnselected(int active) { + for (int i = 0; i < mTargetDrawables.size(); i++) { + if (i != active) { + mTargetDrawables.get(i).setAlpha(0.0f); + } + } + } + + private void hideTargets(boolean animate, boolean expanded) { + mTargetAnimations.cancel(); + // Note: these animations should complete at the same time so that we can swap out + // the target assets asynchronously from the setTargetResources() call. + mAnimatingTargets = animate; + final int duration = animate ? HIDE_ANIMATION_DURATION : 0; + final int delay = animate ? HIDE_ANIMATION_DELAY : 0; + + final float targetScale = expanded ? + TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; + final int length = mTargetDrawables.size(); + final TimeInterpolator interpolator = Ease.Cubic.easeOut; + for (int i = 0; i < length; i++) { + TargetDrawable target = mTargetDrawables.get(i); + target.setState(TargetDrawable.STATE_INACTIVE); + mTargetAnimations.add(Tweener.to(target, duration, + "ease", interpolator, + "alpha", 0.0f, + "scaleX", targetScale, + "scaleY", targetScale, + "delay", delay, + "onUpdate", mUpdateListener)); + } + + float ringScaleTarget = expanded ? + RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; + ringScaleTarget *= mRingScaleFactor; + mTargetAnimations.add(Tweener.to(mOuterRing, duration, + "ease", interpolator, + "alpha", 0.0f, + "scaleX", ringScaleTarget, + "scaleY", ringScaleTarget, + "delay", delay, + "onUpdate", mUpdateListener, + "onComplete", mTargetUpdateListener)); + + mTargetAnimations.start(); + } + + private void showTargets(boolean animate) { + mTargetAnimations.stop(); + mAnimatingTargets = animate; + final int delay = animate ? SHOW_ANIMATION_DELAY : 0; + final int duration = animate ? SHOW_ANIMATION_DURATION : 0; + final int length = mTargetDrawables.size(); + for (int i = 0; i < length; i++) { + TargetDrawable target = mTargetDrawables.get(i); + target.setState(TargetDrawable.STATE_INACTIVE); + mTargetAnimations.add(Tweener.to(target, duration, + "ease", Ease.Cubic.easeOut, + "alpha", 1.0f, + "scaleX", 1.0f, + "scaleY", 1.0f, + "delay", delay, + "onUpdate", mUpdateListener)); + } + + float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED; + mTargetAnimations.add(Tweener.to(mOuterRing, duration, + "ease", Ease.Cubic.easeOut, + "alpha", 1.0f, + "scaleX", ringScale, + "scaleY", ringScale, + "delay", delay, + "onUpdate", mUpdateListener, + "onComplete", mTargetUpdateListener)); + + mTargetAnimations.start(); + } + + private void vibrate() { + final boolean hapticEnabled = Settings.System.getIntForUser( + mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, + UserHandle.USER_CURRENT) != 0; + if (mVibrator != null && hapticEnabled) { + mVibrator.vibrate(mVibrationDuration, VIBRATION_ATTRIBUTES); + } + } + + private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { + Resources res = getContext().getResources(); + TypedArray array = res.obtainTypedArray(resourceId); + final int count = array.length(); + ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); + for (int i = 0; i < count; i++) { + TypedValue value = array.peekValue(i); + TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); + drawables.add(target); + } + array.recycle(); + return drawables; + } + + private void internalSetTargetResources(int resourceId) { + final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId); + mTargetDrawables = targets; + mTargetResourceId = resourceId; + + int maxWidth = mHandleDrawable.getWidth(); + int maxHeight = mHandleDrawable.getHeight(); + final int count = targets.size(); + for (int i = 0; i < count; i++) { + TargetDrawable target = targets.get(i); + maxWidth = Math.max(maxWidth, target.getWidth()); + maxHeight = Math.max(maxHeight, target.getHeight()); + } + if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { + mMaxTargetWidth = maxWidth; + mMaxTargetHeight = maxHeight; + requestLayout(); // required to resize layout and call updateTargetPositions() + } else { + updateTargetPositions(mWaveCenterX, mWaveCenterY); + updatePointCloudPosition(mWaveCenterX, mWaveCenterY); + } + } + + /** + * Loads an array of drawables from the given resourceId. + * + * @param resourceId + */ + public void setTargetResources(int resourceId) { + if (mAnimatingTargets) { + // postpone this change until we return to the initial state + mNewTargetResources = resourceId; + } else { + internalSetTargetResources(resourceId); + } + } + + public int getTargetResourceId() { + return mTargetResourceId; + } + + /** + * Sets the resource id specifying the target descriptions for accessibility. + * + * @param resourceId The resource id. + */ + public void setTargetDescriptionsResourceId(int resourceId) { + mTargetDescriptionsResourceId = resourceId; + if (mTargetDescriptions != null) { + mTargetDescriptions.clear(); + } + } + + /** + * Gets the resource id specifying the target descriptions for accessibility. + * + * @return The resource id. + */ + public int getTargetDescriptionsResourceId() { + return mTargetDescriptionsResourceId; + } + + /** + * Sets the resource id specifying the target direction descriptions for accessibility. + * + * @param resourceId The resource id. + */ + public void setDirectionDescriptionsResourceId(int resourceId) { + mDirectionDescriptionsResourceId = resourceId; + if (mDirectionDescriptions != null) { + mDirectionDescriptions.clear(); + } + } + + /** + * Gets the resource id specifying the target direction descriptions. + * + * @return The resource id. + */ + public int getDirectionDescriptionsResourceId() { + return mDirectionDescriptionsResourceId; + } + + /** + * Enable or disable vibrate on touch. + * + * @param enabled + */ + public void setVibrateEnabled(boolean enabled) { + if (enabled && mVibrator == null) { + mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); + } else { + mVibrator = null; + } + } + + /** + * Starts wave animation. + * + */ + public void ping() { + if (mFeedbackCount > 0) { + boolean doWaveAnimation = true; + final AnimationBundle waveAnimations = mWaveAnimations; + + // Don't do a wave if there's already one in progress + if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { + long t = waveAnimations.get(0).animator.getCurrentPlayTime(); + if (t < WAVE_ANIMATION_DURATION/2) { + doWaveAnimation = false; + } + } + + if (doWaveAnimation) { + startWaveAnimation(); + } + } + } + + private void stopAndHideWaveAnimation() { + mWaveAnimations.cancel(); + mPointCloud.waveManager.setAlpha(0.0f); + } + + private void startWaveAnimation() { + mWaveAnimations.cancel(); + mPointCloud.waveManager.setAlpha(1.0f); + mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); + mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, + "ease", Ease.Quad.easeOut, + "delay", 0, + "radius", 2.0f * mOuterRadius, + "onUpdate", mUpdateListener, + "onComplete", + new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + mPointCloud.waveManager.setRadius(0.0f); + mPointCloud.waveManager.setAlpha(0.0f); + } + })); + mWaveAnimations.start(); + } + + /** + * Resets the widget to default state and cancels all animation. If animate is 'true', will + * animate objects into place. Otherwise, objects will snap back to place. + * + * @param animate + */ + public void reset(boolean animate) { + mGlowAnimations.stop(); + mTargetAnimations.stop(); + startBackgroundAnimation(0, 0.0f); + stopAndHideWaveAnimation(); + hideTargets(animate, false); + hideGlow(0, 0, 0.0f, null); + Tweener.reset(); + } + + private void startBackgroundAnimation(int duration, float alpha) { + final Drawable background = getBackground(); + if (mAlwaysTrackFinger && background != null) { + if (mBackgroundAnimator != null) { + mBackgroundAnimator.animator.cancel(); + } + mBackgroundAnimator = Tweener.to(background, duration, + "ease", Ease.Cubic.easeIn, + "alpha", (int)(255.0f * alpha), + "delay", SHOW_ANIMATION_DELAY); + mBackgroundAnimator.animator.start(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getActionMasked(); + boolean handled = false; + switch (action) { + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_DOWN: + if (DEBUG) Log.v(TAG, "*** DOWN ***"); + handleDown(event); + handleMove(event); + handled = true; + break; + + case MotionEvent.ACTION_MOVE: + if (DEBUG) Log.v(TAG, "*** MOVE ***"); + handleMove(event); + handled = true; + break; + + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + if (DEBUG) Log.v(TAG, "*** UP ***"); + handleMove(event); + handleUp(event); + handled = true; + break; + + case MotionEvent.ACTION_CANCEL: + if (DEBUG) Log.v(TAG, "*** CANCEL ***"); + handleMove(event); + handleCancel(event); + handled = true; + break; + + } + invalidate(); + return handled ? true : super.onTouchEvent(event); + } + + private void updateGlowPosition(float x, float y) { + float dx = x - mOuterRing.getX(); + float dy = y - mOuterRing.getY(); + dx *= 1f / mRingScaleFactor; + dy *= 1f / mRingScaleFactor; + mPointCloud.glowManager.setX(mOuterRing.getX() + dx); + mPointCloud.glowManager.setY(mOuterRing.getY() + dy); + } + + private void handleDown(MotionEvent event) { + int actionIndex = event.getActionIndex(); + float eventX = event.getX(actionIndex); + float eventY = event.getY(actionIndex); + switchToState(STATE_START, eventX, eventY); + if (!trySwitchToFirstTouchState(eventX, eventY)) { + mDragging = false; + } else { + mPointerId = event.getPointerId(actionIndex); + updateGlowPosition(eventX, eventY); + } + } + + private void handleUp(MotionEvent event) { + if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); + int actionIndex = event.getActionIndex(); + if (event.getPointerId(actionIndex) == mPointerId) { + switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); + } + } + + private void handleCancel(MotionEvent event) { + if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); + + // Drop the active target if canceled. + mActiveTarget = -1; + + int actionIndex = event.findPointerIndex(mPointerId); + actionIndex = actionIndex == -1 ? 0 : actionIndex; + switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); + } + + private void handleMove(MotionEvent event) { + int activeTarget = -1; + final int historySize = event.getHistorySize(); + ArrayList<TargetDrawable> targets = mTargetDrawables; + int ntargets = targets.size(); + float x = 0.0f; + float y = 0.0f; + float activeAngle = 0.0f; + int actionIndex = event.findPointerIndex(mPointerId); + + if (actionIndex == -1) { + return; // no data for this pointer + } + + for (int k = 0; k < historySize + 1; k++) { + float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) + : event.getX(actionIndex); + float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) + : event.getY(actionIndex); + // tx and ty are relative to wave center + float tx = eventX - mWaveCenterX; + float ty = eventY - mWaveCenterY; + float touchRadius = (float) Math.hypot(tx, ty); + final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; + float limitX = tx * scale; + float limitY = ty * scale; + double angleRad = Math.atan2(-ty, tx); + + if (!mDragging) { + trySwitchToFirstTouchState(eventX, eventY); + } + + if (mDragging) { + // For multiple targets, snap to the one that matches + final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin; + final float snapDistance2 = snapRadius * snapRadius; + // Find first target in range + for (int i = 0; i < ntargets; i++) { + TargetDrawable target = targets.get(i); + + double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets; + double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets; + if (target.isEnabled()) { + boolean angleMatches = + (angleRad > targetMinRad && angleRad <= targetMaxRad) || + (angleRad + 2 * Math.PI > targetMinRad && + angleRad + 2 * Math.PI <= targetMaxRad) || + (angleRad - 2 * Math.PI > targetMinRad && + angleRad - 2 * Math.PI <= targetMaxRad); + if (angleMatches && (dist2(tx, ty) > snapDistance2)) { + activeTarget = i; + activeAngle = (float) -angleRad; + } + } + } + } + x = limitX; + y = limitY; + } + + if (!mDragging) { + return; + } + + if (activeTarget != -1) { + switchToState(STATE_SNAP, x,y); + updateGlowPosition(x, y); + } else { + switchToState(STATE_TRACKING, x, y); + updateGlowPosition(x, y); + } + + if (mActiveTarget != activeTarget) { + // Defocus the old target + if (mActiveTarget != -1) { + TargetDrawable target = targets.get(mActiveTarget); + if (target.hasState(TargetDrawable.STATE_FOCUSED)) { + target.setState(TargetDrawable.STATE_INACTIVE); + } + if (mMagneticTargets) { + updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY); + } + } + // Focus the new target + if (activeTarget != -1) { + TargetDrawable target = targets.get(activeTarget); + if (target.hasState(TargetDrawable.STATE_FOCUSED)) { + target.setState(TargetDrawable.STATE_FOCUSED); + } + if (mMagneticTargets) { + updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle); + } + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + String targetContentDescription = getTargetDescription(activeTarget); + announceForAccessibility(targetContentDescription); + } + } + } + mActiveTarget = activeTarget; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { + final int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + event.setAction(MotionEvent.ACTION_DOWN); + break; + case MotionEvent.ACTION_HOVER_MOVE: + event.setAction(MotionEvent.ACTION_MOVE); + break; + case MotionEvent.ACTION_HOVER_EXIT: + event.setAction(MotionEvent.ACTION_UP); + break; + } + onTouchEvent(event); + event.setAction(action); + } + super.onHoverEvent(event); + return true; + } + + /** + * Sets the current grabbed state, and dispatches a grabbed state change + * event to our listener. + */ + private void setGrabbedState(int newState) { + if (newState != mGrabbedState) { + if (newState != OnTriggerListener.NO_HANDLE) { + vibrate(); + } + mGrabbedState = newState; + if (mOnTriggerListener != null) { + if (newState == OnTriggerListener.NO_HANDLE) { + mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); + } else { + mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); + } + mOnTriggerListener.onGrabbedStateChange(this, newState); + } + } + } + + private boolean trySwitchToFirstTouchState(float x, float y) { + final float tx = x - mWaveCenterX; + final float ty = y - mWaveCenterY; + if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { + if (DEBUG) Log.v(TAG, "** Handle HIT"); + switchToState(STATE_FIRST_TOUCH, x, y); + updateGlowPosition(tx, ty); + mDragging = true; + return true; + } + return false; + } + + private void assignDefaultsIfNeeded() { + if (mOuterRadius == 0.0f) { + mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; + } + if (mSnapMargin == 0.0f) { + mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); + } + if (mInnerRadius == 0.0f) { + mInnerRadius = mHandleDrawable.getWidth() / 10.0f; + } + } + + private void computeInsets(int dx, int dy) { + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + mHorizontalInset = 0; + break; + case Gravity.RIGHT: + mHorizontalInset = dx; + break; + case Gravity.CENTER_HORIZONTAL: + default: + mHorizontalInset = dx / 2; + break; + } + switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + mVerticalInset = 0; + break; + case Gravity.BOTTOM: + mVerticalInset = dy; + break; + case Gravity.CENTER_VERTICAL: + default: + mVerticalInset = dy / 2; + break; + } + } + + /** + * Given the desired width and height of the ring and the allocated width and height, compute + * how much we need to scale the ring. + */ + private float computeScaleFactor(int desiredWidth, int desiredHeight, + int actualWidth, int actualHeight) { + + // Return unity if scaling is not allowed. + if (!mAllowScaling) return 1f; + + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); + + float scaleX = 1f; + float scaleY = 1f; + + // We use the gravity as a cue for whether we want to scale on a particular axis. + // We only scale to fit horizontally if we're not pinned to the left or right. Likewise, + // we only scale to fit vertically if we're not pinned to the top or bottom. In these + // cases, we want the ring to hang off the side or top/bottom, respectively. + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + case Gravity.RIGHT: + break; + case Gravity.CENTER_HORIZONTAL: + default: + if (desiredWidth > actualWidth) { + scaleX = (1f * actualWidth - mMaxTargetWidth) / + (desiredWidth - mMaxTargetWidth); + } + break; + } + switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + case Gravity.BOTTOM: + break; + case Gravity.CENTER_VERTICAL: + default: + if (desiredHeight > actualHeight) { + scaleY = (1f * actualHeight - mMaxTargetHeight) / + (desiredHeight - mMaxTargetHeight); + } + break; + } + return Math.min(scaleX, scaleY); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int minimumWidth = getSuggestedMinimumWidth(); + final int minimumHeight = getSuggestedMinimumHeight(); + int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); + int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); + + mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight, + computedWidth, computedHeight); + + int scaledWidth = getScaledSuggestedMinimumWidth(); + int scaledHeight = getScaledSuggestedMinimumHeight(); + + computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight); + setMeasuredDimension(computedWidth, computedHeight); + } + + private float getRingWidth() { + return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); + } + + private float getRingHeight() { + return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + final int width = right - left; + final int height = bottom - top; + + // Target placement width/height. This puts the targets on the greater of the ring + // width or the specified outer radius. + final float placementWidth = getRingWidth(); + final float placementHeight = getRingHeight(); + float newWaveCenterX = mHorizontalInset + + Math.max(width, mMaxTargetWidth + placementWidth) / 2; + float newWaveCenterY = mVerticalInset + + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; + + if (mInitialLayout) { + stopAndHideWaveAnimation(); + hideTargets(false, false); + mInitialLayout = false; + } + + mOuterRing.setPositionX(newWaveCenterX); + mOuterRing.setPositionY(newWaveCenterY); + + mPointCloud.setScale(mRingScaleFactor); + + mHandleDrawable.setPositionX(newWaveCenterX); + mHandleDrawable.setPositionY(newWaveCenterY); + + updateTargetPositions(newWaveCenterX, newWaveCenterY); + updatePointCloudPosition(newWaveCenterX, newWaveCenterY); + updateGlowPosition(newWaveCenterX, newWaveCenterY); + + mWaveCenterX = newWaveCenterX; + mWaveCenterY = newWaveCenterY; + + if (DEBUG) dump(); + } + + private void updateTargetPosition(int i, float centerX, float centerY) { + final float angle = getAngle(getSliceAngle(), i); + updateTargetPosition(i, centerX, centerY, angle); + } + + private void updateTargetPosition(int i, float centerX, float centerY, float angle) { + final float placementRadiusX = getRingWidth() / 2; + final float placementRadiusY = getRingHeight() / 2; + if (i >= 0) { + ArrayList<TargetDrawable> targets = mTargetDrawables; + final TargetDrawable targetIcon = targets.get(i); + targetIcon.setPositionX(centerX); + targetIcon.setPositionY(centerY); + targetIcon.setX(placementRadiusX * (float) Math.cos(angle)); + targetIcon.setY(placementRadiusY * (float) Math.sin(angle)); + } + } + + private void updateTargetPositions(float centerX, float centerY) { + updateTargetPositions(centerX, centerY, false); + } + + private void updateTargetPositions(float centerX, float centerY, boolean skipActive) { + final int size = mTargetDrawables.size(); + final float alpha = getSliceAngle(); + // Reposition the target drawables if the view changed. + for (int i = 0; i < size; i++) { + if (!skipActive || i != mActiveTarget) { + updateTargetPosition(i, centerX, centerY, getAngle(alpha, i)); + } + } + } + + private float getAngle(float alpha, int i) { + return mFirstItemOffset + alpha * i; + } + + private float getSliceAngle() { + return (float) (-2.0f * Math.PI / mTargetDrawables.size()); + } + + private void updatePointCloudPosition(float centerX, float centerY) { + mPointCloud.setCenter(centerX, centerY); + } + + @Override + protected void onDraw(Canvas canvas) { + mPointCloud.draw(canvas); + mOuterRing.draw(canvas); + final int ntargets = mTargetDrawables.size(); + for (int i = 0; i < ntargets; i++) { + TargetDrawable target = mTargetDrawables.get(i); + if (target != null) { + target.draw(canvas); + } + } + mHandleDrawable.draw(canvas); + } + + public void setOnTriggerListener(OnTriggerListener listener) { + mOnTriggerListener = listener; + } + + private float square(float d) { + return d * d; + } + + private float dist2(float dx, float dy) { + return dx*dx + dy*dy; + } + + private float getScaledGlowRadiusSquared() { + final float scaledTapRadius; + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; + } else { + scaledTapRadius = mGlowRadius; + } + return square(scaledTapRadius); + } + + private void announceTargets() { + StringBuilder utterance = new StringBuilder(); + final int targetCount = mTargetDrawables.size(); + for (int i = 0; i < targetCount; i++) { + String targetDescription = getTargetDescription(i); + String directionDescription = getDirectionDescription(i); + if (!TextUtils.isEmpty(targetDescription) + && !TextUtils.isEmpty(directionDescription)) { + String text = String.format(directionDescription, targetDescription); + utterance.append(text); + } + } + if (utterance.length() > 0) { + announceForAccessibility(utterance.toString()); + } + } + + private String getTargetDescription(int index) { + if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { + mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); + if (mTargetDrawables.size() != mTargetDescriptions.size()) { + Log.w(TAG, "The number of target drawables must be" + + " equal to the number of target descriptions."); + return null; + } + } + return mTargetDescriptions.get(index); + } + + private String getDirectionDescription(int index) { + if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { + mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); + if (mTargetDrawables.size() != mDirectionDescriptions.size()) { + Log.w(TAG, "The number of target drawables must be" + + " equal to the number of direction descriptions."); + return null; + } + } + return mDirectionDescriptions.get(index); + } + + private ArrayList<String> loadDescriptions(int resourceId) { + TypedArray array = getContext().getResources().obtainTypedArray(resourceId); + final int count = array.length(); + ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); + for (int i = 0; i < count; i++) { + String contentDescription = array.getString(i); + targetContentDescriptions.add(contentDescription); + } + array.recycle(); + return targetContentDescriptions; + } + + public int getResourceIdForTarget(int index) { + final TargetDrawable drawable = mTargetDrawables.get(index); + return drawable == null ? 0 : drawable.getResourceId(); + } + + public void setEnableTarget(int resourceId, boolean enabled) { + for (int i = 0; i < mTargetDrawables.size(); i++) { + final TargetDrawable target = mTargetDrawables.get(i); + if (target.getResourceId() == resourceId) { + target.setEnabled(enabled); + break; // should never be more than one match + } + } + } + + /** + * Gets the position of a target in the array that matches the given resource. + * @param resourceId + * @return the index or -1 if not found + */ + public int getTargetPosition(int resourceId) { + for (int i = 0; i < mTargetDrawables.size(); i++) { + final TargetDrawable target = mTargetDrawables.get(i); + if (target.getResourceId() == resourceId) { + return i; // should never be more than one match + } + } + return -1; + } + + private boolean replaceTargetDrawables(Resources res, int existingResourceId, + int newResourceId) { + if (existingResourceId == 0 || newResourceId == 0) { + return false; + } + + boolean result = false; + final ArrayList<TargetDrawable> drawables = mTargetDrawables; + final int size = drawables.size(); + for (int i = 0; i < size; i++) { + final TargetDrawable target = drawables.get(i); + if (target != null && target.getResourceId() == existingResourceId) { + target.setDrawable(res, newResourceId); + result = true; + } + } + + if (result) { + requestLayout(); // in case any given drawable's size changes + } + + return result; + } + + /** + * Searches the given package for a resource to use to replace the Drawable on the + * target with the given resource id + * @param component of the .apk that contains the resource + * @param name of the metadata in the .apk + * @param existingResId the resource id of the target to search for + * @return true if found in the given package and replaced at least one target Drawables + */ + public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, + int existingResId) { + if (existingResId == 0) return false; + + boolean replaced = false; + if (component != null) { + try { + PackageManager packageManager = mContext.getPackageManager(); + // Look for the search icon specified in the activity meta-data + Bundle metaData = packageManager.getActivityInfo( + component, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(name); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForActivity(component); + replaced = replaceTargetDrawables(res, existingResId, iconResId); + } + } + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to swap drawable; " + + component.flattenToShortString() + " not found", e); + } catch (Resources.NotFoundException nfe) { + Log.w(TAG, "Failed to swap drawable from " + + component.flattenToShortString(), nfe); + } + } + if (!replaced) { + // Restore the original drawable + replaceTargetDrawables(mContext.getResources(), existingResId, existingResId); + } + return replaced; + } +} diff --git a/core/java/com/android/internal/widget/multiwaveview/PointCloud.java b/core/java/com/android/internal/widget/multiwaveview/PointCloud.java new file mode 100644 index 0000000..6f26b99 --- /dev/null +++ b/core/java/com/android/internal/widget/multiwaveview/PointCloud.java @@ -0,0 +1,225 @@ +/* + * 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.internal.widget.multiwaveview; + +import java.util.ArrayList; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.Log; + +public class PointCloud { + private static final float MIN_POINT_SIZE = 2.0f; + private static final float MAX_POINT_SIZE = 4.0f; + private static final int INNER_POINTS = 8; + private static final String TAG = "PointCloud"; + private ArrayList<Point> mPointCloud = new ArrayList<Point>(); + private Drawable mDrawable; + private float mCenterX; + private float mCenterY; + private Paint mPaint; + private float mScale = 1.0f; + private static final float PI = (float) Math.PI; + + // These allow us to have multiple concurrent animations. + WaveManager waveManager = new WaveManager(); + GlowManager glowManager = new GlowManager(); + private float mOuterRadius; + + public class WaveManager { + private float radius = 50; + private float alpha = 0.0f; + + public void setRadius(float r) { + radius = r; + } + + public float getRadius() { + return radius; + } + + public void setAlpha(float a) { + alpha = a; + } + + public float getAlpha() { + return alpha; + } + }; + + public class GlowManager { + private float x; + private float y; + private float radius = 0.0f; + private float alpha = 0.0f; + + public void setX(float x1) { + x = x1; + } + + public float getX() { + return x; + } + + public void setY(float y1) { + y = y1; + } + + public float getY() { + return y; + } + + public void setAlpha(float a) { + alpha = a; + } + + public float getAlpha() { + return alpha; + } + + public void setRadius(float r) { + radius = r; + } + + public float getRadius() { + return radius; + } + } + + class Point { + float x; + float y; + float radius; + + public Point(float x2, float y2, float r) { + x = (float) x2; + y = (float) y2; + radius = r; + } + } + + public PointCloud(Drawable drawable) { + mPaint = new Paint(); + mPaint.setFilterBitmap(true); + mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable + mPaint.setAntiAlias(true); + mPaint.setDither(true); + + mDrawable = drawable; + if (mDrawable != null) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + } + + public void setCenter(float x, float y) { + mCenterX = x; + mCenterY = y; + } + + public void makePointCloud(float innerRadius, float outerRadius) { + if (innerRadius == 0) { + Log.w(TAG, "Must specify an inner radius"); + return; + } + mOuterRadius = outerRadius; + mPointCloud.clear(); + final float pointAreaRadius = (outerRadius - innerRadius); + final float ds = (2.0f * PI * innerRadius / INNER_POINTS); + final int bands = (int) Math.round(pointAreaRadius / ds); + final float dr = pointAreaRadius / bands; + float r = innerRadius; + for (int b = 0; b <= bands; b++, r += dr) { + float circumference = 2.0f * PI * r; + final int pointsInBand = (int) (circumference / ds); + float eta = PI/2.0f; + float dEta = 2.0f * PI / pointsInBand; + for (int i = 0; i < pointsInBand; i++) { + float x = r * (float) Math.cos(eta); + float y = r * (float) Math.sin(eta); + eta += dEta; + mPointCloud.add(new Point(x, y, r)); + } + } + } + + public void setScale(float scale) { + mScale = scale; + } + + public float getScale() { + return mScale; + } + + public int getAlphaForPoint(Point point) { + // Contribution from positional glow + float glowDistance = (float) Math.hypot(glowManager.x - point.x, glowManager.y - point.y); + float glowAlpha = 0.0f; + if (glowDistance < glowManager.radius) { + float cosf = (float) Math.cos(PI * 0.25f * glowDistance / glowManager.radius); + glowAlpha = glowManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 10.0f)); + } + + // Compute contribution from Wave + float radius = (float) Math.hypot(point.x, point.y); + float waveAlpha = 0.0f; + if (radius < waveManager.radius * 2) { + float distanceToWaveRing = (radius - waveManager.radius); + float cosf = (float) Math.cos(PI * 0.5f * distanceToWaveRing / waveManager.radius); + waveAlpha = waveManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 6.0f)); + } + return (int) (Math.max(glowAlpha, waveAlpha) * 255); + } + + private float interp(float min, float max, float f) { + return min + (max - min) * f; + } + + public void draw(Canvas canvas) { + ArrayList<Point> points = mPointCloud; + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(mScale, mScale, mCenterX, mCenterY); + for (int i = 0; i < points.size(); i++) { + Point point = points.get(i); + final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE, + point.radius / mOuterRadius); + final float px = point.x + mCenterX; + final float py = point.y + mCenterY; + int alpha = getAlphaForPoint(point); + + if (alpha == 0) continue; + + if (mDrawable != null) { + canvas.save(Canvas.MATRIX_SAVE_FLAG); + final float cx = mDrawable.getIntrinsicWidth() * 0.5f; + final float cy = mDrawable.getIntrinsicHeight() * 0.5f; + final float s = pointSize / MAX_POINT_SIZE; + canvas.scale(s, s, px, py); + canvas.translate(px - cx, py - cy); + mDrawable.setAlpha(alpha); + mDrawable.draw(canvas); + canvas.restore(); + } else { + mPaint.setAlpha(alpha); + canvas.drawCircle(px, py, pointSize, mPaint); + } + } + canvas.restore(); + } + +} diff --git a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java new file mode 100644 index 0000000..5a4c441 --- /dev/null +++ b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2011 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.internal.widget.multiwaveview; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.Log; + +public class TargetDrawable { + private static final String TAG = "TargetDrawable"; + private static final boolean DEBUG = false; + + public static final int[] STATE_ACTIVE = + { android.R.attr.state_enabled, android.R.attr.state_active }; + public static final int[] STATE_INACTIVE = + { android.R.attr.state_enabled, -android.R.attr.state_active }; + public static final int[] STATE_FOCUSED = + { android.R.attr.state_enabled, -android.R.attr.state_active, + android.R.attr.state_focused }; + + private float mTranslationX = 0.0f; + private float mTranslationY = 0.0f; + private float mPositionX = 0.0f; + private float mPositionY = 0.0f; + private float mScaleX = 1.0f; + private float mScaleY = 1.0f; + private float mAlpha = 1.0f; + private Drawable mDrawable; + private boolean mEnabled = true; + private final int mResourceId; + + public TargetDrawable(Resources res, int resId) { + mResourceId = resId; + setDrawable(res, resId); + } + + public void setDrawable(Resources res, int resId) { + // Note we explicitly don't set mResourceId to resId since we allow the drawable to be + // swapped at runtime and want to re-use the existing resource id for identification. + Drawable drawable = resId == 0 ? null : res.getDrawable(resId); + // Mutate the drawable so we can animate shared drawable properties. + mDrawable = drawable != null ? drawable.mutate() : null; + resizeDrawables(); + setState(STATE_INACTIVE); + } + + public TargetDrawable(TargetDrawable other) { + mResourceId = other.mResourceId; + // Mutate the drawable so we can animate shared drawable properties. + mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null; + resizeDrawables(); + setState(STATE_INACTIVE); + } + + public void setState(int [] state) { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + d.setState(state); + } + } + + public boolean hasState(int [] state) { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + // TODO: this doesn't seem to work + return d.getStateDrawableIndex(state) != -1; + } + return false; + } + + /** + * Returns true if the drawable is a StateListDrawable and is in the focused state. + * + * @return + */ + public boolean isActive() { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + int[] states = d.getState(); + for (int i = 0; i < states.length; i++) { + if (states[i] == android.R.attr.state_focused) { + return true; + } + } + } + return false; + } + + /** + * Returns true if this target is enabled. Typically an enabled target contains a valid + * drawable in a valid state. Currently all targets with valid drawables are valid. + * + * @return + */ + public boolean isEnabled() { + return mDrawable != null && mEnabled; + } + + /** + * Makes drawables in a StateListDrawable all the same dimensions. + * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the + * drawable. + */ + private void resizeDrawables() { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + int maxWidth = 0; + int maxHeight = 0; + for (int i = 0; i < d.getStateCount(); i++) { + Drawable childDrawable = d.getStateDrawable(i); + maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth()); + maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight()); + } + if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: " + + maxWidth + "x" + maxHeight); + d.setBounds(0, 0, maxWidth, maxHeight); + for (int i = 0; i < d.getStateCount(); i++) { + Drawable childDrawable = d.getStateDrawable(i); + if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: " + + maxWidth + "x" + maxHeight); + childDrawable.setBounds(0, 0, maxWidth, maxHeight); + } + } else if (mDrawable != null) { + mDrawable.setBounds(0, 0, + mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); + } + } + + public void setX(float x) { + mTranslationX = x; + } + + public void setY(float y) { + mTranslationY = y; + } + + public void setScaleX(float x) { + mScaleX = x; + } + + public void setScaleY(float y) { + mScaleY = y; + } + + public void setAlpha(float alpha) { + mAlpha = alpha; + } + + public float getX() { + return mTranslationX; + } + + public float getY() { + return mTranslationY; + } + + public float getScaleX() { + return mScaleX; + } + + public float getScaleY() { + return mScaleY; + } + + public float getAlpha() { + return mAlpha; + } + + public void setPositionX(float x) { + mPositionX = x; + } + + public void setPositionY(float y) { + mPositionY = y; + } + + public float getPositionX() { + return mPositionX; + } + + public float getPositionY() { + return mPositionY; + } + + public int getWidth() { + return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0; + } + + public int getHeight() { + return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0; + } + + public void draw(Canvas canvas) { + if (mDrawable == null || !mEnabled) { + return; + } + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY); + canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY); + canvas.translate(-0.5f * getWidth(), -0.5f * getHeight()); + mDrawable.setAlpha((int) Math.round(mAlpha * 255f)); + mDrawable.draw(canvas); + canvas.restore(); + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public int getResourceId() { + return mResourceId; + } +} diff --git a/core/java/com/android/internal/widget/multiwaveview/Tweener.java b/core/java/com/android/internal/widget/multiwaveview/Tweener.java new file mode 100644 index 0000000..d559d9d --- /dev/null +++ b/core/java/com/android/internal/widget/multiwaveview/Tweener.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011 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.internal.widget.multiwaveview; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +import android.animation.Animator.AnimatorListener; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.util.Log; + +class Tweener { + private static final String TAG = "Tweener"; + private static final boolean DEBUG = false; + + ObjectAnimator animator; + private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>(); + + public Tweener(ObjectAnimator anim) { + animator = anim; + } + + private static void remove(Animator animator) { + Iterator<Entry<Object, Tweener>> iter = sTweens.entrySet().iterator(); + while (iter.hasNext()) { + Entry<Object, Tweener> entry = iter.next(); + if (entry.getValue().animator == animator) { + if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey()) + + " sTweens.size() = " + sTweens.size()); + iter.remove(); + break; // an animator can only be attached to one object + } + } + } + + public static Tweener to(Object object, long duration, Object... vars) { + long delay = 0; + AnimatorUpdateListener updateListener = null; + AnimatorListener listener = null; + TimeInterpolator interpolator = null; + + // Iterate through arguments and discover properties to animate + ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2); + for (int i = 0; i < vars.length; i+=2) { + if (!(vars[i] instanceof String)) { + throw new IllegalArgumentException("Key must be a string: " + vars[i]); + } + String key = (String) vars[i]; + Object value = vars[i+1]; + if ("simultaneousTween".equals(key)) { + // TODO + } else if ("ease".equals(key)) { + interpolator = (TimeInterpolator) value; // TODO: multiple interpolators? + } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) { + updateListener = (AnimatorUpdateListener) value; + } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) { + listener = (AnimatorListener) value; + } else if ("delay".equals(key)) { + delay = ((Number) value).longValue(); + } else if ("syncWith".equals(key)) { + // TODO + } else if (value instanceof float[]) { + props.add(PropertyValuesHolder.ofFloat(key, + ((float[])value)[0], ((float[])value)[1])); + } else if (value instanceof int[]) { + props.add(PropertyValuesHolder.ofInt(key, + ((int[])value)[0], ((int[])value)[1])); + } else if (value instanceof Number) { + float floatValue = ((Number)value).floatValue(); + props.add(PropertyValuesHolder.ofFloat(key, floatValue)); + } else { + throw new IllegalArgumentException( + "Bad argument for key \"" + key + "\" with value " + value.getClass()); + } + } + + // Re-use existing tween, if present + Tweener tween = sTweens.get(object); + ObjectAnimator anim = null; + if (tween == null) { + anim = ObjectAnimator.ofPropertyValuesHolder(object, + props.toArray(new PropertyValuesHolder[props.size()])); + tween = new Tweener(anim); + sTweens.put(object, tween); + if (DEBUG) Log.v(TAG, "Added new Tweener " + tween); + } else { + anim = sTweens.get(object).animator; + replace(props, object); // Cancel all animators for given object + } + + if (interpolator != null) { + anim.setInterpolator(interpolator); + } + + // Update animation with properties discovered in loop above + anim.setStartDelay(delay); + anim.setDuration(duration); + if (updateListener != null) { + anim.removeAllUpdateListeners(); // There should be only one + anim.addUpdateListener(updateListener); + } + if (listener != null) { + anim.removeAllListeners(); // There should be only one. + anim.addListener(listener); + } + anim.addListener(mCleanupListener); + + return tween; + } + + Tweener from(Object object, long duration, Object... vars) { + // TODO: for v of vars + // toVars[v] = object[v] + // object[v] = vars[v] + return Tweener.to(object, duration, vars); + } + + // Listener to watch for completed animations and remove them. + private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + remove(animation); + } + + @Override + public void onAnimationCancel(Animator animation) { + remove(animation); + } + }; + + public static void reset() { + if (DEBUG) { + Log.v(TAG, "Reset()"); + if (sTweens.size() > 0) { + Log.v(TAG, "Cleaning up " + sTweens.size() + " animations"); + } + } + sTweens.clear(); + } + + private static void replace(ArrayList<PropertyValuesHolder> props, Object... args) { + for (final Object killobject : args) { + Tweener tween = sTweens.get(killobject); + if (tween != null) { + tween.animator.cancel(); + if (props != null) { + tween.animator.setValues( + props.toArray(new PropertyValuesHolder[props.size()])); + } else { + sTweens.remove(tween); + } + } + } + } +} |