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