QSPanel.java revision cd542b8a98ea9c648622c9360c251de4be4223e4
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.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.content.Context;
23import android.content.Intent;
24import android.content.res.Resources;
25import android.os.Handler;
26import android.os.Message;
27import android.util.AttributeSet;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.ImageView;
32
33import com.android.systemui.R;
34import com.android.systemui.qs.QSTile.DetailAdapter;
35import com.android.systemui.settings.BrightnessController;
36import com.android.systemui.settings.ToggleSlider;
37import com.android.systemui.statusbar.phone.QSTileHost;
38import com.android.systemui.statusbar.policy.BrightnessMirrorController;
39
40import java.util.ArrayList;
41import java.util.Collection;
42
43/** View that represents the quick settings tile panel. **/
44public class QSPanel extends ViewGroup {
45    private static final float TILE_ASPECT = 1.2f;
46
47    private final Context mContext;
48    private final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
49    private final View mDetail;
50    private final ViewGroup mDetailContent;
51    private final View mDetailSettingsButton;
52    private final View mDetailDoneButton;
53    private final View mBrightnessView;
54    private final QSDetailClipper mClipper;
55    private final H mHandler = new H();
56
57    private int mColumns;
58    private int mCellWidth;
59    private int mCellHeight;
60    private int mLargeCellWidth;
61    private int mLargeCellHeight;
62    private int mPanelPaddingBottom;
63    private int mDualTileUnderlap;
64    private int mBrightnessPaddingTop;
65    private boolean mExpanded;
66    private boolean mListening;
67
68    private Record mDetailRecord;
69    private Callback mCallback;
70    private BrightnessController mBrightnessController;
71    private QSTileHost mHost;
72
73    private QSFooter mFooter;
74
75    public QSPanel(Context context) {
76        this(context, null);
77    }
78
79    public QSPanel(Context context, AttributeSet attrs) {
80        super(context, attrs);
81        mContext = context;
82
83        mDetail = LayoutInflater.from(context).inflate(R.layout.qs_detail, this, false);
84        mDetailContent = (ViewGroup) mDetail.findViewById(android.R.id.content);
85        mDetailSettingsButton = mDetail.findViewById(android.R.id.button2);
86        mDetailDoneButton = mDetail.findViewById(android.R.id.button1);
87        mDetail.setVisibility(GONE);
88        mDetail.setClickable(true);
89        mBrightnessView = LayoutInflater.from(context).inflate(
90                R.layout.quick_settings_brightness_dialog, this, false);
91        mFooter = new QSFooter(this, context);
92        addView(mDetail);
93        addView(mBrightnessView);
94        addView(mFooter.getView());
95        mClipper = new QSDetailClipper(mDetail);
96        updateResources();
97
98        mBrightnessController = new BrightnessController(getContext(),
99                (ImageView) findViewById(R.id.brightness_icon),
100                (ToggleSlider) findViewById(R.id.brightness_slider));
101
102        mDetailDoneButton.setOnClickListener(new OnClickListener() {
103            @Override
104            public void onClick(View v) {
105                closeDetail();
106            }
107        });
108    }
109
110    public void setBrightnessMirror(BrightnessMirrorController c) {
111        super.onFinishInflate();
112        ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider);
113        ToggleSlider mirror = (ToggleSlider) c.getMirror().findViewById(R.id.brightness_slider);
114        brightnessSlider.setMirror(mirror);
115        brightnessSlider.setMirrorController(c);
116    }
117
118    public void setCallback(Callback callback) {
119        mCallback = callback;
120    }
121
122    public void setHost(QSTileHost host) {
123        mHost = host;
124        mFooter.setHost(host);
125    }
126
127    public QSTileHost getHost() {
128        return mHost;
129    }
130
131    public void updateResources() {
132        final Resources res = mContext.getResources();
133        final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
134        mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height);
135        mCellWidth = (int)(mCellHeight * TILE_ASPECT);
136        mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height);
137        mLargeCellWidth = (int)(mLargeCellHeight * TILE_ASPECT);
138        mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
139        mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical);
140        mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top);
141        if (mColumns != columns) {
142            mColumns = columns;
143            postInvalidate();
144        }
145        if (mListening) {
146            refreshAllTiles();
147        }
148    }
149
150    public void setExpanded(boolean expanded) {
151        if (mExpanded == expanded) return;
152        mExpanded = expanded;
153        if (!mExpanded) {
154            closeDetail();
155        }
156    }
157
158    public void setListening(boolean listening) {
159        if (mListening == listening) return;
160        mListening = listening;
161        for (TileRecord r : mRecords) {
162            r.tile.setListening(mListening);
163        }
164        mFooter.setListening(mListening);
165        if (mListening) {
166            refreshAllTiles();
167        }
168        if (listening) {
169            mBrightnessController.registerCallbacks();
170        } else {
171            mBrightnessController.unregisterCallbacks();
172        }
173    }
174
175    private void refreshAllTiles() {
176        for (TileRecord r : mRecords) {
177            r.tile.refreshState();
178        }
179        mFooter.refreshState();
180    }
181
182    public void showDetailAdapter(boolean show, DetailAdapter adapter) {
183        Record r = new Record();
184        r.detailAdapter = adapter;
185        showDetail(show, r);
186    }
187
188    private void showDetail(boolean show, Record r) {
189        mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
190    }
191
192    private void setTileVisibility(View v, boolean visible) {
193        mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visible ? 1 : 0, 0, v).sendToTarget();
194    }
195
196    private void handleSetTileVisibility(View v, boolean visible) {
197        if (visible == (v.getVisibility() == VISIBLE)) return;
198        v.setVisibility(visible ? VISIBLE : GONE);
199    }
200
201    public void setTiles(Collection<QSTile<?>> tiles) {
202        for (TileRecord record : mRecords) {
203            removeView(record.tileView);
204        }
205        mRecords.clear();
206        for (QSTile<?> tile : tiles) {
207            addTile(tile);
208        }
209        if (isShowingDetail()) {
210            mDetail.bringToFront();
211        }
212    }
213
214    private void addTile(final QSTile<?> tile) {
215        final TileRecord r = new TileRecord();
216        r.tile = tile;
217        r.tileView = tile.createTileView(mContext);
218        r.tileView.setVisibility(View.GONE);
219        final QSTile.Callback callback = new QSTile.Callback() {
220            @Override
221            public void onStateChanged(QSTile.State state) {
222                setTileVisibility(r.tileView, state.visible);
223                r.tileView.onStateChanged(state);
224            }
225            @Override
226            public void onShowDetail(boolean show) {
227                QSPanel.this.showDetail(show, r);
228            }
229            @Override
230            public void onToggleStateChanged(boolean state) {
231                if (mDetailRecord == r) {
232                    fireToggleStateChanged(state);
233                }
234            }
235            @Override
236            public void onScanStateChanged(boolean state) {
237                if (mDetailRecord == r) {
238                    fireScanStateChanged(state);
239                }
240            }
241        };
242        r.tile.setCallback(callback);
243        final View.OnClickListener click = new View.OnClickListener() {
244            @Override
245            public void onClick(View v) {
246                r.tile.click();
247            }
248        };
249        final View.OnClickListener clickSecondary = new View.OnClickListener() {
250            @Override
251            public void onClick(View v) {
252                r.tile.secondaryClick();
253            }
254        };
255        r.tileView.init(click, clickSecondary);
256        r.tile.setListening(mListening);
257        callback.onStateChanged(r.tile.getState());
258        r.tile.refreshState();
259        mRecords.add(r);
260
261        addView(r.tileView);
262    }
263
264    public boolean isShowingDetail() {
265        return mDetailRecord != null;
266    }
267
268    public void closeDetail() {
269        showDetail(false, mDetailRecord);
270    }
271
272    private void handleShowDetail(Record r, boolean show) {
273        if (r instanceof TileRecord) {
274            handleShowDetailTile((TileRecord) r, show);
275        } else {
276            handleShowDetailImpl(r, show, getWidth() /* x */, 0/* y */);
277        }
278    }
279
280    private void handleShowDetailTile(TileRecord r, boolean show) {
281        if ((mDetailRecord != null) == show) return;
282
283        if (show) {
284            r.detailAdapter = r.tile.getDetailAdapter();
285            if (r.detailAdapter == null) return;
286        }
287        int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
288        int y = r.tileView.getTop() + r.tileView.getHeight() / 2;
289        handleShowDetailImpl(r, show, x, y);
290    }
291
292    private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
293        if ((mDetailRecord != null) == show) return;  // already in right state
294        DetailAdapter detailAdapter = null;
295        AnimatorListener listener = null;
296        if (show) {
297            detailAdapter = r.detailAdapter;
298            r.detailView = detailAdapter.createDetailView(mContext, r.detailView, mDetailContent);
299            if (r.detailView == null) throw new IllegalStateException("Must return detail view");
300
301            final Intent settingsIntent = detailAdapter.getSettingsIntent();
302            mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
303            mDetailSettingsButton.setOnClickListener(new OnClickListener() {
304                @Override
305                public void onClick(View v) {
306                    mHost.startSettingsActivity(settingsIntent);
307                }
308            });
309
310            mDetailContent.removeAllViews();
311            mDetail.bringToFront();
312            mDetailContent.addView(r.detailView);
313            mDetailRecord = r;
314        } else {
315            listener = mTeardownDetailWhenDone;
316        }
317        fireShowingDetail(show ? detailAdapter : null);
318        mClipper.animateCircularClip(x, y, show, listener);
319    }
320
321    @Override
322    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
323        final int width = MeasureSpec.getSize(widthMeasureSpec);
324        mBrightnessView.measure(exactly(width), MeasureSpec.UNSPECIFIED);
325        final int brightnessHeight = mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop;
326        mFooter.getView().measure(exactly(width), MeasureSpec.UNSPECIFIED);
327        int r = -1;
328        int c = -1;
329        int rows = 0;
330        boolean rowIsDual = false;
331        for (TileRecord record : mRecords) {
332            if (record.tileView.getVisibility() == GONE) continue;
333            // wrap to next column if we've reached the max # of columns
334            // also don't allow dual + single tiles on the same row
335            if (r == -1 || c == (mColumns - 1) || rowIsDual != record.tile.supportsDualTargets()) {
336                r++;
337                c = 0;
338                rowIsDual = record.tile.supportsDualTargets();
339            } else {
340                c++;
341            }
342            record.row = r;
343            record.col = c;
344            rows = r + 1;
345        }
346
347        for (TileRecord record : mRecords) {
348            if (record.tileView.getVisibility() == GONE) continue;
349            record.tileView.setDual(record.tile.supportsDualTargets());
350            final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth;
351            final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight;
352            record.tileView.measure(exactly(cw), exactly(ch));
353        }
354        int h = rows == 0 ? brightnessHeight : (getRowTop(rows) + mPanelPaddingBottom);
355        if (mFooter.hasFooter()) {
356            h += mFooter.getView().getMeasuredHeight();
357        }
358        mDetail.measure(exactly(width), MeasureSpec.UNSPECIFIED);
359        if (mDetail.getMeasuredHeight() < h) {
360            mDetail.measure(exactly(width), exactly(h));
361        }
362        setMeasuredDimension(width, Math.max(h, mDetail.getMeasuredHeight()));
363    }
364
365    private static int exactly(int size) {
366        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
367    }
368
369    @Override
370    protected void onLayout(boolean changed, int l, int t, int r, int b) {
371        final int w = getWidth();
372        mBrightnessView.layout(0, mBrightnessPaddingTop,
373                mBrightnessView.getMeasuredWidth(),
374                mBrightnessPaddingTop + mBrightnessView.getMeasuredHeight());
375        for (TileRecord record : mRecords) {
376            if (record.tileView.getVisibility() == GONE) continue;
377            final int cols = getColumnCount(record.row);
378            final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth;
379            final int extra = (w - cw * cols) / (cols + 1);
380            final int left = record.col * cw + (record.col + 1) * extra;
381            final int top = getRowTop(record.row);
382            record.tileView.layout(left, top,
383                    left + record.tileView.getMeasuredWidth(),
384                    top + record.tileView.getMeasuredHeight());
385        }
386        final int dh = Math.max(mDetail.getMeasuredHeight(), getMeasuredHeight());
387        mDetail.layout(0, 0, mDetail.getMeasuredWidth(), dh);
388        if (mFooter.hasFooter()) {
389            View footer = mFooter.getView();
390            footer.layout(0, getMeasuredHeight() - footer.getMeasuredHeight(),
391                    footer.getMeasuredWidth(), getMeasuredHeight());
392        }
393    }
394
395    private int getRowTop(int row) {
396        if (row <= 0) return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop;
397        return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop
398                + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight;
399    }
400
401    private int getColumnCount(int row) {
402        int cols = 0;
403        for (TileRecord record : mRecords) {
404            if (record.tileView.getVisibility() == GONE) continue;
405            if (record.row == row) cols++;
406        }
407        return cols;
408    }
409
410    private void fireShowingDetail(QSTile.DetailAdapter detail) {
411        if (mCallback != null) {
412            mCallback.onShowingDetail(detail);
413        }
414    }
415
416    private void fireToggleStateChanged(boolean state) {
417        if (mCallback != null) {
418            mCallback.onToggleStateChanged(state);
419        }
420    }
421
422    private void fireScanStateChanged(boolean state) {
423        if (mCallback != null) {
424            mCallback.onScanStateChanged(state);
425        }
426    }
427
428    private class H extends Handler {
429        private static final int SHOW_DETAIL = 1;
430        private static final int SET_TILE_VISIBILITY = 2;
431        @Override
432        public void handleMessage(Message msg) {
433            if (msg.what == SHOW_DETAIL) {
434                handleShowDetail((Record)msg.obj, msg.arg1 != 0);
435            } else if (msg.what == SET_TILE_VISIBILITY) {
436                handleSetTileVisibility((View)msg.obj, msg.arg1 != 0);
437            }
438        }
439    }
440
441    private static class Record {
442        View detailView;
443        DetailAdapter detailAdapter;
444    }
445
446    private static final class TileRecord extends Record {
447        QSTile<?> tile;
448        QSTileView tileView;
449        int row;
450        int col;
451    }
452
453    private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
454        public void onAnimationEnd(Animator animation) {
455            mDetailContent.removeAllViews();
456            mDetailRecord = null;
457        };
458    };
459
460    public interface Callback {
461        void onShowingDetail(QSTile.DetailAdapter detail);
462        void onToggleStateChanged(boolean state);
463        void onScanStateChanged(boolean state);
464    }
465}
466