1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.assist; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.Outline; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.view.View; 29 import android.view.ViewOutlineProvider; 30 import android.view.animation.Interpolator; 31 import android.view.animation.OvershootInterpolator; 32 import android.widget.FrameLayout; 33 import android.widget.ImageView; 34 35 import com.android.systemui.Interpolators; 36 import com.android.systemui.R; 37 38 public class AssistOrbView extends FrameLayout { 39 40 private final int mCircleMinSize; 41 private final int mBaseMargin; 42 private final int mStaticOffset; 43 private final Paint mBackgroundPaint = new Paint(); 44 private final Rect mCircleRect = new Rect(); 45 private final Rect mStaticRect = new Rect(); 46 private final Interpolator mOvershootInterpolator = new OvershootInterpolator(); 47 48 private boolean mClipToOutline; 49 private final int mMaxElevation; 50 private float mOutlineAlpha; 51 private float mOffset; 52 private float mCircleSize; 53 private ImageView mLogo; 54 private float mCircleAnimationEndValue; 55 56 private ValueAnimator mOffsetAnimator; 57 private ValueAnimator mCircleAnimator; 58 59 private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener 60 = new ValueAnimator.AnimatorUpdateListener() { 61 @Override 62 public void onAnimationUpdate(ValueAnimator animation) { 63 applyCircleSize((float) animation.getAnimatedValue()); 64 updateElevation(); 65 } 66 }; 67 private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { 68 @Override 69 public void onAnimationEnd(Animator animation) { 70 mCircleAnimator = null; 71 } 72 }; 73 private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener 74 = new ValueAnimator.AnimatorUpdateListener() { 75 @Override 76 public void onAnimationUpdate(ValueAnimator animation) { 77 mOffset = (float) animation.getAnimatedValue(); 78 updateLayout(); 79 } 80 }; 81 82 AssistOrbView(Context context)83 public AssistOrbView(Context context) { 84 this(context, null); 85 } 86 AssistOrbView(Context context, AttributeSet attrs)87 public AssistOrbView(Context context, AttributeSet attrs) { 88 this(context, attrs, 0); 89 } 90 AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr)91 public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) { 92 this(context, attrs, defStyleAttr, 0); 93 } 94 AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)95 public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, 96 int defStyleRes) { 97 super(context, attrs, defStyleAttr, defStyleRes); 98 setOutlineProvider(new ViewOutlineProvider() { 99 @Override 100 public void getOutline(View view, Outline outline) { 101 if (mCircleSize > 0.0f) { 102 outline.setOval(mCircleRect); 103 } else { 104 outline.setEmpty(); 105 } 106 outline.setAlpha(mOutlineAlpha); 107 } 108 }); 109 setWillNotDraw(false); 110 mCircleMinSize = context.getResources().getDimensionPixelSize( 111 R.dimen.assist_orb_size); 112 mBaseMargin = context.getResources().getDimensionPixelSize( 113 R.dimen.assist_orb_base_margin); 114 mStaticOffset = context.getResources().getDimensionPixelSize( 115 R.dimen.assist_orb_travel_distance); 116 mMaxElevation = context.getResources().getDimensionPixelSize( 117 R.dimen.assist_orb_elevation); 118 mBackgroundPaint.setAntiAlias(true); 119 mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color)); 120 } 121 getLogo()122 public ImageView getLogo() { 123 return mLogo; 124 } 125 126 @Override onDraw(Canvas canvas)127 protected void onDraw(Canvas canvas) { 128 super.onDraw(canvas); 129 drawBackground(canvas); 130 } 131 drawBackground(Canvas canvas)132 private void drawBackground(Canvas canvas) { 133 canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, 134 mBackgroundPaint); 135 } 136 137 @Override onFinishInflate()138 protected void onFinishInflate() { 139 super.onFinishInflate(); 140 mLogo = (ImageView) findViewById(R.id.search_logo); 141 } 142 143 @Override onLayout(boolean changed, int l, int t, int r, int b)144 protected void onLayout(boolean changed, int l, int t, int r, int b) { 145 mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); 146 if (changed) { 147 updateCircleRect(mStaticRect, mStaticOffset, true); 148 } 149 } 150 animateCircleSize(float circleSize, long duration, long startDelay, Interpolator interpolator)151 public void animateCircleSize(float circleSize, long duration, 152 long startDelay, Interpolator interpolator) { 153 if (circleSize == mCircleAnimationEndValue) { 154 return; 155 } 156 if (mCircleAnimator != null) { 157 mCircleAnimator.cancel(); 158 } 159 mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); 160 mCircleAnimator.addUpdateListener(mCircleUpdateListener); 161 mCircleAnimator.addListener(mClearAnimatorListener); 162 mCircleAnimator.setInterpolator(interpolator); 163 mCircleAnimator.setDuration(duration); 164 mCircleAnimator.setStartDelay(startDelay); 165 mCircleAnimator.start(); 166 mCircleAnimationEndValue = circleSize; 167 } 168 applyCircleSize(float circleSize)169 private void applyCircleSize(float circleSize) { 170 mCircleSize = circleSize; 171 updateLayout(); 172 } 173 updateElevation()174 private void updateElevation() { 175 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 176 t = 1.0f - Math.max(t, 0.0f); 177 float offset = t * mMaxElevation; 178 setElevation(offset); 179 } 180 181 /** 182 * Animates the offset to the edge of the screen. 183 * 184 * @param offset The offset to apply. 185 * @param startDelay The desired start delay if animated. 186 * 187 * @param interpolator The desired interpolator if animated. If null, 188 * a default interpolator will be taken designed for appearing or 189 * disappearing. 190 */ animateOffset(float offset, long duration, long startDelay, Interpolator interpolator)191 private void animateOffset(float offset, long duration, long startDelay, 192 Interpolator interpolator) { 193 if (mOffsetAnimator != null) { 194 mOffsetAnimator.removeAllListeners(); 195 mOffsetAnimator.cancel(); 196 } 197 mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); 198 mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); 199 mOffsetAnimator.addListener(new AnimatorListenerAdapter() { 200 @Override 201 public void onAnimationEnd(Animator animation) { 202 mOffsetAnimator = null; 203 } 204 }); 205 mOffsetAnimator.setInterpolator(interpolator); 206 mOffsetAnimator.setStartDelay(startDelay); 207 mOffsetAnimator.setDuration(duration); 208 mOffsetAnimator.start(); 209 } 210 updateLayout()211 private void updateLayout() { 212 updateCircleRect(); 213 updateLogo(); 214 invalidateOutline(); 215 invalidate(); 216 updateClipping(); 217 } 218 updateClipping()219 private void updateClipping() { 220 boolean clip = mCircleSize < mCircleMinSize; 221 if (clip != mClipToOutline) { 222 setClipToOutline(clip); 223 mClipToOutline = clip; 224 } 225 } 226 227 private void updateLogo() { 228 float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f; 229 float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f 230 - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f; 231 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 232 translationY += t * mStaticOffset * 0.1f; 233 float alpha = 1.0f-t; 234 alpha = Math.max((alpha - 0.5f) * 2.0f, 0); 235 mLogo.setImageAlpha((int) (alpha * 255)); 236 mLogo.setTranslationX(translationX); 237 mLogo.setTranslationY(translationY); 238 } 239 240 private void updateCircleRect() { 241 updateCircleRect(mCircleRect, mOffset, false); 242 } 243 244 private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { 245 int left, top; 246 float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; 247 left = (int) (getWidth() - circleSize) / 2; 248 top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); 249 rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); 250 } 251 252 public void startExitAnimation(long delay) { 253 animateCircleSize(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN); 254 animateOffset(0, 200, delay, Interpolators.FAST_OUT_LINEAR_IN); 255 } 256 257 public void startEnterAnimation() { 258 applyCircleSize(0); 259 post(new Runnable() { 260 @Override 261 public void run() { 262 animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator); 263 animateOffset(mStaticOffset, 400, 0 /* delay */, Interpolators.LINEAR_OUT_SLOW_IN); 264 } 265 }); 266 } 267 268 public void reset() { 269 mClipToOutline = false; 270 mBackgroundPaint.setAlpha(255); 271 mOutlineAlpha = 1.0f; 272 } 273 274 @Override 275 public boolean hasOverlappingRendering() { 276 // not really true but it's ok during an animation, as it's never permanent 277 return false; 278 } 279 } 280