Merge "Revert "Remove dead code #11: Remove more unused classes""
diff --git a/api/current.txt b/api/current.txt
index 120025d..7ea866a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -482,7 +482,7 @@
field public static final int dialogTitle = 16843250; // 0x10101f2
field public static final int digits = 16843110; // 0x1010166
field public static final int direction = 16843217; // 0x10101d1
- field public static final deprecated int directionDescriptions = 16843681; // 0x10103a1
+ field public static final int directionDescriptions = 16843681; // 0x10103a1
field public static final int directionPriority = 16843218; // 0x10101d2
field public static final int disableDependentsState = 16843249; // 0x10101f1
field public static final int disabledAlpha = 16842803; // 0x1010033
@@ -1200,7 +1200,7 @@
field public static final int tag = 16842961; // 0x10100d1
field public static final int targetActivity = 16843266; // 0x1010202
field public static final int targetClass = 16842799; // 0x101002f
- field public static final deprecated int targetDescriptions = 16843680; // 0x10103a0
+ field public static final int targetDescriptions = 16843680; // 0x10103a0
field public static final int targetId = 16843740; // 0x10103dc
field public static final int targetName = 16843853; // 0x101044d
field public static final int targetPackage = 16842785; // 0x1010021
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);
+ }
+ }
+ }
+ }
+}
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index ea22b15..3fdcaf7 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -16,9 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
>
<include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_big_base.xml b/core/res/res/layout/notification_template_material_big_base.xml
index 2a3ee90..935424a 100644
--- a/core/res/res/layout/notification_template_material_big_base.xml
+++ b/core/res/res/layout/notification_template_material_big_base.xml
@@ -16,9 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"
>
<include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_big_picture.xml b/core/res/res/layout/notification_template_material_big_picture.xml
index f1a9549..302e651 100644
--- a/core/res/res/layout/notification_template_material_big_picture.xml
+++ b/core/res/res/layout/notification_template_material_big_picture.xml
@@ -16,9 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"
>
<ImageView
android:id="@+id/big_picture"
diff --git a/core/res/res/layout/notification_template_material_big_text.xml b/core/res/res/layout/notification_template_material_big_text.xml
index f657f04..d0c10b2 100644
--- a/core/res/res/layout/notification_template_material_big_text.xml
+++ b/core/res/res/layout/notification_template_material_big_text.xml
@@ -16,9 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"
>
<include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_inbox.xml b/core/res/res/layout/notification_template_material_inbox.xml
index d292d4e..ac448ee 100644
--- a/core/res/res/layout/notification_template_material_inbox.xml
+++ b/core/res/res/layout/notification_template_material_inbox.xml
@@ -16,9 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"
>
<include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_media.xml b/core/res/res/layout/notification_template_material_media.xml
index 0292d28..69020a4 100644
--- a/core/res/res/layout/notification_template_material_media.xml
+++ b/core/res/res/layout/notification_template_material_media.xml
@@ -16,11 +16,14 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="64dp"
android:orientation="horizontal"
android:background="#00000000"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
>
<include layout="@layout/notification_template_icon_group"
android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 39c42ee..551c044 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7034,16 +7034,75 @@
<!-- =============================== -->
<eat-comment />
<declare-styleable name="GlowPadView">
- <!-- Reference to an array resource that be used as description for the targets around the circle.
- {@deprecated Removed.} -->
+ <!-- Reference to an array resource that be shown as targets around a circle. -->
+ <attr name="targetDrawables" format="reference" />
+
+ <!-- Reference to an array resource that be used as description for the targets around the circle. -->
<attr name="targetDescriptions" format="reference" />
- <!-- Reference to an array resource that be used to announce the directions with targets around the circle.
- {@deprecated Removed.}-->
+ <!-- Reference to an array resource that be used to announce the directions with targets around the circle. -->
<attr name="directionDescriptions" format="reference" />
+
+ <!-- Sets a drawable as the center. -->
+ <attr name="handleDrawable" format="reference" />
+
+ <!-- Drawable to use for wave ripple animation. -->
+ <attr name="outerRingDrawable" format="reference"/>
+
+ <!-- Drawble used for drawing points -->
+ <attr name="pointDrawable" format="reference" />
+
+ <!-- Inner radius of glow area. -->
+ <attr name="innerRadius"/>
+
+ <!-- Outer radius of glow area. Target icons will be drawn on this circle. -->
+ <attr name="outerRadius" format="dimension" />
+
+ <!-- Radius of glow under finger. -->
+ <attr name="glowRadius" format="dimension" />
+
+ <!-- Tactile feedback duration for actions. Set to '0' for no vibration. -->
+ <attr name="vibrationDuration" format="integer" />
+
+ <!-- How close we need to be before snapping to a target. -->
+ <attr name="snapMargin" format="dimension" />
+
+ <!-- Number of waves/chevrons to show in animation. -->
+ <attr name="feedbackCount" format="integer" />
+
+ <!-- Used when the handle shouldn't wait to be hit before following the finger -->
+ <attr name="alwaysTrackFinger" format="boolean" />
+
+ <!-- Location along the circle of the first item, in degrees.-->
+ <attr name="firstItemOffset" format="float" />
+
+ <!-- Causes targets to snap to the finger location on activation. -->
+ <attr name="magneticTargets" format="boolean" />
+
+ <attr name="gravity" />
+
+ <!-- Determine whether the glow pad is allowed to scale to fit the bounds indicated
+ by its parent. If this is set to false, no scaling will occur. If this is set to true
+ scaling will occur to fit for any axis in which gravity is set to center. -->
+ <attr name="allowScaling" format="boolean" />
</declare-styleable>
<!-- =============================== -->
+ <!-- SizeAdaptiveLayout class attributes -->
+ <!-- =============================== -->
+ <eat-comment />
+ <declare-styleable name="SizeAdaptiveLayout_Layout">
+ <!-- The maximum valid height for this item. -->
+ <attr name="layout_maxHeight" format="dimension">
+ <!-- Indicates that the view may be resized arbitrarily large. -->
+ <enum name="unbounded" value="-1" />
+ </attr>
+ <!-- The minimum valid height for this item. -->
+ <attr name="layout_minHeight" format="dimension" />
+ </declare-styleable>
+ <declare-styleable name="SizeAdaptiveLayout" />
+
+ <!-- =============================== -->
<!-- Location package class attributes -->
<!-- =============================== -->
<eat-comment />
@@ -7450,6 +7509,11 @@
<enum name="pageDeleteDropTarget" value="7" />
</attr>
+ <declare-styleable name="SlidingChallengeLayout_Layout">
+ <attr name="layout_childType" />
+ <attr name="layout_maxHeight" />
+ </declare-styleable>
+
<!-- Attributes that can be used with <code><FragmentBreadCrumbs></code>
tags. -->
<declare-styleable name="FragmentBreadCrumbs">
@@ -7458,6 +7522,27 @@
<attr name="itemColor" format="color|reference" />
</declare-styleable>
+ <declare-styleable name="MultiPaneChallengeLayout">
+ <!-- Influences how layout_centerWithinArea behaves -->
+ <attr name="orientation" />
+ </declare-styleable>
+
+ <declare-styleable name="MultiPaneChallengeLayout_Layout">
+ <!-- Percentage of the screen this child should consume or center within.
+ If 0/default, the view will be measured by standard rules
+ as if this were a FrameLayout. -->
+ <attr name="layout_centerWithinArea" format="float" />
+ <attr name="layout_childType" />
+ <attr name="layout_gravity" />
+ <attr name="layout_maxWidth" format="dimension" />
+ <attr name="layout_maxHeight" />
+ </declare-styleable>
+
+ <declare-styleable name="KeyguardSecurityViewFlipper_Layout">
+ <attr name="layout_maxWidth" />
+ <attr name="layout_maxHeight" />
+ </declare-styleable>
+
<declare-styleable name="Toolbar">
<attr name="titleTextAppearance" format="reference" />
<attr name="subtitleTextAppearance" format="reference" />
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 9945c63..9f49b08 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1759,9 +1759,7 @@
<public type="attr" name="actionModeSplitBackground" id="0x0101039d" />
<public type="attr" name="textAppearanceListItem" id="0x0101039e" />
<public type="attr" name="textAppearanceListItemSmall" id="0x0101039f" />
- <!-- @deprecated Removed. -->
<public type="attr" name="targetDescriptions" id="0x010103a0" />
- <!-- @deprecated Removed. -->
<public type="attr" name="directionDescriptions" id="0x010103a1" />
<public type="attr" name="overridesImplicitlyEnabledSubtype" id="0x010103a2" />
<public type="attr" name="listPreferredItemPaddingLeft" id="0x010103a3" />
diff --git a/core/tests/coretests/res/layout/size_adaptive.xml b/core/tests/coretests/res/layout/size_adaptive.xml
new file mode 100644
index 0000000..03d0574
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/multi1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_color.xml b/core/tests/coretests/res/layout/size_adaptive_color.xml
new file mode 100644
index 0000000..cdb7a59
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_color.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:background="#ffffff"
+ android:id="@+id/multi1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml b/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml
new file mode 100644
index 0000000..d24df5b
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:background="@drawable/size_adaptive_statelist"
+ android:id="@+id/multi1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_four_u.xml b/core/tests/coretests/res/layout/size_adaptive_four_u.xml
new file mode 100644
index 0000000..232b921
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_four_u.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridLayout4"
+ android:layout_width="match_parent"
+ android:layout_height="256dp"
+ android:background="#000000"
+ android:columnCount="2"
+ android:padding="1dp" >
+
+ <ImageView
+ android:id="@+id/actor"
+ android:layout_width="62dp"
+ android:layout_height="62dp"
+ android:layout_row="0"
+ android:layout_column="0"
+ android:layout_rowSpan="2"
+ android:contentDescription="@string/actor"
+ android:src="@drawable/abe" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/name"
+ android:layout_row="0"
+ android:layout_column="1"
+ android:layout_gravity="fill_horizontal"
+ android:padding="3dp"
+ android:text="@string/actor"
+ android:textColor="#ffffff"
+ android:textStyle="bold" />
+
+ <ImageView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_row="1"
+ android:layout_column="1"
+ android:layout_gravity="fill_horizontal"
+ android:padding="5dp"
+ android:adjustViewBounds="true"
+ android:background="#555555"
+ android:scaleType="centerCrop"
+ android:src="@drawable/gettysburg"
+ android:contentDescription="@string/caption" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/note"
+ android:layout_row="2"
+ android:layout_column="1"
+ android:layout_gravity="fill_horizontal"
+ android:padding="3dp"
+ android:singleLine="true"
+ android:text="@string/first"
+ android:textColor="#ffffff" />
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml b/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml
new file mode 100644
index 0000000..93a10de
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridLayout4"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:columnCount="2"
+ android:padding="1dp"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/actor"
+ android:layout_width="62dp"
+ android:layout_height="62dp"
+ android:layout_row="0"
+ android:layout_column="0"
+ android:layout_rowSpan="2"
+ android:contentDescription="@string/actor"
+ android:src="@drawable/abe" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/name"
+ android:layout_row="0"
+ android:layout_column="1"
+ android:layout_gravity="fill_horizontal"
+ android:padding="3dp"
+ android:text="@string/actor"
+ android:textColor="#ffffff"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/note"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_column="1"
+ android:layout_gravity="fill_horizontal"
+ android:layout_marginTop="5dp"
+ android:layout_row="1"
+ android:padding="3dp"
+ android:singleLine="false"
+ android:text="@string/first"
+ android:textColor="#ffffff" />
+
+ </GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_gappy.xml b/core/tests/coretests/res/layout/size_adaptive_gappy.xml
new file mode 100644
index 0000000..d5e3b41
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_gappy.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/multi_with_gap"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="256dp"
+ internal:layout_minHeight="128dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_large_only.xml b/core/tests/coretests/res/layout/size_adaptive_large_only.xml
new file mode 100644
index 0000000..cf58265
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_large_only.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/large_only_multi"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="256dp"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_lies.xml b/core/tests/coretests/res/layout/size_adaptive_lies.xml
new file mode 100644
index 0000000..7de892e
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_lies.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/multi1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_one_u.xml b/core/tests/coretests/res/layout/size_adaptive_one_u.xml
new file mode 100644
index 0000000..b6fe4a0
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_one_u.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:background="#000000"
+ android:columnCount="3"
+ android:padding="1dp"
+ android:rowCount="2" >
+
+ <ImageView
+ android:id="@+id/actor"
+ android:layout_width="62dp"
+ android:layout_height="62dp"
+ android:layout_column="0"
+ android:layout_row="0"
+ android:layout_rowSpan="2"
+ android:contentDescription="@string/actor"
+ android:src="@drawable/abe" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_gravity="fill"
+ android:padding="3dp"
+ android:text="@string/actor"
+ android:textColor="#ffffff"
+ android:textStyle="bold" />
+
+ <ImageView
+ android:layout_width="62dp"
+ android:layout_height="62dp"
+ android:layout_gravity="fill_vertical"
+ android:layout_rowSpan="2"
+ android:adjustViewBounds="true"
+ android:background="#555555"
+ android:padding="2dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/gettysburg"
+ android:contentDescription="@string/caption" />
+
+ <TextView
+ android:id="@+id/note"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="fill"
+ android:layout_marginTop="5dp"
+ android:padding="3dp"
+ android:singleLine="true"
+ android:text="@string/first"
+ android:textColor="#ffffff" />
+
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml b/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml
new file mode 100644
index 0000000..df54eb6
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/gridLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:background="#000000"
+ android:columnCount="2"
+ android:padding="1dp"
+ android:rowCount="2" >
+
+ <ImageView
+ android:id="@+id/actor"
+ android:layout_width="62dp"
+ android:layout_height="62dp"
+ android:layout_column="0"
+ android:layout_row="0"
+ android:layout_rowSpan="2"
+ android:contentDescription="@string/actor"
+ android:src="@drawable/abe" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_gravity="fill"
+ android:padding="3dp"
+ android:text="@string/actor"
+ android:textColor="#ffffff"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/note"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="fill"
+ android:layout_marginTop="5dp"
+ android:padding="3dp"
+ android:singleLine="true"
+ android:text="@string/first"
+ android:textColor="#ffffff" />
+
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_overlapping.xml b/core/tests/coretests/res/layout/size_adaptive_overlapping.xml
new file mode 100644
index 0000000..4abe8b0
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_overlapping.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/multi_with_overlap"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="256dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="256dp"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_singleton.xml b/core/tests/coretests/res/layout/size_adaptive_singleton.xml
new file mode 100644
index 0000000..eba387f
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_singleton.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u_text"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_text.xml b/core/tests/coretests/res/layout/size_adaptive_text.xml
new file mode 100644
index 0000000..a9f0ba9
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_text.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/multi1"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u_text"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u_text"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_three_way.xml b/core/tests/coretests/res/layout/size_adaptive_three_way.xml
new file mode 100644
index 0000000..1eb5396
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_three_way.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/three_way_multi"
+ android:layout_width="match_parent"
+ android:layout_height="64dp" >
+
+ <include
+ android:id="@+id/one_u"
+ layout="@layout/size_adaptive_one_u"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
+ />
+
+ <include
+ android:id="@+id/two_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="128dp"
+ internal:layout_minHeight="65dp"
+ internal:layout_maxHeight="128dp"/>
+
+ <include
+ android:id="@+id/four_u"
+ layout="@layout/size_adaptive_four_u"
+ android:layout_width="fill_parent"
+ android:layout_height="256dp"
+ internal:layout_minHeight="129dp"
+ internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java
new file mode 100644
index 0000000..18411b0
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java
@@ -0,0 +1,479 @@
+/*
+ * 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 com.android.frameworks.coretests.R;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.widget.SizeAdaptiveLayout;
+
+
+public class SizeAdaptiveLayoutTest extends AndroidTestCase {
+
+ private LayoutInflater mInflater;
+ private int mOneU;
+ private int mFourU;
+ private SizeAdaptiveLayout mSizeAdaptiveLayout;
+ private View mSmallView;
+ private View mMediumView;
+ private View mLargeView;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // inflate the layout
+ final Context context = getContext();
+ mInflater = LayoutInflater.from(context);
+ mOneU = 64;
+ mFourU = 4 * mOneU;
+ }
+
+ private void inflate(int resource){
+ mSizeAdaptiveLayout = (SizeAdaptiveLayout) mInflater.inflate(resource, null);
+ mSizeAdaptiveLayout.onAttachedToWindow();
+
+ mSmallView = mSizeAdaptiveLayout.findViewById(R.id.one_u);
+ mMediumView = mSizeAdaptiveLayout.findViewById(R.id.two_u);
+ mLargeView = mSizeAdaptiveLayout.findViewById(R.id.four_u);
+ }
+
+ /**
+ * The name 'test preconditions' is a convention to signal that if this
+ * test doesn't pass, the test case was not set up properly and it might
+ * explain any and all failures in other tests. This is not guaranteed
+ * to run before other tests, as junit uses reflection to find the tests.
+ */
+ @SmallTest
+ public void testPreconditions() {
+ assertNotNull(mInflater);
+
+ inflate(R.layout.size_adaptive);
+ assertNotNull(mSizeAdaptiveLayout);
+ assertNotNull(mSmallView);
+ assertNotNull(mLargeView);
+ }
+
+ @SmallTest
+ public void testOpenLarge() {
+ inflate(R.layout.size_adaptive);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight + 10;
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertEquals("1U should be gone",
+ View.GONE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenSmall() {
+ inflate(R.layout.size_adaptive);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ assertEquals("4U should be gone",
+ View.GONE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenTooSmall() {
+ inflate(R.layout.size_adaptive);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight - 10;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ assertEquals("4U should be gone",
+ View.GONE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenTooBig() {
+ inflate(R.layout.size_adaptive);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ lp.maxHeight = 500;
+ mLargeView.setLayoutParams(lp);
+ int height = (int) (lp.minHeight + 10);
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertEquals("1U should be gone",
+ View.GONE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenWrapContent() {
+ inflate(R.layout.size_adaptive_text);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight + 10;
+
+ // manually measure it, and lay it out
+ int measureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
+ mSizeAdaptiveLayout.measure(500, measureSpec);
+ assertTrue("should not be forced to 4U",
+ mSizeAdaptiveLayout.getMeasuredHeight() < mFourU);
+ }
+
+ @SmallTest
+ public void testOpenOneUOnlySmall() {
+ inflate(R.layout.size_adaptive_singleton);
+ assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight - 10;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenOneUOnlyLarge() {
+ inflate(R.layout.size_adaptive_singleton);
+ assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.maxHeight + 10;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenOneUOnlyJustRight() {
+ inflate(R.layout.size_adaptive_singleton);
+ assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenFourUOnlySmall() {
+ inflate(R.layout.size_adaptive_large_only);
+ assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight - 10;
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenFourUOnlyLarge() {
+ inflate(R.layout.size_adaptive_large_only);
+ assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.maxHeight + 10;
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenFourUOnlyJustRight() {
+ inflate(R.layout.size_adaptive_large_only);
+ assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenIntoAGap() {
+ inflate(R.layout.size_adaptive_gappy);
+
+ SizeAdaptiveLayout.LayoutParams smallParams =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ SizeAdaptiveLayout.LayoutParams largeParams =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ assertTrue("gappy layout should have a gap",
+ smallParams.maxHeight + 10 < largeParams.minHeight);
+ int height = (int) smallParams.maxHeight + 10;
+
+ measureAndLayout(height);
+
+ assertTrue("one and only one view should be visible",
+ mLargeView.getVisibility() != mSmallView.getVisibility());
+ // behavior is undefined in this case.
+ }
+
+ @SmallTest
+ public void testOpenIntoAnOverlap() {
+ inflate(R.layout.size_adaptive_overlapping);
+
+ SizeAdaptiveLayout.LayoutParams smallParams =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ SizeAdaptiveLayout.LayoutParams largeParams =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ assertEquals("overlapping layout should overlap",
+ smallParams.minHeight,
+ largeParams.minHeight);
+ int height = (int) smallParams.maxHeight;
+
+ measureAndLayout(height);
+
+ assertTrue("one and only one view should be visible",
+ mLargeView.getVisibility() != mSmallView.getVisibility());
+ assertEquals("1U should get priority in an overlap because it is first",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenThreeWayViewSmall() {
+ inflate(R.layout.size_adaptive_three_way);
+ assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ assertEquals("2U should be gone",
+ View.GONE,
+ mMediumView.getVisibility());
+ assertEquals("4U should be gone",
+ View.GONE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenThreeWayViewMedium() {
+ inflate(R.layout.size_adaptive_three_way);
+ assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mMediumView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be gone",
+ View.GONE,
+ mSmallView.getVisibility());
+ assertEquals("2U should be visible",
+ View.VISIBLE,
+ mMediumView.getVisibility());
+ assertEquals("4U should be gone",
+ View.GONE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testOpenThreeWayViewLarge() {
+ inflate(R.layout.size_adaptive_three_way);
+ assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be gone",
+ View.GONE,
+ mSmallView.getVisibility());
+ assertEquals("2U should be gone",
+ View.GONE,
+ mMediumView.getVisibility());
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ }
+
+ @SmallTest
+ public void testResizeWithoutAnimation() {
+ inflate(R.layout.size_adaptive);
+
+ SizeAdaptiveLayout.LayoutParams largeParams =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int startHeight = (int) largeParams.minHeight + 10;
+ int endHeight = (int) largeParams.minHeight + 10;
+
+ measureAndLayout(startHeight);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertFalse("There should be no animation on initial rendering.",
+ mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+
+ measureAndLayout(endHeight);
+
+ assertEquals("4U should still be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertFalse("There should be no animation on scale within a view.",
+ mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+ }
+
+ @SmallTest
+ public void testResizeWithAnimation() {
+ inflate(R.layout.size_adaptive);
+
+ SizeAdaptiveLayout.LayoutParams smallParams =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ SizeAdaptiveLayout.LayoutParams largeParams =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int startHeight = (int) largeParams.minHeight + 10;
+ int endHeight = (int) smallParams.maxHeight;
+
+ measureAndLayout(startHeight);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertFalse("There should be no animation on initial rendering.",
+ mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+
+ measureAndLayout(endHeight);
+
+ assertEquals("1U should now be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ assertTrue("There should be an animation on scale between views.",
+ mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+ }
+
+ @SmallTest
+ public void testModestyPanelChangesColorWhite() {
+ inflate(R.layout.size_adaptive_color);
+ View panel = mSizeAdaptiveLayout.getModestyPanel();
+ assertTrue("ModestyPanel should have a ColorDrawable background",
+ panel.getBackground() instanceof ColorDrawable);
+ ColorDrawable panelColor = (ColorDrawable) panel.getBackground();
+ ColorDrawable salColor = (ColorDrawable) mSizeAdaptiveLayout.getBackground();
+ assertEquals("ModestyPanel color should match the SizeAdaptiveLayout",
+ panelColor.getColor(), salColor.getColor());
+ }
+
+ @SmallTest
+ public void testModestyPanelTracksStateListColor() {
+ inflate(R.layout.size_adaptive_color_statelist);
+ View panel = mSizeAdaptiveLayout.getModestyPanel();
+ assertEquals("ModestyPanel should have a ColorDrawable background" ,
+ panel.getBackground().getClass(), ColorDrawable.class);
+ ColorDrawable panelColor = (ColorDrawable) panel.getBackground();
+ assertEquals("ModestyPanel color should match the SizeAdaptiveLayout",
+ panelColor.getColor(), Color.RED);
+ }
+ @SmallTest
+ public void testOpenSmallEvenWhenLargeIsActuallySmall() {
+ inflate(R.layout.size_adaptive_lies);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("1U should be visible",
+ View.VISIBLE,
+ mSmallView.getVisibility());
+ assertTrue("1U should also have been measured",
+ mSmallView.getMeasuredHeight() > 0);
+ }
+
+ @SmallTest
+ public void testOpenLargeEvenWhenLargeIsActuallySmall() {
+ inflate(R.layout.size_adaptive_lies);
+ SizeAdaptiveLayout.LayoutParams lp =
+ (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+ int height = (int) lp.minHeight;
+
+ measureAndLayout(height);
+
+ assertEquals("4U should be visible",
+ View.VISIBLE,
+ mLargeView.getVisibility());
+ assertTrue("4U should also have been measured",
+ mLargeView.getMeasuredHeight() > 0);
+ }
+
+ private void measureAndLayout(int height) {
+ // manually measure it, and lay it out
+ int measureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
+ mSizeAdaptiveLayout.measure(500, measureSpec);
+ mSizeAdaptiveLayout.layout(0, 0, 500, mSizeAdaptiveLayout.getMeasuredHeight());
+ }
+}
diff --git a/packages/SystemUI/res/layout/notification_public_default.xml b/packages/SystemUI/res/layout/notification_public_default.xml
index 044ba09..efabc06 100644
--- a/packages/SystemUI/res/layout/notification_public_default.xml
+++ b/packages/SystemUI/res/layout/notification_public_default.xml
@@ -16,9 +16,12 @@
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:internal="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="64dp"
+ internal:layout_minHeight="64dp"
+ internal:layout_maxHeight="64dp"
>
<ImageView android:id="@+id/icon"
android:layout_width="40dp"