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 
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         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
onSizeChanged(int w, int h, int oldw, int oldh)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      */
getHideIndicatorOnFocusListener()84     public View.OnFocusChangeListener getHideIndicatorOnFocusListener() {
85         return mHideIndicatorOnFocusListener;
86     }
87 
88     @Override
onFocusChange(View v, boolean hasFocus)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 
endCurrentAnimation()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 
applyState(ViewAnimState state)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
onDraw(Canvas canvas)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      */
computeLocationRelativeToParent(View v, View parent, int[] pos)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 
computeLocationRelativeToParentHelper(View child, View commonParent, int[] shift)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