Home | History | Annotate | Download | only in quickcontact
      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 
     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
     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 
    106     public View getChild() {
    107         return mChild;
    108     }
    109 
    110     /**
    111      * FloatingChildLayout manages its own background, don't set it.
    112      */
    113     @Override
    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      */
    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      */
    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
    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 
    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 
    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
    183     public void setBackgroundColorAlpha(int alpha) {
    184         setBackgroundColor(alpha << 24);
    185     }
    186 
    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 
    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 
    214     public boolean isContentFullyVisible() {
    215         return mForegroundPhase == ForegroundPhase.IDLE;
    216     }
    217 
    218     /** Begin animating {@link #getChild()} visible. */
    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      */
    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 
    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 */
    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 
    289     public void setOnOutsideTouchListener(View.OnTouchListener listener) {
    290         mOutsideTouchListener = listener;
    291     }
    292 
    293     @Override
    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