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