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 17package com.android.launcher3; 18 19import android.animation.ObjectAnimator; 20import android.animation.PropertyValuesHolder; 21import android.content.Context; 22import android.graphics.Canvas; 23import android.util.AttributeSet; 24import android.util.Pair; 25import android.view.View; 26 27import com.android.launcher3.util.Thunk; 28 29public 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; 34 private static final long ANIM_DURATION = 150; 35 36 private final int[] mIndicatorPos = new int[2]; 37 private final int[] mTargetViewPos = new int[2]; 38 39 private ObjectAnimator mCurrentAnimation; 40 private ViewAnimState mTargetState; 41 42 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) { 81 // The parent view should always the a parent of the target view. 82 computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos); 83 mInitiated = true; 84 } 85 86 if (hasFocus) { 87 int indicatorWidth = getWidth(); 88 int indicatorHeight = getHeight(); 89 90 endCurrentAnimation(); 91 ViewAnimState nextState = new ViewAnimState(); 92 nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth; 93 nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight; 94 95 computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos); 96 nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2; 97 nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2; 98 99 if (getAlpha() > MIN_VISIBLE_ALPHA) { 100 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)); 107 } else { 108 applyState(nextState); 109 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this, 110 PropertyValuesHolder.ofFloat(View.ALPHA, 1)); 111 } 112 mLastFocusedView = v; 113 } else { 114 if (mLastFocusedView == v) { 115 mLastFocusedView = null; 116 endCurrentAnimation(); 117 mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this, 118 PropertyValuesHolder.ofFloat(View.ALPHA, 0)); 119 } 120 } 121 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); 142 } 143 144 @Override 145 protected void onDraw(Canvas canvas) { 146 if (mPendingCall != null) { 147 onFocusChange(mPendingCall.first, mPendingCall.second); 148 } 149 } 150 151 /** 152 * Computes the location of a view relative to {@param parent}, off-setting 153 * any shift due to page view scroll. 154 * @param pos an array of two integers in which to hold the coordinates 155 */ 156 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; 164 } 165 166 private static void computeLocationRelativeToParentHelper(View child, 167 View commonParent, int[] shift) { 168 View parent = (View) child.getParent(); 169 shift[0] += child.getLeft(); 170 shift[1] += child.getTop(); 171 if (parent instanceof PagedView) { 172 PagedView page = (PagedView) parent; 173 shift[0] -= page.getScrollForPage(page.indexOfChild(child)); 174 } 175 176 if (parent != commonParent) { 177 computeLocationRelativeToParentHelper(parent, commonParent, shift); 178 } 179 } 180 181 @Thunk static final class ViewAnimState { 182 float x, y, scaleX, scaleY; 183 } 184} 185