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