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