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