1 /* 2 * Copyright (C) 2014 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.systemui.qs; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Typeface; 24 import android.graphics.drawable.Animatable; 25 import android.graphics.drawable.Drawable; 26 import android.graphics.drawable.RippleDrawable; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.Message; 30 import android.util.MathUtils; 31 import android.util.TypedValue; 32 import android.view.Gravity; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.ImageView; 36 import android.widget.ImageView.ScaleType; 37 import android.widget.TextView; 38 39 import com.android.systemui.FontSizeUtils; 40 import com.android.systemui.R; 41 import com.android.systemui.qs.QSTile.AnimationIcon; 42 import com.android.systemui.qs.QSTile.State; 43 44 import java.util.Objects; 45 46 /** View that represents a standard quick settings tile. **/ 47 public class QSTileView extends ViewGroup { 48 private static final Typeface CONDENSED = Typeface.create("sans-serif-condensed", 49 Typeface.NORMAL); 50 51 protected final Context mContext; 52 private final View mIcon; 53 private final View mDivider; 54 private final H mHandler = new H(); 55 private final int mIconSizePx; 56 private final int mTileSpacingPx; 57 private int mTilePaddingTopPx; 58 private final int mTilePaddingBelowIconPx; 59 private final int mDualTileVerticalPaddingPx; 60 private final View mTopBackgroundView; 61 62 private TextView mLabel; 63 private QSDualTileLabel mDualLabel; 64 private boolean mDual; 65 private OnClickListener mClickPrimary; 66 private OnClickListener mClickSecondary; 67 private OnLongClickListener mLongClick; 68 private Drawable mTileBackground; 69 private RippleDrawable mRipple; 70 QSTileView(Context context)71 public QSTileView(Context context) { 72 super(context); 73 74 mContext = context; 75 final Resources res = context.getResources(); 76 mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_tile_icon_size); 77 mTileSpacingPx = res.getDimensionPixelSize(R.dimen.qs_tile_spacing); 78 mTilePaddingBelowIconPx = res.getDimensionPixelSize(R.dimen.qs_tile_padding_below_icon); 79 mDualTileVerticalPaddingPx = 80 res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical); 81 mTileBackground = newTileBackground(); 82 recreateLabel(); 83 setClipChildren(false); 84 85 mTopBackgroundView = new View(context); 86 mTopBackgroundView.setId(View.generateViewId()); 87 addView(mTopBackgroundView); 88 89 mIcon = createIcon(); 90 addView(mIcon); 91 92 mDivider = new View(mContext); 93 mDivider.setBackgroundColor(context.getColor(R.color.qs_tile_divider)); 94 final int dh = res.getDimensionPixelSize(R.dimen.qs_tile_divider_height); 95 mDivider.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, dh)); 96 addView(mDivider); 97 98 setClickable(true); 99 updateTopPadding(); 100 setId(View.generateViewId()); 101 } 102 updateTopPadding()103 private void updateTopPadding() { 104 Resources res = getResources(); 105 int padding = res.getDimensionPixelSize(R.dimen.qs_tile_padding_top); 106 int largePadding = res.getDimensionPixelSize(R.dimen.qs_tile_padding_top_large_text); 107 float largeFactor = (MathUtils.constrain(getResources().getConfiguration().fontScale, 108 1.0f, FontSizeUtils.LARGE_TEXT_SCALE) - 1f) / (FontSizeUtils.LARGE_TEXT_SCALE - 1f); 109 mTilePaddingTopPx = Math.round((1 - largeFactor) * padding + largeFactor * largePadding); 110 requestLayout(); 111 } 112 113 @Override onConfigurationChanged(Configuration newConfig)114 protected void onConfigurationChanged(Configuration newConfig) { 115 super.onConfigurationChanged(newConfig); 116 updateTopPadding(); 117 FontSizeUtils.updateFontSize(mLabel, R.dimen.qs_tile_text_size); 118 if (mDualLabel != null) { 119 mDualLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 120 getResources().getDimensionPixelSize(R.dimen.qs_tile_text_size)); 121 } 122 } 123 recreateLabel()124 private void recreateLabel() { 125 CharSequence labelText = null; 126 CharSequence labelDescription = null; 127 if (mLabel != null) { 128 labelText = mLabel.getText(); 129 removeView(mLabel); 130 mLabel = null; 131 } 132 if (mDualLabel != null) { 133 labelText = mDualLabel.getText(); 134 labelDescription = mLabel.getContentDescription(); 135 removeView(mDualLabel); 136 mDualLabel = null; 137 } 138 final Resources res = mContext.getResources(); 139 if (mDual) { 140 mDualLabel = new QSDualTileLabel(mContext); 141 mDualLabel.setId(View.generateViewId()); 142 mDualLabel.setBackgroundResource(R.drawable.btn_borderless_rect); 143 mDualLabel.setFirstLineCaret(mContext.getDrawable(R.drawable.qs_dual_tile_caret)); 144 mDualLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); 145 mDualLabel.setPadding(0, mDualTileVerticalPaddingPx, 0, mDualTileVerticalPaddingPx); 146 mDualLabel.setTypeface(CONDENSED); 147 mDualLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 148 res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); 149 mDualLabel.setClickable(true); 150 mDualLabel.setOnClickListener(mClickSecondary); 151 mDualLabel.setFocusable(true); 152 if (labelText != null) { 153 mDualLabel.setText(labelText); 154 } 155 if (labelDescription != null) { 156 mDualLabel.setContentDescription(labelDescription); 157 } 158 addView(mDualLabel); 159 mDualLabel.setAccessibilityTraversalAfter(mTopBackgroundView.getId()); 160 } else { 161 mLabel = new TextView(mContext); 162 mLabel.setTextColor(mContext.getColor(R.color.qs_tile_text)); 163 mLabel.setGravity(Gravity.CENTER_HORIZONTAL); 164 mLabel.setMinLines(2); 165 mLabel.setPadding(0, 0, 0, 0); 166 mLabel.setTypeface(CONDENSED); 167 mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 168 res.getDimensionPixelSize(R.dimen.qs_tile_text_size)); 169 mLabel.setClickable(false); 170 if (labelText != null) { 171 mLabel.setText(labelText); 172 } 173 addView(mLabel); 174 } 175 } 176 setDual(boolean dual)177 public boolean setDual(boolean dual) { 178 final boolean changed = dual != mDual; 179 mDual = dual; 180 if (changed) { 181 recreateLabel(); 182 } 183 if (mTileBackground instanceof RippleDrawable) { 184 setRipple((RippleDrawable) mTileBackground); 185 } 186 if (dual) { 187 mTopBackgroundView.setOnClickListener(mClickPrimary); 188 setOnClickListener(null); 189 setClickable(false); 190 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 191 mTopBackgroundView.setBackground(mTileBackground); 192 } else { 193 mTopBackgroundView.setOnClickListener(null); 194 mTopBackgroundView.setClickable(false); 195 setOnClickListener(mClickPrimary); 196 setOnLongClickListener(mLongClick); 197 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 198 setBackground(mTileBackground); 199 } 200 mTopBackgroundView.setFocusable(dual); 201 setFocusable(!dual); 202 mDivider.setVisibility(dual ? VISIBLE : GONE); 203 postInvalidate(); 204 return changed; 205 } 206 setRipple(RippleDrawable tileBackground)207 private void setRipple(RippleDrawable tileBackground) { 208 mRipple = tileBackground; 209 if (getWidth() != 0) { 210 updateRippleSize(getWidth(), getHeight()); 211 } 212 } 213 init(OnClickListener clickPrimary, OnClickListener clickSecondary, OnLongClickListener longClick)214 public void init(OnClickListener clickPrimary, OnClickListener clickSecondary, 215 OnLongClickListener longClick) { 216 mClickPrimary = clickPrimary; 217 mClickSecondary = clickSecondary; 218 mLongClick = longClick; 219 } 220 createIcon()221 protected View createIcon() { 222 final ImageView icon = new ImageView(mContext); 223 icon.setId(android.R.id.icon); 224 icon.setScaleType(ScaleType.CENTER_INSIDE); 225 return icon; 226 } 227 newTileBackground()228 private Drawable newTileBackground() { 229 final int[] attrs = new int[] { android.R.attr.selectableItemBackgroundBorderless }; 230 final TypedArray ta = mContext.obtainStyledAttributes(attrs); 231 final Drawable d = ta.getDrawable(0); 232 ta.recycle(); 233 return d; 234 } 235 labelView()236 private View labelView() { 237 return mDual ? mDualLabel : mLabel; 238 } 239 240 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)241 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 242 final int w = MeasureSpec.getSize(widthMeasureSpec); 243 final int h = MeasureSpec.getSize(heightMeasureSpec); 244 final int iconSpec = exactly(mIconSizePx); 245 mIcon.measure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST), iconSpec); 246 labelView().measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.AT_MOST)); 247 if (mDual) { 248 mDivider.measure(widthMeasureSpec, exactly(mDivider.getLayoutParams().height)); 249 } 250 int heightSpec = exactly( 251 mIconSizePx + mTilePaddingBelowIconPx + mTilePaddingTopPx); 252 mTopBackgroundView.measure(widthMeasureSpec, heightSpec); 253 setMeasuredDimension(w, h); 254 } 255 exactly(int size)256 private static int exactly(int size) { 257 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 258 } 259 260 @Override onLayout(boolean changed, int l, int t, int r, int b)261 protected void onLayout(boolean changed, int l, int t, int r, int b) { 262 final int w = getMeasuredWidth(); 263 final int h = getMeasuredHeight(); 264 265 layout(mTopBackgroundView, 0, mTileSpacingPx); 266 267 int top = 0; 268 top += mTileSpacingPx; 269 top += mTilePaddingTopPx; 270 final int iconLeft = (w - mIcon.getMeasuredWidth()) / 2; 271 layout(mIcon, iconLeft, top); 272 if (mRipple != null) { 273 updateRippleSize(w, h); 274 275 } 276 top = mIcon.getBottom(); 277 top += mTilePaddingBelowIconPx; 278 if (mDual) { 279 layout(mDivider, 0, top); 280 top = mDivider.getBottom(); 281 } 282 layout(labelView(), 0, top); 283 } 284 updateRippleSize(int width, int height)285 private void updateRippleSize(int width, int height) { 286 // center the touch feedback on the center of the icon, and dial it down a bit 287 final int cx = width / 2; 288 final int cy = mDual ? mIcon.getTop() + mIcon.getHeight() / 2 : height / 2; 289 final int rad = (int)(mIcon.getHeight() * 1.25f); 290 mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad); 291 } 292 layout(View child, int left, int top)293 private static void layout(View child, int left, int top) { 294 child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); 295 } 296 handleStateChanged(QSTile.State state)297 protected void handleStateChanged(QSTile.State state) { 298 if (mIcon instanceof ImageView) { 299 setIcon((ImageView) mIcon, state); 300 } 301 if (mDual) { 302 mDualLabel.setText(state.label); 303 mDualLabel.setContentDescription(state.dualLabelContentDescription); 304 mTopBackgroundView.setContentDescription(state.contentDescription); 305 } else { 306 mLabel.setText(state.label); 307 setContentDescription(state.contentDescription); 308 } 309 } 310 setIcon(ImageView iv, QSTile.State state)311 protected void setIcon(ImageView iv, QSTile.State state) { 312 if (!Objects.equals(state.icon, iv.getTag(R.id.qs_icon_tag))) { 313 Drawable d = state.icon != null ? state.icon.getDrawable(mContext) : null; 314 if (d != null && state.autoMirrorDrawable) { 315 d.setAutoMirrored(true); 316 } 317 iv.setImageDrawable(d); 318 iv.setTag(R.id.qs_icon_tag, state.icon); 319 if (d instanceof Animatable) { 320 Animatable a = (Animatable) d; 321 if (state.icon instanceof AnimationIcon && !iv.isShown()) { 322 a.stop(); // skip directly to end state 323 } 324 } 325 } 326 } 327 onStateChanged(QSTile.State state)328 public void onStateChanged(QSTile.State state) { 329 mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget(); 330 } 331 332 /** 333 * Update the accessibility order for this view. 334 * 335 * @param previousView the view which should be before this one 336 * @return the last view in this view which is accessible 337 */ updateAccessibilityOrder(View previousView)338 public View updateAccessibilityOrder(View previousView) { 339 View firstView; 340 View lastView; 341 if (mDual) { 342 lastView = mDualLabel; 343 firstView = mTopBackgroundView; 344 } else { 345 firstView = this; 346 lastView = this; 347 } 348 firstView.setAccessibilityTraversalAfter(previousView.getId()); 349 return lastView; 350 } 351 352 private class H extends Handler { 353 private static final int STATE_CHANGED = 1; H()354 public H() { 355 super(Looper.getMainLooper()); 356 } 357 @Override handleMessage(Message msg)358 public void handleMessage(Message msg) { 359 if (msg.what == STATE_CHANGED) { 360 handleStateChanged((State) msg.obj); 361 } 362 } 363 } 364 } 365