blob: a732a253f5a3677cbe185945364ed3d3c55a0123 [file] [log] [blame]
Jason Monkc133d262015-10-27 12:32:45 -04001/*
Jason Monk702e2eb2017-03-03 16:53:44 -05002 * Copyright (C) 2017 The Android Open Source Project
Jason Monkc133d262015-10-27 12:32:45 -04003 *
Jason Monk702e2eb2017-03-03 16:53:44 -05004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
Jason Monkc133d262015-10-27 12:32:45 -04006 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
Jason Monk702e2eb2017-03-03 16:53:44 -05009 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
Jason Monkc133d262015-10-27 12:32:45 -040013 */
Jason Monk702e2eb2017-03-03 16:53:44 -050014package com.android.systemui.qs.tileimpl;
Jason Monkc133d262015-10-27 12:32:45 -040015
Jason Monkcb2307c2018-01-15 14:19:45 -050016import static com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH;
17
Jason Monkcb2307c2018-01-15 14:19:45 -050018import android.animation.ValueAnimator;
Jason Monkc133d262015-10-27 12:32:45 -040019import android.content.Context;
Jason Monkcb2307c2018-01-15 14:19:45 -050020import android.content.res.ColorStateList;
Jason Monkdc35dcb2015-12-04 16:36:15 -050021import android.content.res.TypedArray;
Fabian Kozynski1ab7ede2018-12-13 15:05:31 -050022import android.graphics.Color;
Fabian Kozynski487e4592018-12-06 14:05:11 -050023import android.graphics.Path;
24import android.graphics.drawable.AdaptiveIconDrawable;
Jason Monkdc35dcb2015-12-04 16:36:15 -050025import android.graphics.drawable.Drawable;
26import android.graphics.drawable.RippleDrawable;
Fabian Kozynski487e4592018-12-06 14:05:11 -050027import android.graphics.drawable.ShapeDrawable;
28import android.graphics.drawable.shapes.PathShape;
Jason Monkc133d262015-10-27 12:32:45 -040029import android.os.Handler;
30import android.os.Looper;
31import android.os.Message;
Jason Monke2f05d22017-03-23 10:45:37 -040032import android.service.quicksettings.Tile;
Julia Reynolds20aef8a2016-05-04 16:44:08 -040033import android.text.TextUtils;
Jason Monkcb2307c2018-01-15 14:19:45 -050034import android.util.Log;
Fabian Kozynski487e4592018-12-06 14:05:11 -050035import android.util.PathParser;
Jason Monk32508852017-01-18 09:17:13 -050036import android.view.Gravity;
Jason Monkc133d262015-10-27 12:32:45 -040037import android.view.View;
Jason Monk32508852017-01-18 09:17:13 -050038import android.view.ViewGroup;
Julia Reynolds20aef8a2016-05-04 16:44:08 -040039import android.view.accessibility.AccessibilityEvent;
40import android.view.accessibility.AccessibilityNodeInfo;
Jason Monk32508852017-01-18 09:17:13 -050041import android.widget.FrameLayout;
Jason Monkcb2307c2018-01-15 14:19:45 -050042import android.widget.ImageView;
Julia Reynolds20aef8a2016-05-04 16:44:08 -040043import android.widget.Switch;
Winsonc0d70582016-01-29 10:24:39 -080044
Jason Monkcb2307c2018-01-15 14:19:45 -050045import com.android.settingslib.Utils;
Jason Monkdc35dcb2015-12-04 16:36:15 -050046import com.android.systemui.R;
Jason Monkcb2307c2018-01-15 14:19:45 -050047import com.android.systemui.plugins.qs.QSIconView;
48import com.android.systemui.plugins.qs.QSTile;
Jason Monk90950d92017-06-20 13:16:05 -040049import com.android.systemui.plugins.qs.QSTile.BooleanState;
Jason Monkc133d262015-10-27 12:32:45 -040050
Jason Monk702e2eb2017-03-03 16:53:44 -050051public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
Jason Monkc133d262015-10-27 12:32:45 -040052
Jason Monk32508852017-01-18 09:17:13 -050053 private static final String TAG = "QSTileBaseView";
Fabian Kozynski487e4592018-12-06 14:05:11 -050054 private static final int ICON_MASK_ID = com.android.internal.R.string.config_icon_mask;
Jason Monkc133d262015-10-27 12:32:45 -040055 private final H mHandler = new H();
Amin Shaikh32d786f2018-08-01 17:08:26 -040056 private final int[] mLocInScreen = new int[2];
Jason Monkf13413e2017-02-15 15:49:32 -050057 private final FrameLayout mIconFrame;
Jiaquan He074ae462016-05-13 08:56:19 -070058 protected QSIconView mIcon;
59 protected RippleDrawable mRipple;
Jason Monkdc35dcb2015-12-04 16:36:15 -050060 private Drawable mTileBackground;
Julia Reynolds20aef8a2016-05-04 16:44:08 -040061 private String mAccessibilityClass;
62 private boolean mTileState;
63 private boolean mCollapsedView;
Jason Monk90950d92017-06-20 13:16:05 -040064 private boolean mClicked;
shawnlin2e7d4e42019-04-08 17:52:15 +080065 private boolean mShowRippleEffect = true;
Jason Monkc133d262015-10-27 12:32:45 -040066
Jason Monkcb2307c2018-01-15 14:19:45 -050067 private final ImageView mBg;
68 private final int mColorActive;
69 private final int mColorInactive;
70 private final int mColorDisabled;
71 private int mCircleColor;
Fabian Kozynski487e4592018-12-06 14:05:11 -050072 private int mBgSize;
Jason Monkcb2307c2018-01-15 14:19:45 -050073
Jason Monkdc35dcb2015-12-04 16:36:15 -050074 public QSTileBaseView(Context context, QSIconView icon) {
Julia Reynolds20aef8a2016-05-04 16:44:08 -040075 this(context, icon, false);
76 }
77
78 public QSTileBaseView(Context context, QSIconView icon, boolean collapsedView) {
Jason Monkc133d262015-10-27 12:32:45 -040079 super(context);
Jason Monk32508852017-01-18 09:17:13 -050080 // Default to Quick Tile padding, and QSTileView will specify its own padding.
81 int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
Jason Monkf13413e2017-02-15 15:49:32 -050082 mIconFrame = new FrameLayout(context);
Jason Monk32508852017-01-18 09:17:13 -050083 int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
Jason Monkf13413e2017-02-15 15:49:32 -050084 addView(mIconFrame, new LayoutParams(size, size));
Jason Monkcb2307c2018-01-15 14:19:45 -050085 mBg = new ImageView(getContext());
Fabian Kozynski487e4592018-12-06 14:05:11 -050086 Path path = new Path(PathParser.createPathFromPathData(
87 context.getResources().getString(ICON_MASK_ID)));
88 float pathSize = AdaptiveIconDrawable.MASK_SIZE;
89 PathShape p = new PathShape(path, pathSize, pathSize);
90 ShapeDrawable d = new ShapeDrawable(p);
Fabian Kozynski1ab7ede2018-12-13 15:05:31 -050091 d.setTintList(ColorStateList.valueOf(Color.TRANSPARENT));
Fabian Kozynski487e4592018-12-06 14:05:11 -050092 int bgSize = context.getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size);
93 d.setIntrinsicHeight(bgSize);
94 d.setIntrinsicWidth(bgSize);
Fabian Kozynski487e4592018-12-06 14:05:11 -050095 mBg.setImageDrawable(d);
Fabian Kozynskibca09b72019-04-04 15:57:42 -040096 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(bgSize, bgSize, Gravity.CENTER);
97 mIconFrame.addView(mBg, lp);
98 mBg.setLayoutParams(lp);
Jason Monkdc35dcb2015-12-04 16:36:15 -050099 mIcon = icon;
Jason Monk32508852017-01-18 09:17:13 -0500100 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
Fabian Kozynskibca09b72019-04-04 15:57:42 -0400101 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
102 Gravity.CENTER);
Jason Monkf13413e2017-02-15 15:49:32 -0500103 mIconFrame.addView(mIcon, params);
Rohan Shah28191fc2018-01-16 14:28:55 -0800104 mIconFrame.setClipChildren(false);
105 mIconFrame.setClipToPadding(false);
Jason Monkdc35dcb2015-12-04 16:36:15 -0500106
107 mTileBackground = newTileBackground();
108 if (mTileBackground instanceof RippleDrawable) {
109 setRipple((RippleDrawable) mTileBackground);
110 }
111 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
112 setBackground(mTileBackground);
113
Jason Changb4e879d2018-04-11 11:17:58 +0800114 mColorActive = Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent);
Jason Monkcb2307c2018-01-15 14:19:45 -0500115 mColorDisabled = Utils.getDisabled(context,
Jason Changb4e879d2018-04-11 11:17:58 +0800116 Utils.getColorAttrDefaultColor(context, android.R.attr.textColorTertiary));
117 mColorInactive = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
Jason Monkcb2307c2018-01-15 14:19:45 -0500118
Jason Monk32508852017-01-18 09:17:13 -0500119 setPadding(0, 0, 0, 0);
Jason Monk162011e2016-02-19 08:11:55 -0500120 setClipChildren(false);
121 setClipToPadding(false);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400122 mCollapsedView = collapsedView;
Min Yunf07f1702016-06-27 13:55:15 -0700123 setFocusable(true);
Jason Monkc133d262015-10-27 12:32:45 -0400124 }
125
Fabian Kozynski487e4592018-12-06 14:05:11 -0500126 public View getBgCircle() {
Jason Monkcb2307c2018-01-15 14:19:45 -0500127 return mBg;
128 }
129
Jiaquan He074ae462016-05-13 08:56:19 -0700130 protected Drawable newTileBackground() {
Jason Monk32508852017-01-18 09:17:13 -0500131 final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless};
132 final TypedArray ta = getContext().obtainStyledAttributes(attrs);
Jason Monkdc35dcb2015-12-04 16:36:15 -0500133 final Drawable d = ta.getDrawable(0);
134 ta.recycle();
135 return d;
Jason Monkc133d262015-10-27 12:32:45 -0400136 }
137
Jason Monkdc35dcb2015-12-04 16:36:15 -0500138 private void setRipple(RippleDrawable tileBackground) {
139 mRipple = tileBackground;
140 if (getWidth() != 0) {
Amin Shaikh30330992018-03-26 17:50:42 -0400141 updateRippleSize();
Jason Monkdc35dcb2015-12-04 16:36:15 -0500142 }
143 }
144
Amin Shaikh30330992018-03-26 17:50:42 -0400145 private void updateRippleSize() {
Jason Monkdc35dcb2015-12-04 16:36:15 -0500146 // center the touch feedback on the center of the icon, and dial it down a bit
Amin Shaikh30330992018-03-26 17:50:42 -0400147 final int cx = mIconFrame.getMeasuredWidth() / 2 + mIconFrame.getLeft();
148 final int cy = mIconFrame.getMeasuredHeight() / 2 + mIconFrame.getTop();
Jason Monk32508852017-01-18 09:17:13 -0500149 final int rad = (int) (mIcon.getHeight() * .85f);
Jason Monkdc35dcb2015-12-04 16:36:15 -0500150 mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad);
151 }
152
Jason Monk702e2eb2017-03-03 16:53:44 -0500153 @Override
154 public void init(QSTile tile) {
155 init(v -> tile.click(), v -> tile.secondaryClick(), view -> {
156 tile.longClick();
157 return true;
158 });
159 }
160
Jason Monk32508852017-01-18 09:17:13 -0500161 public void init(OnClickListener click, OnClickListener secondaryClick,
162 OnLongClickListener longClick) {
Jason Monkdc35dcb2015-12-04 16:36:15 -0500163 setOnClickListener(click);
Jason Monk76c67aa2016-02-19 14:49:42 -0500164 setOnLongClickListener(longClick);
Jason Monkdc35dcb2015-12-04 16:36:15 -0500165 }
166
167 @Override
168 protected void onLayout(boolean changed, int l, int t, int r, int b) {
169 super.onLayout(changed, l, t, r, b);
Jason Monkdc35dcb2015-12-04 16:36:15 -0500170 if (mRipple != null) {
Amin Shaikh30330992018-03-26 17:50:42 -0400171 updateRippleSize();
Jason Monkdc35dcb2015-12-04 16:36:15 -0500172 }
173 }
174
Jorim Jaggic3081172016-04-08 23:56:37 -0700175 @Override
176 public boolean hasOverlappingRendering() {
177 // Avoid layers for this layout - we don't need them.
178 return false;
179 }
180
Jason Monkdc35dcb2015-12-04 16:36:15 -0500181 /**
182 * Update the accessibility order for this view.
183 *
184 * @param previousView the view which should be before this one
185 * @return the last view in this view which is accessible
186 */
187 public View updateAccessibilityOrder(View previousView) {
188 setAccessibilityTraversalAfter(previousView.getId());
189 return this;
190 }
Jason Monkc133d262015-10-27 12:32:45 -0400191
192 public void onStateChanged(QSTile.State state) {
193 mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget();
194 }
195
Jason Monkdc35dcb2015-12-04 16:36:15 -0500196 protected void handleStateChanged(QSTile.State state) {
Jason Monkcb2307c2018-01-15 14:19:45 -0500197 int circleColor = getCircleColor(state.state);
Amin Shaikh32d786f2018-08-01 17:08:26 -0400198 boolean allowAnimations = animationsEnabled();
Jason Monkcb2307c2018-01-15 14:19:45 -0500199 if (circleColor != mCircleColor) {
Amin Shaikh32d786f2018-08-01 17:08:26 -0400200 if (allowAnimations) {
Jason Monkcb2307c2018-01-15 14:19:45 -0500201 ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor)
202 .setDuration(QS_ANIM_LENGTH);
203 animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf(
204 (Integer) animation.getAnimatedValue())));
205 animator.start();
206 } else {
207 QSIconViewImpl.setTint(mBg, circleColor);
208 }
209 mCircleColor = circleColor;
210 }
211
shawnlin2e7d4e42019-04-08 17:52:15 +0800212 mShowRippleEffect = state.showRippleEffect;
Jason Monke2f05d22017-03-23 10:45:37 -0400213 setClickable(state.state != Tile.STATE_UNAVAILABLE);
Fabian Kozynski58d9b862018-10-29 12:27:42 -0400214 setLongClickable(state.handlesLongClick);
Amin Shaikh32d786f2018-08-01 17:08:26 -0400215 mIcon.setIcon(state, allowAnimations);
Jason Monk702e2eb2017-03-03 16:53:44 -0500216 setContentDescription(state.contentDescription);
Rohan Shah185f6612018-03-29 17:35:00 -0700217
Fabian Kozynskia0cbcf92018-11-29 09:53:57 -0500218 mAccessibilityClass =
219 state.state == Tile.STATE_UNAVAILABLE ? null : state.expandedAccessibilityClassName;
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400220 if (state instanceof QSTile.BooleanState) {
Jason Monk90950d92017-06-20 13:16:05 -0400221 boolean newState = ((BooleanState) state).value;
222 if (mTileState != newState) {
223 mClicked = false;
224 mTileState = newState;
225 }
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400226 }
Jason Monkc133d262015-10-27 12:32:45 -0400227 }
228
Amin Shaikh32d786f2018-08-01 17:08:26 -0400229 /* The view should not be animated if it's not on screen and no part of it is visible.
230 */
Amin Shaikh5df1a752018-04-03 13:39:53 -0400231 protected boolean animationsEnabled() {
Amin Shaikh32d786f2018-08-01 17:08:26 -0400232 if (!isShown()) {
233 return false;
234 }
235 if (getAlpha() != 1f) {
236 return false;
237 }
238 getLocationOnScreen(mLocInScreen);
239 return mLocInScreen[1] >= -getHeight();
Amin Shaikh5df1a752018-04-03 13:39:53 -0400240 }
241
Jason Monkcb2307c2018-01-15 14:19:45 -0500242 private int getCircleColor(int state) {
243 switch (state) {
244 case Tile.STATE_ACTIVE:
245 return mColorActive;
246 case Tile.STATE_INACTIVE:
247 case Tile.STATE_UNAVAILABLE:
248 return mColorDisabled;
249 default:
250 Log.e(TAG, "Invalid state " + state);
251 return 0;
252 }
253 }
254
Jason Monk702e2eb2017-03-03 16:53:44 -0500255 @Override
Jason Monke2f05d22017-03-23 10:45:37 -0400256 public void setClickable(boolean clickable) {
257 super.setClickable(clickable);
shawnlin2e7d4e42019-04-08 17:52:15 +0800258 setBackground(clickable && mShowRippleEffect ? mRipple : null);
Jason Monke2f05d22017-03-23 10:45:37 -0400259 }
260
261 @Override
Jason Monk702e2eb2017-03-03 16:53:44 -0500262 public int getDetailY() {
263 return getTop() + getHeight() / 2;
264 }
265
Jason Monk1aec93f2016-03-01 09:39:30 -0500266 public QSIconView getIcon() {
Jason Monk162011e2016-02-19 08:11:55 -0500267 return mIcon;
268 }
269
Rohan Shaha8401992018-01-25 18:22:44 -0800270 public View getIconWithBackground() {
271 return mIconFrame;
272 }
273
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400274 @Override
Jason Monk90950d92017-06-20 13:16:05 -0400275 public boolean performClick() {
276 mClicked = true;
277 return super.performClick();
278 }
279
280 @Override
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400281 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
282 super.onInitializeAccessibilityEvent(event);
283 if (!TextUtils.isEmpty(mAccessibilityClass)) {
284 event.setClassName(mAccessibilityClass);
285 if (Switch.class.getName().equals(mAccessibilityClass)) {
Jason Monk90950d92017-06-20 13:16:05 -0400286 boolean b = mClicked ? !mTileState : mTileState;
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400287 String label = getResources()
Jason Monk90950d92017-06-20 13:16:05 -0400288 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400289 event.setContentDescription(label);
Jason Monk90950d92017-06-20 13:16:05 -0400290 event.setChecked(b);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400291 }
292 }
293 }
294
295 @Override
296 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
297 super.onInitializeAccessibilityNodeInfo(info);
Amin Shaikhbb2c6c92018-06-12 18:25:43 -0400298 // Clear selected state so it is not announce by talkback.
299 info.setSelected(false);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400300 if (!TextUtils.isEmpty(mAccessibilityClass)) {
301 info.setClassName(mAccessibilityClass);
302 if (Switch.class.getName().equals(mAccessibilityClass)) {
Jason Monk90950d92017-06-20 13:16:05 -0400303 boolean b = mClicked ? !mTileState : mTileState;
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400304 String label = getResources()
Jason Monk90950d92017-06-20 13:16:05 -0400305 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400306 info.setText(label);
Jason Monk90950d92017-06-20 13:16:05 -0400307 info.setChecked(b);
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400308 info.setCheckable(true);
Fabian Kozynski58d9b862018-10-29 12:27:42 -0400309 if (isLongClickable()) {
310 info.addAction(
311 new AccessibilityNodeInfo.AccessibilityAction(
312 AccessibilityNodeInfo.AccessibilityAction
313 .ACTION_LONG_CLICK.getId(),
314 getResources().getString(
315 R.string.accessibility_long_click_tile)));
316 }
Julia Reynolds20aef8a2016-05-04 16:44:08 -0400317 }
318 }
319 }
320
Jason Monkc133d262015-10-27 12:32:45 -0400321 private class H extends Handler {
322 private static final int STATE_CHANGED = 1;
Fabian Kozynski487e4592018-12-06 14:05:11 -0500323
Jason Monkc133d262015-10-27 12:32:45 -0400324 public H() {
325 super(Looper.getMainLooper());
326 }
327
328 @Override
329 public void handleMessage(Message msg) {
330 if (msg.what == STATE_CHANGED) {
331 handleStateChanged((QSTile.State) msg.obj);
332 }
333 }
334 }
Fabian Kozynski487e4592018-12-06 14:05:11 -0500335}