blob: ecf93e4b3d60aeea52f4e01ae205616e944a5b60 [file] [log] [blame]
Sunny Goyaldcbcc862014-08-12 15:58:36 -07001/*
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
17package com.android.launcher3;
18
Sunny Goyal5edd6bf2014-10-14 16:12:12 -070019import android.animation.ObjectAnimator;
20import android.animation.PropertyValuesHolder;
Sunny Goyaldcbcc862014-08-12 15:58:36 -070021import android.content.Context;
22import android.graphics.Canvas;
23import android.util.AttributeSet;
24import android.util.Pair;
25import android.view.View;
Sunny Goyaldcbcc862014-08-12 15:58:36 -070026
Adam Cohen091440a2015-03-18 14:16:05 -070027import com.android.launcher3.util.Thunk;
28
Sunny Goyaldcbcc862014-08-12 15:58:36 -070029public 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 private static final float MIN_VISIBLE_ALPHA = 0.2f;
Sunny Goyal5edd6bf2014-10-14 16:12:12 -070034 private static final long ANIM_DURATION = 150;
Sunny Goyaldcbcc862014-08-12 15:58:36 -070035
Sunny Goyaldcbcc862014-08-12 15:58:36 -070036 private final int[] mIndicatorPos = new int[2];
37 private final int[] mTargetViewPos = new int[2];
38
Sunny Goyal5edd6bf2014-10-14 16:12:12 -070039 private ObjectAnimator mCurrentAnimation;
40 private ViewAnimState mTargetState;
41
Sunny Goyaldcbcc862014-08-12 15:58:36 -070042 private View mLastFocusedView;
43 private boolean mInitiated;
44
45 private Pair<View, Boolean> mPendingCall;
46
47 public FocusIndicatorView(Context context) {
48 this(context, null);
49 }
50
51 public FocusIndicatorView(Context context, AttributeSet attrs) {
52 super(context, attrs);
53 setAlpha(0);
54 setBackgroundColor(getResources().getColor(R.color.focused_background));
55 }
56
57 @Override
58 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
59 super.onSizeChanged(w, h, oldw, oldh);
60
61 // Redraw if it is already showing. This avoids a bug where the height changes by a small
62 // amount on connecting/disconnecting a bluetooth keyboard.
63 if (mLastFocusedView != null) {
64 mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE);
65 invalidate();
66 }
67 }
68
69 @Override
70 public void onFocusChange(View v, boolean hasFocus) {
71 mPendingCall = null;
72 if (!mInitiated && (getWidth() == 0)) {
73 // View not yet laid out. Wait until the view is ready to be drawn, so that be can
74 // get the location on screen.
75 mPendingCall = Pair.create(v, hasFocus);
76 invalidate();
77 return;
78 }
79
80 if (!mInitiated) {
Sunny Goyala39b82e2015-03-05 13:43:28 -080081 // The parent view should always the a parent of the target view.
Sunny Goyalee995102015-03-05 16:27:37 -080082 computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos);
Sunny Goyaldcbcc862014-08-12 15:58:36 -070083 mInitiated = true;
84 }
85
86 if (hasFocus) {
87 int indicatorWidth = getWidth();
88 int indicatorHeight = getHeight();
89
Sunny Goyal5edd6bf2014-10-14 16:12:12 -070090 endCurrentAnimation();
91 ViewAnimState nextState = new ViewAnimState();
92 nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
93 nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
Sunny Goyaldcbcc862014-08-12 15:58:36 -070094
Sunny Goyalee995102015-03-05 16:27:37 -080095 computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos);
Sunny Goyal5edd6bf2014-10-14 16:12:12 -070096 nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
97 nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
Sunny Goyaldcbcc862014-08-12 15:58:36 -070098
99 if (getAlpha() > MIN_VISIBLE_ALPHA) {
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700100 mTargetState = nextState;
101 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
102 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
103 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mTargetState.x),
104 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, mTargetState.y),
105 PropertyValuesHolder.ofFloat(View.SCALE_X, mTargetState.scaleX),
106 PropertyValuesHolder.ofFloat(View.SCALE_Y, mTargetState.scaleY));
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700107 } else {
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700108 applyState(nextState);
109 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
110 PropertyValuesHolder.ofFloat(View.ALPHA, 1));
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700111 }
112 mLastFocusedView = v;
113 } else {
114 if (mLastFocusedView == v) {
115 mLastFocusedView = null;
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700116 endCurrentAnimation();
117 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
118 PropertyValuesHolder.ofFloat(View.ALPHA, 0));
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700119 }
120 }
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700121 if (mCurrentAnimation != null) {
122 mCurrentAnimation.setDuration(ANIM_DURATION).start();
123 }
124 }
125
126 private void endCurrentAnimation() {
127 if (mCurrentAnimation != null) {
128 mCurrentAnimation.cancel();
129 mCurrentAnimation = null;
130 }
131 if (mTargetState != null) {
132 applyState(mTargetState);
133 mTargetState = null;
134 }
135 }
136
137 private void applyState(ViewAnimState state) {
138 setTranslationX(state.x);
139 setTranslationY(state.y);
140 setScaleX(state.scaleX);
141 setScaleY(state.scaleY);
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700142 }
143
144 @Override
145 protected void onDraw(Canvas canvas) {
146 if (mPendingCall != null) {
147 onFocusChange(mPendingCall.first, mPendingCall.second);
148 }
149 }
150
151 /**
Sunny Goyal059228a2015-04-07 15:30:26 -0700152 * Computes the location of a view relative to {@param parent}, off-setting
Sunny Goyala39b82e2015-03-05 13:43:28 -0800153 * any shift due to page view scroll.
154 * @param pos an array of two integers in which to hold the coordinates
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700155 */
Sunny Goyala39b82e2015-03-05 13:43:28 -0800156 private static void computeLocationRelativeToParent(View v, View parent, int[] pos) {
157 pos[0] = pos[1] = 0;
158 computeLocationRelativeToParentHelper(v, parent, pos);
159
160 // If a view is scaled, its position will also shift accordingly. For optimization, only
161 // consider this for the last node.
162 pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2;
163 pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2;
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700164 }
165
Sunny Goyala39b82e2015-03-05 13:43:28 -0800166 private static void computeLocationRelativeToParentHelper(View child,
167 View commonParent, int[] shift) {
168 View parent = (View) child.getParent();
Sunny Goyala39b82e2015-03-05 13:43:28 -0800169 shift[0] += child.getLeft();
170 shift[1] += child.getTop();
Sunny Goyal059228a2015-04-07 15:30:26 -0700171 if (parent instanceof PagedView) {
172 PagedView page = (PagedView) parent;
173 shift[0] -= page.getScrollForPage(page.indexOfChild(child));
174 }
Sunny Goyala39b82e2015-03-05 13:43:28 -0800175
176 if (parent != commonParent) {
177 computeLocationRelativeToParentHelper(parent, commonParent, shift);
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700178 }
179 }
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700180
Adam Cohen091440a2015-03-18 14:16:05 -0700181 @Thunk static final class ViewAnimState {
Sunny Goyal5edd6bf2014-10-14 16:12:12 -0700182 float x, y, scaleX, scaleY;
183 }
Sunny Goyaldcbcc862014-08-12 15:58:36 -0700184}