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 import android.view.ViewParent;
27 
28 public class FocusIndicatorView extends View implements View.OnFocusChangeListener {
29 
30     // It can be any number >0. The view is resized using scaleX and scaleY.
31     static final int DEFAULT_LAYOUT_SIZE = 100;
32     private static final float MIN_VISIBLE_ALPHA = 0.2f;
33     private static final long ANIM_DURATION = 150;
34 
35     private static final int[] sTempPos = new int[2];
36     private static final int[] sTempShift = new int[2];
37 
38     private final int[] mIndicatorPos = new int[2];
39     private final int[] mTargetViewPos = new int[2];
40 
41     private ObjectAnimator mCurrentAnimation;
42     private ViewAnimState mTargetState;
43 
44     private View mLastFocusedView;
45     private boolean mInitiated;
46 
47     private Pair<View, Boolean> mPendingCall;
48 
FocusIndicatorView(Context context)49     public FocusIndicatorView(Context context) {
50         this(context, null);
51     }
52 
FocusIndicatorView(Context context, AttributeSet attrs)53     public FocusIndicatorView(Context context, AttributeSet attrs) {
54         super(context, attrs);
55         setAlpha(0);
56         setBackgroundColor(getResources().getColor(R.color.focused_background));
57     }
58 
59     @Override
onSizeChanged(int w, int h, int oldw, int oldh)60     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
61         super.onSizeChanged(w, h, oldw, oldh);
62 
63         // Redraw if it is already showing. This avoids a bug where the height changes by a small
64         // amount on connecting/disconnecting a bluetooth keyboard.
65         if (mLastFocusedView != null) {
66             mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE);
67             invalidate();
68         }
69     }
70 
71     @Override
onFocusChange(View v, boolean hasFocus)72     public void onFocusChange(View v, boolean hasFocus) {
73         mPendingCall = null;
74         if (!mInitiated && (getWidth() == 0)) {
75             // View not yet laid out. Wait until the view is ready to be drawn, so that be can
76             // get the location on screen.
77             mPendingCall = Pair.create(v, hasFocus);
78             invalidate();
79             return;
80         }
81 
82         if (!mInitiated) {
83             getLocationRelativeToParentPagedView(this, mIndicatorPos);
84             mInitiated = true;
85         }
86 
87         if (hasFocus) {
88             int indicatorWidth = getWidth();
89             int indicatorHeight = getHeight();
90 
91             endCurrentAnimation();
92             ViewAnimState nextState = new ViewAnimState();
93             nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
94             nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
95 
96             getLocationRelativeToParentPagedView(v, mTargetViewPos);
97             nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
98             nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
99 
100             if (getAlpha() > MIN_VISIBLE_ALPHA) {
101                 mTargetState = nextState;
102                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
103                         PropertyValuesHolder.ofFloat(View.ALPHA, 1),
104                         PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mTargetState.x),
105                         PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, mTargetState.y),
106                         PropertyValuesHolder.ofFloat(View.SCALE_X, mTargetState.scaleX),
107                         PropertyValuesHolder.ofFloat(View.SCALE_Y, mTargetState.scaleY));
108             } else {
109                 applyState(nextState);
110                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
111                         PropertyValuesHolder.ofFloat(View.ALPHA, 1));
112             }
113             mLastFocusedView = v;
114         } else {
115             if (mLastFocusedView == v) {
116                 mLastFocusedView = null;
117                 endCurrentAnimation();
118                 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
119                         PropertyValuesHolder.ofFloat(View.ALPHA, 0));
120             }
121         }
122         if (mCurrentAnimation != null) {
123             mCurrentAnimation.setDuration(ANIM_DURATION).start();
124         }
125     }
126 
endCurrentAnimation()127     private void endCurrentAnimation() {
128         if (mCurrentAnimation != null) {
129             mCurrentAnimation.cancel();
130             mCurrentAnimation = null;
131         }
132         if (mTargetState != null) {
133             applyState(mTargetState);
134             mTargetState = null;
135         }
136     }
137 
applyState(ViewAnimState state)138     private void applyState(ViewAnimState state) {
139         setTranslationX(state.x);
140         setTranslationY(state.y);
141         setScaleX(state.scaleX);
142         setScaleY(state.scaleY);
143     }
144 
145     @Override
onDraw(Canvas canvas)146     protected void onDraw(Canvas canvas) {
147         if (mPendingCall != null) {
148             onFocusChange(mPendingCall.first, mPendingCall.second);
149         }
150     }
151 
152     /**
153      * Gets the location of a view relative in the window, off-setting any shift due to
154      * page view scroll
155      */
getLocationRelativeToParentPagedView(View v, int[] pos)156     private static void getLocationRelativeToParentPagedView(View v, int[] pos) {
157         getPagedViewScrollShift(v, sTempShift);
158         v.getLocationInWindow(sTempPos);
159         pos[0] = sTempPos[0] + sTempShift[0];
160         pos[1] = sTempPos[1] + sTempShift[1];
161     }
162 
getPagedViewScrollShift(View child, int[] shift)163     private static void getPagedViewScrollShift(View child, int[] shift) {
164         ViewParent parent = child.getParent();
165         if (parent instanceof PagedView) {
166             View parentView = (View) parent;
167             child.getLocationInWindow(sTempPos);
168             shift[0] = parentView.getPaddingLeft() - sTempPos[0];
169             shift[1] = -(int) child.getTranslationY();
170         } else if (parent instanceof View) {
171             getPagedViewScrollShift((View) parent, shift);
172         } else {
173             shift[0] = shift[1] = 0;
174         }
175     }
176 
177     private static final class ViewAnimState {
178         float x, y, scaleX, scaleY;
179     }
180 }
181