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 
     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