Home | History | Annotate | Download | only in launcher3
      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.launcher3;
     18 
     19 import android.animation.ObjectAnimator;
     20 import android.animation.PropertyValuesHolder;
     21 import android.content.Context;
     22 import android.graphics.Canvas;
     23 import android.util.AttributeSet;
     24 import android.util.Pair;
     25 import android.view.View;
     26 
     27 import com.android.launcher3.util.Thunk;
     28 
     29 public class FocusIndicatorView extends View implements View.OnFocusChangeListener {
     30 
     31     // It can be any number >0. The view is resized using scaleX and scaleY.
     32     static final int DEFAULT_LAYOUT_SIZE = 100;
     33 
     34     private static final float MIN_VISIBLE_ALPHA = 0.2f;
     35     private static final long ANIM_DURATION = 150;
     36 
     37     private final int[] mIndicatorPos = new int[2];
     38     private final int[] mTargetViewPos = new int[2];
     39 
     40     private ObjectAnimator mCurrentAnimation;
     41     private ViewAnimState mTargetState;
     42 
     43     private View mLastFocusedView;
     44     private boolean mInitiated;
     45     private final OnFocusChangeListener mHideIndicatorOnFocusListener;
     46 
     47     private Pair<View, Boolean> mPendingCall;
     48 
     49     public FocusIndicatorView(Context context) {
     50         this(context, null);
     51     }
     52 
     53     public FocusIndicatorView(Context context, AttributeSet attrs) {
     54         super(context, attrs);
     55         setAlpha(0);
     56         setBackgroundColor(getResources().getColor(R.color.focused_background));
     57 
     58         mHideIndicatorOnFocusListener = new OnFocusChangeListener() {
     59             @Override
     60             public void onFocusChange(View v, boolean hasFocus) {
     61                 if (hasFocus) {
     62                     endCurrentAnimation();
     63                     setAlpha(0);
     64                 }
     65             }
     66         };
     67     }
     68 
     69     @Override
     70     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
     71         super.onSizeChanged(w, h, oldw, oldh);
     72 
     73         // Redraw if it is already showing. This avoids a bug where the height changes by a small
     74         // amount on connecting/disconnecting a bluetooth keyboard.
     75         if (mLastFocusedView != null) {
     76             mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE);
     77             invalidate();
     78         }
     79     }
     80 
     81     /**
     82      * Sets the alpha of this FocusIndicatorView to 0 when a view with this listener receives focus.
     83      */
     84     public View.OnFocusChangeListener getHideIndicatorOnFocusListener() {
     85         return mHideIndicatorOnFocusListener;
     86     }
     87 
     88     @Override
     89     public void onFocusChange(View v, boolean hasFocus) {
     90         mPendingCall = null;
     91         if (!mInitiated && (getWidth() == 0)) {
     92             // View not yet laid out. Wait until the view is ready to be drawn, so that be can
     93             // get the location on screen.
     94             mPendingCall = Pair.create(v, hasFocus);
     95             invalidate();
     96             return;
     97         }
     98 
     99         if (!mInitiated) {
    100             // The parent view should always the a parent of the target view.
    101             computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos);
    102             mInitiated = true;
    103         }
    104 
    105         if (hasFocus) {
    106             int indicatorWidth = getWidth();
    107             int indicatorHeight = getHeight();
    108 
    109             endCurrentAnimation();
    110             ViewAnimState nextState = new ViewAnimState();
    111             nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
    112             nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
    113 
    114             computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos);
    115             nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
    116             nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
    117 
    118             if (getAlpha() > MIN_VISIBLE_ALPHA) {
    119                 mTargetState = nextState;
    120                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
    121                         PropertyValuesHolder.ofFloat(View.ALPHA, 1),
    122                         PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mTargetState.x),
    123                         PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, mTargetState.y),
    124                         PropertyValuesHolder.ofFloat(View.SCALE_X, mTargetState.scaleX),
    125                         PropertyValuesHolder.ofFloat(View.SCALE_Y, mTargetState.scaleY));
    126             } else {
    127                 applyState(nextState);
    128                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
    129                         PropertyValuesHolder.ofFloat(View.ALPHA, 1));
    130             }
    131             mLastFocusedView = v;
    132         } else {
    133             if (mLastFocusedView == v) {
    134                 mLastFocusedView = null;
    135                 endCurrentAnimation();
    136                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
    137                         PropertyValuesHolder.ofFloat(View.ALPHA, 0));
    138             }
    139         }
    140         if (mCurrentAnimation != null) {
    141             mCurrentAnimation.setDuration(ANIM_DURATION).start();
    142         }
    143     }
    144 
    145     private void endCurrentAnimation() {
    146         if (mCurrentAnimation != null) {
    147             mCurrentAnimation.cancel();
    148             mCurrentAnimation = null;
    149         }
    150         if (mTargetState != null) {
    151             applyState(mTargetState);
    152             mTargetState = null;
    153         }
    154     }
    155 
    156     private void applyState(ViewAnimState state) {
    157         setTranslationX(state.x);
    158         setTranslationY(state.y);
    159         setScaleX(state.scaleX);
    160         setScaleY(state.scaleY);
    161     }
    162 
    163     @Override
    164     protected void onDraw(Canvas canvas) {
    165         if (mPendingCall != null) {
    166             onFocusChange(mPendingCall.first, mPendingCall.second);
    167         }
    168     }
    169 
    170     /**
    171      * Computes the location of a view relative to {@param parent}, off-setting
    172      * any shift due to page view scroll.
    173      * @param pos an array of two integers in which to hold the coordinates
    174      */
    175     private static void computeLocationRelativeToParent(View v, View parent, int[] pos) {
    176         pos[0] = pos[1] = 0;
    177         computeLocationRelativeToParentHelper(v, parent, pos);
    178 
    179         // If a view is scaled, its position will also shift accordingly. For optimization, only
    180         // consider this for the last node.
    181         pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2;
    182         pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2;
    183     }
    184 
    185     private static void computeLocationRelativeToParentHelper(View child,
    186             View commonParent, int[] shift) {
    187         View parent = (View) child.getParent();
    188         shift[0] += child.getLeft();
    189         shift[1] += child.getTop();
    190         if (parent instanceof PagedView) {
    191             PagedView page = (PagedView) parent;
    192             shift[0] -= page.getScrollForPage(page.indexOfChild(child));
    193         }
    194 
    195         if (parent != commonParent) {
    196             computeLocationRelativeToParentHelper(parent, commonParent, shift);
    197         }
    198     }
    199 
    200     @Thunk static final class ViewAnimState {
    201         float x, y, scaleX, scaleY;
    202     }
    203 }
    204