1 /* 2 * Copyright (C) 2011 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.contacts.quickcontact; 18 19 import com.android.contacts.R; 20 import com.android.contacts.test.NeededForReflection; 21 import com.android.contacts.util.SchedulingUtils; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ObjectAnimator; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Rect; 29 import android.graphics.drawable.ColorDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.animation.AnimationUtils; 36 import android.widget.FrameLayout; 37 import android.widget.PopupWindow; 38 39 /** 40 * Layout containing single child {@link View} which it attempts to center 41 * around {@link #setChildTargetScreen(Rect)}. 42 * <p> 43 * Updates drawable state to be {@link android.R.attr#state_first} when child is 44 * above target, and {@link android.R.attr#state_last} when child is below 45 * target. Also updates {@link Drawable#setLevel(int)} on child 46 * {@link View#getBackground()} to reflect horizontal center of target. 47 * <p> 48 * The reason for this approach is because target {@link Rect} is in screen 49 * coordinates disregarding decor insets; otherwise something like 50 * {@link PopupWindow} might work better. 51 */ 52 public class FloatingChildLayout extends FrameLayout { 53 private static final String TAG = "FloatingChildLayout"; 54 private int mFixedTopPosition; 55 private View mChild; 56 private Rect mTargetScreen = new Rect(); 57 private final int mAnimationDuration; 58 59 /** The phase of the background dim. This is one of the values of {@link BackgroundPhase} */ 60 private int mBackgroundPhase = BackgroundPhase.BEFORE; 61 62 private ObjectAnimator mBackgroundAnimator = ObjectAnimator.ofInt(this, 63 "backgroundColorAlpha", 0, DIM_BACKGROUND_ALPHA); 64 65 private interface BackgroundPhase { 66 public static final int BEFORE = 0; 67 public static final int APPEARING_OR_VISIBLE = 1; 68 public static final int DISAPPEARING_OR_GONE = 3; 69 } 70 71 /** The phase of the contents window. This is one of the values of {@link ForegroundPhase} */ 72 private int mForegroundPhase = ForegroundPhase.BEFORE; 73 74 private interface ForegroundPhase { 75 public static final int BEFORE = 0; 76 public static final int APPEARING = 1; 77 public static final int IDLE = 2; 78 public static final int DISAPPEARING = 3; 79 public static final int AFTER = 4; 80 } 81 82 // Black, 50% alpha as per the system default. 83 private static final int DIM_BACKGROUND_ALPHA = 0x7F; 84 FloatingChildLayout(Context context, AttributeSet attrs)85 public FloatingChildLayout(Context context, AttributeSet attrs) { 86 super(context, attrs); 87 final Resources resources = getResources(); 88 mFixedTopPosition = 89 resources.getDimensionPixelOffset(R.dimen.quick_contact_top_position); 90 mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime); 91 92 super.setBackground(new ColorDrawable(0)); 93 } 94 95 @Override onFinishInflate()96 protected void onFinishInflate() { 97 mChild = findViewById(android.R.id.content); 98 mChild.setDuplicateParentStateEnabled(true); 99 100 // this will be expanded in showChild() 101 mChild.setScaleX(0.5f); 102 mChild.setScaleY(0.5f); 103 mChild.setAlpha(0.0f); 104 } 105 getChild()106 public View getChild() { 107 return mChild; 108 } 109 110 /** 111 * FloatingChildLayout manages its own background, don't set it. 112 */ 113 @Override setBackground(Drawable background)114 public void setBackground(Drawable background) { 115 Log.wtf(TAG, "don't setBackground(), it is managed internally"); 116 } 117 118 /** 119 * Set {@link Rect} in screen coordinates that {@link #getChild()} should be 120 * centered around. 121 */ setChildTargetScreen(Rect targetScreen)122 public void setChildTargetScreen(Rect targetScreen) { 123 mTargetScreen = targetScreen; 124 requestLayout(); 125 } 126 127 /** 128 * Return {@link #mTargetScreen} in local window coordinates, taking any 129 * decor insets into account. 130 */ getTargetInWindow()131 private Rect getTargetInWindow() { 132 final Rect windowScreen = new Rect(); 133 getWindowVisibleDisplayFrame(windowScreen); 134 135 final Rect target = new Rect(mTargetScreen); 136 target.offset(-windowScreen.left, -windowScreen.top); 137 return target; 138 } 139 140 @Override onLayout(boolean changed, int left, int top, int right, int bottom)141 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 142 143 final View child = mChild; 144 final Rect target = getTargetInWindow(); 145 146 final int childWidth = child.getMeasuredWidth(); 147 final int childHeight = child.getMeasuredHeight(); 148 149 if (mFixedTopPosition != -1) { 150 // Horizontally centered, vertically fixed position 151 final int childLeft = (getWidth() - childWidth) / 2; 152 final int childTop = mFixedTopPosition; 153 layoutChild(child, childLeft, childTop); 154 } else { 155 // default is centered horizontally around target... 156 final int childLeft = target.centerX() - (childWidth / 2); 157 // ... and vertically aligned a bit below centered 158 final int childTop = target.centerY() - Math.round(childHeight * 0.35f); 159 160 // when child is outside bounds, nudge back inside 161 final int clampedChildLeft = clampDimension(childLeft, childWidth, getWidth()); 162 final int clampedChildTop = clampDimension(childTop, childHeight, getHeight()); 163 164 layoutChild(child, clampedChildLeft, clampedChildTop); 165 } 166 } 167 clampDimension(int value, int size, int max)168 private static int clampDimension(int value, int size, int max) { 169 // when larger than bounds, just center 170 if (size > max) { 171 return (max - size) / 2; 172 } 173 174 // clamp to bounds 175 return Math.min(Math.max(value, 0), max - size); 176 } 177 layoutChild(View child, int left, int top)178 private static void layoutChild(View child, int left, int top) { 179 child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); 180 } 181 182 @NeededForReflection setBackgroundColorAlpha(int alpha)183 public void setBackgroundColorAlpha(int alpha) { 184 setBackgroundColor(alpha << 24); 185 } 186 fadeInBackground()187 public void fadeInBackground() { 188 if (mBackgroundPhase == BackgroundPhase.BEFORE) { 189 mBackgroundPhase = BackgroundPhase.APPEARING_OR_VISIBLE; 190 191 createChildLayer(); 192 193 SchedulingUtils.doAfterDraw(this, new Runnable() { 194 @Override 195 public void run() { 196 mBackgroundAnimator.setDuration(mAnimationDuration).start(); 197 } 198 }); 199 } 200 } 201 fadeOutBackground()202 public void fadeOutBackground() { 203 if (mBackgroundPhase == BackgroundPhase.APPEARING_OR_VISIBLE) { 204 mBackgroundPhase = BackgroundPhase.DISAPPEARING_OR_GONE; 205 if (mBackgroundAnimator.isRunning()) { 206 mBackgroundAnimator.reverse(); 207 } else { 208 ObjectAnimator.ofInt(this, "backgroundColorAlpha", DIM_BACKGROUND_ALPHA, 0). 209 setDuration(mAnimationDuration).start(); 210 } 211 } 212 } 213 isContentFullyVisible()214 public boolean isContentFullyVisible() { 215 return mForegroundPhase == ForegroundPhase.IDLE; 216 } 217 218 /** Begin animating {@link #getChild()} visible. */ showContent(final Runnable onAnimationEndRunnable)219 public void showContent(final Runnable onAnimationEndRunnable) { 220 if (mForegroundPhase == ForegroundPhase.BEFORE) { 221 mForegroundPhase = ForegroundPhase.APPEARING; 222 animateScale(false, onAnimationEndRunnable); 223 } 224 } 225 226 /** 227 * Begin animating {@link #getChild()} invisible. Returns false if animation is not valid in 228 * this state 229 */ hideContent(final Runnable onAnimationEndRunnable)230 public boolean hideContent(final Runnable onAnimationEndRunnable) { 231 if (mForegroundPhase == ForegroundPhase.APPEARING || 232 mForegroundPhase == ForegroundPhase.IDLE) { 233 mForegroundPhase = ForegroundPhase.DISAPPEARING; 234 235 createChildLayer(); 236 237 animateScale(true, onAnimationEndRunnable); 238 return true; 239 } else { 240 return false; 241 } 242 } 243 createChildLayer()244 private void createChildLayer() { 245 mChild.invalidate(); 246 mChild.setLayerType(LAYER_TYPE_HARDWARE, null); 247 mChild.buildLayer(); 248 } 249 250 /** Creates the open/close animation */ animateScale( final boolean isExitAnimation, final Runnable onAnimationEndRunnable)251 private void animateScale( 252 final boolean isExitAnimation, 253 final Runnable onAnimationEndRunnable) { 254 mChild.setPivotX(mTargetScreen.centerX() - mChild.getLeft()); 255 mChild.setPivotY(mTargetScreen.centerY() - mChild.getTop()); 256 257 final int scaleInterpolator = isExitAnimation 258 ? android.R.interpolator.accelerate_quint 259 : android.R.interpolator.decelerate_quint; 260 final float scaleTarget = isExitAnimation ? 0.5f : 1.0f; 261 262 mChild.animate() 263 .setDuration(mAnimationDuration) 264 .setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator)) 265 .scaleX(scaleTarget) 266 .scaleY(scaleTarget) 267 .alpha(isExitAnimation ? 0.0f : 1.0f) 268 .setListener(new AnimatorListenerAdapter() { 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 mChild.setLayerType(LAYER_TYPE_NONE, null); 272 if (isExitAnimation) { 273 if (mForegroundPhase == ForegroundPhase.DISAPPEARING) { 274 mForegroundPhase = ForegroundPhase.AFTER; 275 if (onAnimationEndRunnable != null) onAnimationEndRunnable.run(); 276 } 277 } else { 278 if (mForegroundPhase == ForegroundPhase.APPEARING) { 279 mForegroundPhase = ForegroundPhase.IDLE; 280 if (onAnimationEndRunnable != null) onAnimationEndRunnable.run(); 281 } 282 } 283 } 284 }); 285 } 286 287 private View.OnTouchListener mOutsideTouchListener; 288 setOnOutsideTouchListener(View.OnTouchListener listener)289 public void setOnOutsideTouchListener(View.OnTouchListener listener) { 290 mOutsideTouchListener = listener; 291 } 292 293 @Override onTouchEvent(MotionEvent event)294 public boolean onTouchEvent(MotionEvent event) { 295 // at this point, touch wasn't handled by child view; assume outside 296 if (mOutsideTouchListener != null) { 297 return mOutsideTouchListener.onTouch(this, event); 298 } 299 return false; 300 } 301 } 302