1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * 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 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * 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. 13 */ 14package com.android.systemui.qs.tileimpl; 15 16import static com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH; 17 18import android.animation.ValueAnimator; 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.TypedArray; 22import android.graphics.drawable.Drawable; 23import android.graphics.drawable.RippleDrawable; 24import android.os.Handler; 25import android.os.Looper; 26import android.os.Message; 27import android.service.quicksettings.Tile; 28import android.text.TextUtils; 29import android.util.Log; 30import android.view.Gravity; 31import android.view.View; 32import android.view.ViewGroup; 33import android.view.accessibility.AccessibilityEvent; 34import android.view.accessibility.AccessibilityNodeInfo; 35import android.widget.FrameLayout; 36import android.widget.ImageView; 37import android.widget.ImageView.ScaleType; 38import android.widget.Switch; 39 40import com.android.settingslib.Utils; 41import com.android.systemui.R; 42import com.android.systemui.plugins.qs.QSIconView; 43import com.android.systemui.plugins.qs.QSTile; 44import com.android.systemui.plugins.qs.QSTile.BooleanState; 45 46public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { 47 48 private static final String TAG = "QSTileBaseView"; 49 private final H mHandler = new H(); 50 private final FrameLayout mIconFrame; 51 protected QSIconView mIcon; 52 protected RippleDrawable mRipple; 53 private Drawable mTileBackground; 54 private String mAccessibilityClass; 55 private boolean mTileState; 56 private boolean mCollapsedView; 57 private boolean mClicked; 58 59 private final ImageView mBg; 60 private final int mColorActive; 61 private final int mColorInactive; 62 private final int mColorDisabled; 63 private int mCircleColor; 64 65 public QSTileBaseView(Context context, QSIconView icon) { 66 this(context, icon, false); 67 } 68 69 public QSTileBaseView(Context context, QSIconView icon, boolean collapsedView) { 70 super(context); 71 // Default to Quick Tile padding, and QSTileView will specify its own padding. 72 int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding); 73 74 mIconFrame = new FrameLayout(context); 75 mIconFrame.setForegroundGravity(Gravity.CENTER); 76 int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size); 77 addView(mIconFrame, new LayoutParams(size, size)); 78 mBg = new ImageView(getContext()); 79 mBg.setScaleType(ScaleType.FIT_CENTER); 80 mBg.setImageResource(R.drawable.ic_qs_circle); 81 mIconFrame.addView(mBg); 82 mIcon = icon; 83 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 84 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 85 params.setMargins(0, padding, 0, padding); 86 mIconFrame.addView(mIcon, params); 87 mIconFrame.setClipChildren(false); 88 mIconFrame.setClipToPadding(false); 89 90 mTileBackground = newTileBackground(); 91 if (mTileBackground instanceof RippleDrawable) { 92 setRipple((RippleDrawable) mTileBackground); 93 } 94 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 95 setBackground(mTileBackground); 96 97 mColorActive = Utils.getColorAttr(context, android.R.attr.colorAccent); 98 mColorDisabled = Utils.getDisabled(context, 99 Utils.getColorAttr(context, android.R.attr.textColorTertiary)); 100 mColorInactive = Utils.getColorAttr(context, android.R.attr.textColorSecondary); 101 102 setPadding(0, 0, 0, 0); 103 setClipChildren(false); 104 setClipToPadding(false); 105 mCollapsedView = collapsedView; 106 setFocusable(true); 107 } 108 109 public View getBgCicle() { 110 return mBg; 111 } 112 113 protected Drawable newTileBackground() { 114 final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless}; 115 final TypedArray ta = getContext().obtainStyledAttributes(attrs); 116 final Drawable d = ta.getDrawable(0); 117 ta.recycle(); 118 return d; 119 } 120 121 private void setRipple(RippleDrawable tileBackground) { 122 mRipple = tileBackground; 123 if (getWidth() != 0) { 124 updateRippleSize(); 125 } 126 } 127 128 private void updateRippleSize() { 129 // center the touch feedback on the center of the icon, and dial it down a bit 130 final int cx = mIconFrame.getMeasuredWidth() / 2 + mIconFrame.getLeft(); 131 final int cy = mIconFrame.getMeasuredHeight() / 2 + mIconFrame.getTop(); 132 final int rad = (int) (mIcon.getHeight() * .85f); 133 mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad); 134 } 135 136 @Override 137 public void init(QSTile tile) { 138 init(v -> tile.click(), v -> tile.secondaryClick(), view -> { 139 tile.longClick(); 140 return true; 141 }); 142 } 143 144 public void init(OnClickListener click, OnClickListener secondaryClick, 145 OnLongClickListener longClick) { 146 setOnClickListener(click); 147 setOnLongClickListener(longClick); 148 } 149 150 @Override 151 protected void onLayout(boolean changed, int l, int t, int r, int b) { 152 super.onLayout(changed, l, t, r, b); 153 if (mRipple != null) { 154 updateRippleSize(); 155 } 156 } 157 158 @Override 159 public boolean hasOverlappingRendering() { 160 // Avoid layers for this layout - we don't need them. 161 return false; 162 } 163 164 /** 165 * Update the accessibility order for this view. 166 * 167 * @param previousView the view which should be before this one 168 * @return the last view in this view which is accessible 169 */ 170 public View updateAccessibilityOrder(View previousView) { 171 setAccessibilityTraversalAfter(previousView.getId()); 172 return this; 173 } 174 175 public void onStateChanged(QSTile.State state) { 176 mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); 177 } 178 179 protected void handleStateChanged(QSTile.State state) { 180 int circleColor = getCircleColor(state.state); 181 if (circleColor != mCircleColor) { 182 if (mBg.isShown() && animationsEnabled()) { 183 ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor) 184 .setDuration(QS_ANIM_LENGTH); 185 animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf( 186 (Integer) animation.getAnimatedValue()))); 187 animator.start(); 188 } else { 189 QSIconViewImpl.setTint(mBg, circleColor); 190 } 191 mCircleColor = circleColor; 192 } 193 194 setClickable(state.state != Tile.STATE_UNAVAILABLE); 195 mIcon.setIcon(state); 196 setContentDescription(state.contentDescription); 197 198 mAccessibilityClass = state.expandedAccessibilityClassName; 199 if (state instanceof QSTile.BooleanState) { 200 boolean newState = ((BooleanState) state).value; 201 if (mTileState != newState) { 202 mClicked = false; 203 mTileState = newState; 204 } 205 } 206 } 207 208 protected boolean animationsEnabled() { 209 return true; 210 } 211 212 private int getCircleColor(int state) { 213 switch (state) { 214 case Tile.STATE_ACTIVE: 215 return mColorActive; 216 case Tile.STATE_INACTIVE: 217 case Tile.STATE_UNAVAILABLE: 218 return mColorDisabled; 219 default: 220 Log.e(TAG, "Invalid state " + state); 221 return 0; 222 } 223 } 224 225 @Override 226 public void setClickable(boolean clickable) { 227 super.setClickable(clickable); 228 setBackground(clickable ? mRipple : null); 229 } 230 231 @Override 232 public int getDetailY() { 233 return getTop() + getHeight() / 2; 234 } 235 236 public QSIconView getIcon() { 237 return mIcon; 238 } 239 240 public View getIconWithBackground() { 241 return mIconFrame; 242 } 243 244 @Override 245 public boolean performClick() { 246 mClicked = true; 247 return super.performClick(); 248 } 249 250 @Override 251 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 252 super.onInitializeAccessibilityEvent(event); 253 if (!TextUtils.isEmpty(mAccessibilityClass)) { 254 event.setClassName(mAccessibilityClass); 255 if (Switch.class.getName().equals(mAccessibilityClass)) { 256 boolean b = mClicked ? !mTileState : mTileState; 257 String label = getResources() 258 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off); 259 event.setContentDescription(label); 260 event.setChecked(b); 261 } 262 } 263 } 264 265 @Override 266 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 267 super.onInitializeAccessibilityNodeInfo(info); 268 if (!TextUtils.isEmpty(mAccessibilityClass)) { 269 info.setClassName(mAccessibilityClass); 270 if (Switch.class.getName().equals(mAccessibilityClass)) { 271 boolean b = mClicked ? !mTileState : mTileState; 272 String label = getResources() 273 .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off); 274 info.setText(label); 275 info.setChecked(b); 276 info.setCheckable(true); 277 info.addAction( 278 new AccessibilityNodeInfo.AccessibilityAction( 279 AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.getId(), 280 getResources().getString(R.string.accessibility_long_click_tile))); 281 } 282 } 283 } 284 285 private class H extends Handler { 286 private static final int STATE_CHANGED = 1; 287 public H() { 288 super(Looper.getMainLooper()); 289 } 290 291 @Override 292 public void handleMessage(Message msg) { 293 if (msg.what == STATE_CHANGED) { 294 handleStateChanged((QSTile.State) msg.obj); 295 } 296 } 297 } 298} 299