QSTileView.java revision c737b9bd5a0bc60fd92150d55f8ddb530697b987
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
17package com.android.systemui.qs;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Typeface;
24import android.graphics.drawable.Drawable;
25import android.graphics.drawable.RippleDrawable;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.Message;
29import android.util.MathUtils;
30import android.util.TypedValue;
31import android.view.Gravity;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.ImageView;
35import android.widget.ImageView.ScaleType;
36import android.widget.TextView;
37
38import com.android.systemui.FontSizeUtils;
39import com.android.systemui.R;
40import com.android.systemui.qs.QSTile.State;
41
42/** View that represents a standard quick settings tile. **/
43public 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        } else {
302            mLabel.setText(state.label);
303        }
304        setContentDescription(state.contentDescription);
305    }
306
307    public void onStateChanged(QSTile.State state) {
308        mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget();
309    }
310
311    private class H extends Handler {
312        private static final int STATE_CHANGED = 1;
313        public H() {
314            super(Looper.getMainLooper());
315        }
316        @Override
317        public void handleMessage(Message msg) {
318            if (msg.what == STATE_CHANGED) {
319                handleStateChanged((State) msg.obj);
320            }
321        }
322    }
323}
324