• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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