• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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