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