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 21 import android.animation.Animator; 22 import android.animation.Animator.AnimatorListener; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.util.AttributeSet; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewPropertyAnimator; 31 import android.view.animation.AnimationUtils; 32 import android.widget.FrameLayout; 33 import android.widget.PopupWindow; 34 35 /** 36 * Layout containing single child {@link View} which it attempts to center 37 * around {@link #setChildTargetScreen(Rect)}. 38 * <p> 39 * Updates drawable state to be {@link android.R.attr#state_first} when child is 40 * above target, and {@link android.R.attr#state_last} when child is below 41 * target. Also updates {@link Drawable#setLevel(int)} on child 42 * {@link View#getBackground()} to reflect horizontal center of target. 43 * <p> 44 * The reason for this approach is because target {@link Rect} is in screen 45 * coordinates disregarding decor insets; otherwise something like 46 * {@link PopupWindow} might work better. 47 */ 48 public class FloatingChildLayout extends FrameLayout { 49 private static final String TAG = "FloatingChild"; 50 private int mFixedTopPosition; 51 private View mChild; 52 private Rect mTargetScreen = new Rect(); 53 private final int mAnimationDuration; 54 55 public FloatingChildLayout(Context context, AttributeSet attrs) { 56 super(context, attrs); 57 final Resources resources = getResources(); 58 mFixedTopPosition = 59 resources.getDimensionPixelOffset(R.dimen.quick_contact_top_position); 60 mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime); 61 } 62 63 @Override 64 protected void onFinishInflate() { 65 mChild = findViewById(android.R.id.content); 66 mChild.setDuplicateParentStateEnabled(true); 67 68 // this will be expanded in showChild() 69 mChild.setScaleX(0.0f); 70 mChild.setScaleY(0.0f); 71 mChild.setAlpha(0.0f); 72 } 73 74 public View getChild() { 75 return mChild; 76 } 77 78 /** 79 * Set {@link Rect} in screen coordinates that {@link #getChild()} should be 80 * centered around. 81 */ 82 public void setChildTargetScreen(Rect targetScreen) { 83 mTargetScreen = targetScreen; 84 requestLayout(); 85 } 86 87 /** 88 * Return {@link #mTargetScreen} in local window coordinates, taking any 89 * decor insets into account. 90 */ 91 private Rect getTargetInWindow() { 92 final Rect windowScreen = new Rect(); 93 getWindowVisibleDisplayFrame(windowScreen); 94 95 final Rect target = new Rect(mTargetScreen); 96 target.offset(-windowScreen.left, -windowScreen.top); 97 return target; 98 } 99 100 @Override 101 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 102 103 final View child = mChild; 104 final Rect target = getTargetInWindow(); 105 106 final int childWidth = child.getMeasuredWidth(); 107 final int childHeight = child.getMeasuredHeight(); 108 109 if (mFixedTopPosition != -1) { 110 // Horizontally centered, vertically fixed position 111 final int childLeft = (getWidth() - childWidth) / 2; 112 final int childTop = mFixedTopPosition; 113 layoutChild(child, childLeft, childTop); 114 } else { 115 // default is centered horizontally around target... 116 final int childLeft = target.centerX() - (childWidth / 2); 117 // ... and vertically aligned a bit below centered 118 final int childTop = target.centerY() - Math.round(childHeight * 0.35f); 119 120 // when child is outside bounds, nudge back inside 121 final int clampedChildLeft = clampDimension(childLeft, childWidth, getWidth()); 122 final int clampedChildTop = clampDimension(childTop, childHeight, getHeight()); 123 124 layoutChild(child, clampedChildLeft, clampedChildTop); 125 } 126 } 127 128 private static int clampDimension(int value, int size, int max) { 129 // when larger than bounds, just center 130 if (size > max) { 131 return (max - size) / 2; 132 } 133 134 // clamp to bounds 135 return Math.min(Math.max(value, 0), max - size); 136 } 137 138 private static void layoutChild(View child, int left, int top) { 139 child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); 140 } 141 142 /** Begin animating {@link #getChild()} visible. */ 143 public void showChild(Runnable onAnimationEndRunnable) { 144 animateScale(false, onAnimationEndRunnable); 145 } 146 147 /** Begin animating {@link #getChild()} invisible. */ 148 public void hideChild(Runnable onAnimationEndRunnable) { 149 animateScale(true, onAnimationEndRunnable); 150 } 151 152 /** Creates the open/close animation */ 153 private void animateScale(boolean isExitAnimation, final Runnable onAnimationEndRunnable) { 154 mChild.setPivotX(mTargetScreen.centerX() - mChild.getLeft()); 155 mChild.setPivotY(mTargetScreen.centerY() - mChild.getTop()); 156 ViewPropertyAnimator animator = mChild.animate(); 157 animator.setDuration(mAnimationDuration); 158 final int scaleInterpolator = isExitAnimation ? android.R.interpolator.accelerate_quint 159 : android.R.interpolator.decelerate_quint; 160 animator.setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator)); 161 final float scaleTarget = isExitAnimation ? 0.5f : 1.0f; 162 animator.scaleX(scaleTarget); 163 animator.scaleY(scaleTarget); 164 animator.alpha(isExitAnimation ? 0.0f : 1.0f); 165 166 if (onAnimationEndRunnable != null) { 167 animator.setListener(new AnimatorListener() { 168 @Override 169 public void onAnimationStart(Animator animation) {} 170 171 @Override 172 public void onAnimationRepeat(Animator animation) {} 173 174 @Override 175 public void onAnimationCancel(Animator animation) {} 176 177 @Override 178 public void onAnimationEnd(Animator animation) { 179 onAnimationEndRunnable.run(); 180 } 181 }); 182 } 183 } 184 185 private View.OnTouchListener mOutsideTouchListener; 186 187 public void setOnOutsideTouchListener(View.OnTouchListener listener) { 188 mOutsideTouchListener = listener; 189 } 190 191 @Override 192 public boolean onTouchEvent(MotionEvent event) { 193 // at this point, touch wasn't handled by child view; assume outside 194 if (mOutsideTouchListener != null) { 195 return mOutsideTouchListener.onTouch(this, event); 196 } 197 return false; 198 } 199 } 200