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.AnimationUtils; 31 import android.view.animation.Interpolator; 32 import android.view.animation.OvershootInterpolator; 33 import android.widget.FrameLayout; 34 import android.widget.ImageView; 35 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 mAppearInterpolator; 47 private final Interpolator mDisappearInterpolator; 48 private final Interpolator mOvershootInterpolator = new OvershootInterpolator(); 49 50 private boolean mClipToOutline; 51 private final int mMaxElevation; 52 private float mOutlineAlpha; 53 private float mOffset; 54 private float mCircleSize; 55 private ImageView mLogo; 56 private float mCircleAnimationEndValue; 57 58 private ValueAnimator mOffsetAnimator; 59 private ValueAnimator mCircleAnimator; 60 61 private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener 62 = new ValueAnimator.AnimatorUpdateListener() { 63 @Override 64 public void onAnimationUpdate(ValueAnimator animation) { 65 applyCircleSize((float) animation.getAnimatedValue()); 66 updateElevation(); 67 } 68 }; 69 private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { 70 @Override 71 public void onAnimationEnd(Animator animation) { 72 mCircleAnimator = null; 73 } 74 }; 75 private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener 76 = new ValueAnimator.AnimatorUpdateListener() { 77 @Override 78 public void onAnimationUpdate(ValueAnimator animation) { 79 mOffset = (float) animation.getAnimatedValue(); 80 updateLayout(); 81 } 82 }; 83 84 85 public AssistOrbView(Context context) { 86 this(context, null); 87 } 88 89 public AssistOrbView(Context context, AttributeSet attrs) { 90 this(context, attrs, 0); 91 } 92 93 public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr) { 94 this(context, attrs, defStyleAttr, 0); 95 } 96 97 public AssistOrbView(Context context, AttributeSet attrs, int defStyleAttr, 98 int defStyleRes) { 99 super(context, attrs, defStyleAttr, defStyleRes); 100 setOutlineProvider(new ViewOutlineProvider() { 101 @Override 102 public void getOutline(View view, Outline outline) { 103 if (mCircleSize > 0.0f) { 104 outline.setOval(mCircleRect); 105 } else { 106 outline.setEmpty(); 107 } 108 outline.setAlpha(mOutlineAlpha); 109 } 110 }); 111 setWillNotDraw(false); 112 mCircleMinSize = context.getResources().getDimensionPixelSize( 113 R.dimen.assist_orb_size); 114 mBaseMargin = context.getResources().getDimensionPixelSize( 115 R.dimen.assist_orb_base_margin); 116 mStaticOffset = context.getResources().getDimensionPixelSize( 117 R.dimen.assist_orb_travel_distance); 118 mMaxElevation = context.getResources().getDimensionPixelSize( 119 R.dimen.assist_orb_elevation); 120 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 121 android.R.interpolator.linear_out_slow_in); 122 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 123 android.R.interpolator.fast_out_linear_in); 124 mBackgroundPaint.setAntiAlias(true); 125 mBackgroundPaint.setColor(getResources().getColor(R.color.assist_orb_color)); 126 } 127 128 public ImageView getLogo() { 129 return mLogo; 130 } 131 132 @Override 133 protected void onDraw(Canvas canvas) { 134 super.onDraw(canvas); 135 drawBackground(canvas); 136 } 137 138 private void drawBackground(Canvas canvas) { 139 canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, 140 mBackgroundPaint); 141 } 142 143 @Override 144 protected void onFinishInflate() { 145 super.onFinishInflate(); 146 mLogo = (ImageView) findViewById(R.id.search_logo); 147 } 148 149 @Override 150 protected void onLayout(boolean changed, int l, int t, int r, int b) { 151 mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); 152 if (changed) { 153 updateCircleRect(mStaticRect, mStaticOffset, true); 154 } 155 } 156 157 public void animateCircleSize(float circleSize, long duration, 158 long startDelay, Interpolator interpolator) { 159 if (circleSize == mCircleAnimationEndValue) { 160 return; 161 } 162 if (mCircleAnimator != null) { 163 mCircleAnimator.cancel(); 164 } 165 mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); 166 mCircleAnimator.addUpdateListener(mCircleUpdateListener); 167 mCircleAnimator.addListener(mClearAnimatorListener); 168 mCircleAnimator.setInterpolator(interpolator); 169 mCircleAnimator.setDuration(duration); 170 mCircleAnimator.setStartDelay(startDelay); 171 mCircleAnimator.start(); 172 mCircleAnimationEndValue = circleSize; 173 } 174 175 private void applyCircleSize(float circleSize) { 176 mCircleSize = circleSize; 177 updateLayout(); 178 } 179 180 private void updateElevation() { 181 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 182 t = 1.0f - Math.max(t, 0.0f); 183 float offset = t * mMaxElevation; 184 setElevation(offset); 185 } 186 187 /** 188 * Animates the offset to the edge of the screen. 189 * 190 * @param offset The offset to apply. 191 * @param startDelay The desired start delay if animated. 192 * 193 * @param interpolator The desired interpolator if animated. If null, 194 * a default interpolator will be taken designed for appearing or 195 * disappearing. 196 */ 197 private void animateOffset(float offset, long duration, long startDelay, 198 Interpolator interpolator) { 199 if (mOffsetAnimator != null) { 200 mOffsetAnimator.removeAllListeners(); 201 mOffsetAnimator.cancel(); 202 } 203 mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); 204 mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); 205 mOffsetAnimator.addListener(new AnimatorListenerAdapter() { 206 @Override 207 public void onAnimationEnd(Animator animation) { 208 mOffsetAnimator = null; 209 } 210 }); 211 mOffsetAnimator.setInterpolator(interpolator); 212 mOffsetAnimator.setStartDelay(startDelay); 213 mOffsetAnimator.setDuration(duration); 214 mOffsetAnimator.start(); 215 } 216 217 private void updateLayout() { 218 updateCircleRect(); 219 updateLogo(); 220 invalidateOutline(); 221 invalidate(); 222 updateClipping(); 223 } 224 225 private void updateClipping() { 226 boolean clip = mCircleSize < mCircleMinSize; 227 if (clip != mClipToOutline) { 228 setClipToOutline(clip); 229 mClipToOutline = clip; 230 } 231 } 232 233 private void updateLogo() { 234 float translationX = (mCircleRect.left + mCircleRect.right) / 2.0f - mLogo.getWidth() / 2.0f; 235 float translationY = (mCircleRect.top + mCircleRect.bottom) / 2.0f 236 - mLogo.getHeight() / 2.0f - mCircleMinSize / 7f; 237 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 238 translationY += t * mStaticOffset * 0.1f; 239 float alpha = 1.0f-t; 240 alpha = Math.max((alpha - 0.5f) * 2.0f, 0); 241 mLogo.setImageAlpha((int) (alpha * 255)); 242 mLogo.setTranslationX(translationX); 243 mLogo.setTranslationY(translationY); 244 } 245 246 private void updateCircleRect() { 247 updateCircleRect(mCircleRect, mOffset, false); 248 } 249 250 private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { 251 int left, top; 252 float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; 253 left = (int) (getWidth() - circleSize) / 2; 254 top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); 255 rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); 256 } 257 258 public void startExitAnimation(long delay) { 259 animateCircleSize(0, 200, delay, mDisappearInterpolator); 260 animateOffset(0, 200, delay, mDisappearInterpolator); 261 } 262 263 public void startEnterAnimation() { 264 applyCircleSize(0); 265 post(new Runnable() { 266 @Override 267 public void run() { 268 animateCircleSize(mCircleMinSize, 300, 0 /* delay */, mOvershootInterpolator); 269 animateOffset(mStaticOffset, 400, 0 /* delay */, mAppearInterpolator); 270 } 271 }); 272 } 273 274 public void reset() { 275 mClipToOutline = false; 276 mBackgroundPaint.setAlpha(255); 277 mOutlineAlpha = 1.0f; 278 } 279 280 @Override 281 public boolean hasOverlappingRendering() { 282 // not really true but it's ok during an animation, as it's never permanent 283 return false; 284 } 285 } 286