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