1 /* 2 * Copyright (C) 2008 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 17 package com.android.internal.app; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 21 import com.android.internal.R; 22 23 import android.annotation.Nullable; 24 import android.app.AlertDialog; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.res.TypedArray; 28 import android.database.Cursor; 29 import android.graphics.drawable.Drawable; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.text.TextUtils; 33 import android.util.AttributeSet; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.KeyEvent; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewGroup.LayoutParams; 41 import android.view.ViewParent; 42 import android.view.ViewStub; 43 import android.view.Window; 44 import android.view.WindowInsets; 45 import android.view.WindowManager; 46 import android.widget.AbsListView; 47 import android.widget.AdapterView; 48 import android.widget.AdapterView.OnItemClickListener; 49 import android.widget.ArrayAdapter; 50 import android.widget.Button; 51 import android.widget.CheckedTextView; 52 import android.widget.CursorAdapter; 53 import android.widget.FrameLayout; 54 import android.widget.ImageView; 55 import android.widget.LinearLayout; 56 import android.widget.ListAdapter; 57 import android.widget.ListView; 58 import android.widget.ScrollView; 59 import android.widget.SimpleCursorAdapter; 60 import android.widget.TextView; 61 62 import java.lang.ref.WeakReference; 63 64 public class AlertController { 65 66 private final Context mContext; 67 private final DialogInterface mDialogInterface; 68 private final Window mWindow; 69 70 private CharSequence mTitle; 71 private CharSequence mMessage; 72 private ListView mListView; 73 private View mView; 74 75 private int mViewLayoutResId; 76 77 private int mViewSpacingLeft; 78 private int mViewSpacingTop; 79 private int mViewSpacingRight; 80 private int mViewSpacingBottom; 81 private boolean mViewSpacingSpecified = false; 82 83 private Button mButtonPositive; 84 private CharSequence mButtonPositiveText; 85 private Message mButtonPositiveMessage; 86 87 private Button mButtonNegative; 88 private CharSequence mButtonNegativeText; 89 private Message mButtonNegativeMessage; 90 91 private Button mButtonNeutral; 92 private CharSequence mButtonNeutralText; 93 private Message mButtonNeutralMessage; 94 95 private ScrollView mScrollView; 96 97 private int mIconId = 0; 98 private Drawable mIcon; 99 100 private ImageView mIconView; 101 private TextView mTitleView; 102 private TextView mMessageView; 103 private View mCustomTitleView; 104 105 private boolean mForceInverseBackground; 106 107 private ListAdapter mAdapter; 108 109 private int mCheckedItem = -1; 110 111 private int mAlertDialogLayout; 112 private int mButtonPanelSideLayout; 113 private int mListLayout; 114 private int mMultiChoiceItemLayout; 115 private int mSingleChoiceItemLayout; 116 private int mListItemLayout; 117 118 private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE; 119 120 private Handler mHandler; 121 122 private final View.OnClickListener mButtonHandler = new View.OnClickListener() { 123 @Override 124 public void onClick(View v) { 125 final Message m; 126 if (v == mButtonPositive && mButtonPositiveMessage != null) { 127 m = Message.obtain(mButtonPositiveMessage); 128 } else if (v == mButtonNegative && mButtonNegativeMessage != null) { 129 m = Message.obtain(mButtonNegativeMessage); 130 } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { 131 m = Message.obtain(mButtonNeutralMessage); 132 } else { 133 m = null; 134 } 135 136 if (m != null) { 137 m.sendToTarget(); 138 } 139 140 // Post a message so we dismiss after the above handlers are executed 141 mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface) 142 .sendToTarget(); 143 } 144 }; 145 146 private static final class ButtonHandler extends Handler { 147 // Button clicks have Message.what as the BUTTON{1,2,3} constant 148 private static final int MSG_DISMISS_DIALOG = 1; 149 150 private WeakReference<DialogInterface> mDialog; 151 ButtonHandler(DialogInterface dialog)152 public ButtonHandler(DialogInterface dialog) { 153 mDialog = new WeakReference<DialogInterface>(dialog); 154 } 155 156 @Override handleMessage(Message msg)157 public void handleMessage(Message msg) { 158 switch (msg.what) { 159 160 case DialogInterface.BUTTON_POSITIVE: 161 case DialogInterface.BUTTON_NEGATIVE: 162 case DialogInterface.BUTTON_NEUTRAL: 163 ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what); 164 break; 165 166 case MSG_DISMISS_DIALOG: 167 ((DialogInterface) msg.obj).dismiss(); 168 } 169 } 170 } 171 shouldCenterSingleButton(Context context)172 private static boolean shouldCenterSingleButton(Context context) { 173 final TypedValue outValue = new TypedValue(); 174 context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true); 175 return outValue.data != 0; 176 } 177 AlertController(Context context, DialogInterface di, Window window)178 public AlertController(Context context, DialogInterface di, Window window) { 179 mContext = context; 180 mDialogInterface = di; 181 mWindow = window; 182 mHandler = new ButtonHandler(di); 183 184 final TypedArray a = context.obtainStyledAttributes(null, 185 R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); 186 187 mAlertDialogLayout = a.getResourceId( 188 R.styleable.AlertDialog_layout, R.layout.alert_dialog); 189 mButtonPanelSideLayout = a.getResourceId( 190 R.styleable.AlertDialog_buttonPanelSideLayout, 0); 191 mListLayout = a.getResourceId( 192 R.styleable.AlertDialog_listLayout, R.layout.select_dialog); 193 194 mMultiChoiceItemLayout = a.getResourceId( 195 R.styleable.AlertDialog_multiChoiceItemLayout, 196 R.layout.select_dialog_multichoice); 197 mSingleChoiceItemLayout = a.getResourceId( 198 R.styleable.AlertDialog_singleChoiceItemLayout, 199 R.layout.select_dialog_singlechoice); 200 mListItemLayout = a.getResourceId( 201 R.styleable.AlertDialog_listItemLayout, 202 R.layout.select_dialog_item); 203 204 a.recycle(); 205 } 206 canTextInput(View v)207 static boolean canTextInput(View v) { 208 if (v.onCheckIsTextEditor()) { 209 return true; 210 } 211 212 if (!(v instanceof ViewGroup)) { 213 return false; 214 } 215 216 ViewGroup vg = (ViewGroup)v; 217 int i = vg.getChildCount(); 218 while (i > 0) { 219 i--; 220 v = vg.getChildAt(i); 221 if (canTextInput(v)) { 222 return true; 223 } 224 } 225 226 return false; 227 } 228 installContent()229 public void installContent() { 230 /* We use a custom title so never request a window title */ 231 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 232 int contentView = selectContentView(); 233 mWindow.setContentView(contentView); 234 setupView(); 235 setupDecor(); 236 } 237 selectContentView()238 private int selectContentView() { 239 if (mButtonPanelSideLayout == 0) { 240 return mAlertDialogLayout; 241 } 242 if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) { 243 return mButtonPanelSideLayout; 244 } 245 // TODO: use layout hint side for long messages/lists 246 return mAlertDialogLayout; 247 } 248 setTitle(CharSequence title)249 public void setTitle(CharSequence title) { 250 mTitle = title; 251 if (mTitleView != null) { 252 mTitleView.setText(title); 253 } 254 } 255 256 /** 257 * @see AlertDialog.Builder#setCustomTitle(View) 258 */ setCustomTitle(View customTitleView)259 public void setCustomTitle(View customTitleView) { 260 mCustomTitleView = customTitleView; 261 } 262 setMessage(CharSequence message)263 public void setMessage(CharSequence message) { 264 mMessage = message; 265 if (mMessageView != null) { 266 mMessageView.setText(message); 267 } 268 } 269 270 /** 271 * Set the view resource to display in the dialog. 272 */ setView(int layoutResId)273 public void setView(int layoutResId) { 274 mView = null; 275 mViewLayoutResId = layoutResId; 276 mViewSpacingSpecified = false; 277 } 278 279 /** 280 * Set the view to display in the dialog. 281 */ setView(View view)282 public void setView(View view) { 283 mView = view; 284 mViewLayoutResId = 0; 285 mViewSpacingSpecified = false; 286 } 287 288 /** 289 * Set the view to display in the dialog along with the spacing around that view 290 */ setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, int viewSpacingBottom)291 public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, 292 int viewSpacingBottom) { 293 mView = view; 294 mViewLayoutResId = 0; 295 mViewSpacingSpecified = true; 296 mViewSpacingLeft = viewSpacingLeft; 297 mViewSpacingTop = viewSpacingTop; 298 mViewSpacingRight = viewSpacingRight; 299 mViewSpacingBottom = viewSpacingBottom; 300 } 301 302 /** 303 * Sets a hint for the best button panel layout. 304 */ setButtonPanelLayoutHint(int layoutHint)305 public void setButtonPanelLayoutHint(int layoutHint) { 306 mButtonPanelLayoutHint = layoutHint; 307 } 308 309 /** 310 * Sets a click listener or a message to be sent when the button is clicked. 311 * You only need to pass one of {@code listener} or {@code msg}. 312 * 313 * @param whichButton Which button, can be one of 314 * {@link DialogInterface#BUTTON_POSITIVE}, 315 * {@link DialogInterface#BUTTON_NEGATIVE}, or 316 * {@link DialogInterface#BUTTON_NEUTRAL} 317 * @param text The text to display in positive button. 318 * @param listener The {@link DialogInterface.OnClickListener} to use. 319 * @param msg The {@link Message} to be sent when clicked. 320 */ setButton(int whichButton, CharSequence text, DialogInterface.OnClickListener listener, Message msg)321 public void setButton(int whichButton, CharSequence text, 322 DialogInterface.OnClickListener listener, Message msg) { 323 324 if (msg == null && listener != null) { 325 msg = mHandler.obtainMessage(whichButton, listener); 326 } 327 328 switch (whichButton) { 329 330 case DialogInterface.BUTTON_POSITIVE: 331 mButtonPositiveText = text; 332 mButtonPositiveMessage = msg; 333 break; 334 335 case DialogInterface.BUTTON_NEGATIVE: 336 mButtonNegativeText = text; 337 mButtonNegativeMessage = msg; 338 break; 339 340 case DialogInterface.BUTTON_NEUTRAL: 341 mButtonNeutralText = text; 342 mButtonNeutralMessage = msg; 343 break; 344 345 default: 346 throw new IllegalArgumentException("Button does not exist"); 347 } 348 } 349 350 /** 351 * Specifies the icon to display next to the alert title. 352 * 353 * @param resId the resource identifier of the drawable to use as the icon, 354 * or 0 for no icon 355 */ setIcon(int resId)356 public void setIcon(int resId) { 357 mIcon = null; 358 mIconId = resId; 359 360 if (mIconView != null) { 361 if (resId != 0) { 362 mIconView.setImageResource(mIconId); 363 } else { 364 mIconView.setVisibility(View.GONE); 365 } 366 } 367 } 368 369 /** 370 * Specifies the icon to display next to the alert title. 371 * 372 * @param icon the drawable to use as the icon or null for no icon 373 */ setIcon(Drawable icon)374 public void setIcon(Drawable icon) { 375 mIcon = icon; 376 mIconId = 0; 377 378 if (mIconView != null) { 379 if (icon != null) { 380 mIconView.setImageDrawable(icon); 381 } else { 382 mIconView.setVisibility(View.GONE); 383 } 384 } 385 } 386 387 /** 388 * @param attrId the attributeId of the theme-specific drawable 389 * to resolve the resourceId for. 390 * 391 * @return resId the resourceId of the theme-specific drawable 392 */ getIconAttributeResId(int attrId)393 public int getIconAttributeResId(int attrId) { 394 TypedValue out = new TypedValue(); 395 mContext.getTheme().resolveAttribute(attrId, out, true); 396 return out.resourceId; 397 } 398 setInverseBackgroundForced(boolean forceInverseBackground)399 public void setInverseBackgroundForced(boolean forceInverseBackground) { 400 mForceInverseBackground = forceInverseBackground; 401 } 402 getListView()403 public ListView getListView() { 404 return mListView; 405 } 406 getButton(int whichButton)407 public Button getButton(int whichButton) { 408 switch (whichButton) { 409 case DialogInterface.BUTTON_POSITIVE: 410 return mButtonPositive; 411 case DialogInterface.BUTTON_NEGATIVE: 412 return mButtonNegative; 413 case DialogInterface.BUTTON_NEUTRAL: 414 return mButtonNeutral; 415 default: 416 return null; 417 } 418 } 419 420 @SuppressWarnings({"UnusedDeclaration"}) onKeyDown(int keyCode, KeyEvent event)421 public boolean onKeyDown(int keyCode, KeyEvent event) { 422 return mScrollView != null && mScrollView.executeKeyEvent(event); 423 } 424 425 @SuppressWarnings({"UnusedDeclaration"}) onKeyUp(int keyCode, KeyEvent event)426 public boolean onKeyUp(int keyCode, KeyEvent event) { 427 return mScrollView != null && mScrollView.executeKeyEvent(event); 428 } 429 setupDecor()430 private void setupDecor() { 431 final View decor = mWindow.getDecorView(); 432 final View parent = mWindow.findViewById(R.id.parentPanel); 433 if (parent != null && decor != null) { 434 decor.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { 435 @Override 436 public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { 437 if (insets.isRound()) { 438 // TODO: Get the padding as a function of the window size. 439 int roundOffset = mContext.getResources().getDimensionPixelOffset( 440 R.dimen.alert_dialog_round_padding); 441 parent.setPadding(roundOffset, roundOffset, roundOffset, roundOffset); 442 } 443 return insets.consumeSystemWindowInsets(); 444 } 445 }); 446 decor.setFitsSystemWindows(true); 447 decor.requestApplyInsets(); 448 } 449 } 450 451 /** 452 * Resolves whether a custom or default panel should be used. Removes the 453 * default panel if a custom panel should be used. If the resolved panel is 454 * a view stub, inflates before returning. 455 * 456 * @param customPanel the custom panel 457 * @param defaultPanel the default panel 458 * @return the panel to use 459 */ 460 @Nullable resolvePanel(@ullable View customPanel, @Nullable View defaultPanel)461 private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) { 462 if (customPanel == null) { 463 // Inflate the default panel, if needed. 464 if (defaultPanel instanceof ViewStub) { 465 defaultPanel = ((ViewStub) defaultPanel).inflate(); 466 } 467 468 return (ViewGroup) defaultPanel; 469 } 470 471 // Remove the default panel entirely. 472 if (defaultPanel != null) { 473 final ViewParent parent = defaultPanel.getParent(); 474 if (parent instanceof ViewGroup) { 475 ((ViewGroup) parent).removeView(defaultPanel); 476 } 477 } 478 479 // Inflate the custom panel, if needed. 480 if (customPanel instanceof ViewStub) { 481 customPanel = ((ViewStub) customPanel).inflate(); 482 } 483 484 return (ViewGroup) customPanel; 485 } 486 setupView()487 private void setupView() { 488 final View parentPanel = mWindow.findViewById(R.id.parentPanel); 489 final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel); 490 final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel); 491 final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel); 492 493 // Install custom content before setting up the title or buttons so 494 // that we can handle panel overrides. 495 final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel); 496 setupCustomContent(customPanel); 497 498 final View customTopPanel = customPanel.findViewById(R.id.topPanel); 499 final View customContentPanel = customPanel.findViewById(R.id.contentPanel); 500 final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel); 501 502 // Resolve the correct panels and remove the defaults, if needed. 503 final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel); 504 final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel); 505 final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel); 506 507 setupContent(contentPanel); 508 setupButtons(buttonPanel); 509 setupTitle(topPanel); 510 511 final boolean hasCustomPanel = customPanel != null 512 && customPanel.getVisibility() != View.GONE; 513 final boolean hasTopPanel = topPanel != null 514 && topPanel.getVisibility() != View.GONE; 515 final boolean hasButtonPanel = buttonPanel != null 516 && buttonPanel.getVisibility() != View.GONE; 517 518 // Only display the text spacer if we don't have buttons. 519 if (!hasButtonPanel) { 520 if (contentPanel != null) { 521 final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons); 522 if (spacer != null) { 523 spacer.setVisibility(View.VISIBLE); 524 } 525 } 526 mWindow.setCloseOnTouchOutsideIfNotSet(true); 527 } 528 529 if (hasTopPanel) { 530 // Only clip scrolling content to padding if we have a title. 531 if (mScrollView != null) { 532 mScrollView.setClipToPadding(true); 533 } 534 535 // Only show the divider if we have a title. 536 final View divider; 537 if (mMessage != null || mListView != null || hasCustomPanel) { 538 divider = topPanel.findViewById(R.id.titleDivider); 539 } else { 540 divider = topPanel.findViewById(R.id.titleDividerTop); 541 } 542 543 if (divider != null) { 544 divider.setVisibility(View.VISIBLE); 545 } 546 } 547 548 // Update scroll indicators as needed. 549 if (!hasCustomPanel) { 550 final View content = mListView != null ? mListView : mScrollView; 551 if (content != null) { 552 final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0) 553 | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0); 554 content.setScrollIndicators(indicators, 555 View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 556 } 557 } 558 559 final TypedArray a = mContext.obtainStyledAttributes( 560 null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); 561 setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, 562 hasTopPanel, hasCustomPanel, hasButtonPanel); 563 a.recycle(); 564 } 565 setupCustomContent(ViewGroup customPanel)566 private void setupCustomContent(ViewGroup customPanel) { 567 final View customView; 568 if (mView != null) { 569 customView = mView; 570 } else if (mViewLayoutResId != 0) { 571 final LayoutInflater inflater = LayoutInflater.from(mContext); 572 customView = inflater.inflate(mViewLayoutResId, customPanel, false); 573 } else { 574 customView = null; 575 } 576 577 final boolean hasCustomView = customView != null; 578 if (!hasCustomView || !canTextInput(customView)) { 579 mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 580 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 581 } 582 583 if (hasCustomView) { 584 final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); 585 custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 586 587 if (mViewSpacingSpecified) { 588 custom.setPadding( 589 mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom); 590 } 591 592 if (mListView != null) { 593 ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0; 594 } 595 } else { 596 customPanel.setVisibility(View.GONE); 597 } 598 } 599 setupTitle(ViewGroup topPanel)600 private void setupTitle(ViewGroup topPanel) { 601 if (mCustomTitleView != null) { 602 // Add the custom title view directly to the topPanel layout 603 LayoutParams lp = new LayoutParams( 604 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 605 606 topPanel.addView(mCustomTitleView, 0, lp); 607 608 // Hide the title template 609 View titleTemplate = mWindow.findViewById(R.id.title_template); 610 titleTemplate.setVisibility(View.GONE); 611 } else { 612 mIconView = (ImageView) mWindow.findViewById(R.id.icon); 613 614 final boolean hasTextTitle = !TextUtils.isEmpty(mTitle); 615 if (hasTextTitle) { 616 // Display the title if a title is supplied, else hide it. 617 mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle); 618 mTitleView.setText(mTitle); 619 620 // Do this last so that if the user has supplied any icons we 621 // use them instead of the default ones. If the user has 622 // specified 0 then make it disappear. 623 if (mIconId != 0) { 624 mIconView.setImageResource(mIconId); 625 } else if (mIcon != null) { 626 mIconView.setImageDrawable(mIcon); 627 } else { 628 // Apply the padding from the icon to ensure the title is 629 // aligned correctly. 630 mTitleView.setPadding(mIconView.getPaddingLeft(), 631 mIconView.getPaddingTop(), 632 mIconView.getPaddingRight(), 633 mIconView.getPaddingBottom()); 634 mIconView.setVisibility(View.GONE); 635 } 636 } else { 637 // Hide the title template 638 final View titleTemplate = mWindow.findViewById(R.id.title_template); 639 titleTemplate.setVisibility(View.GONE); 640 mIconView.setVisibility(View.GONE); 641 topPanel.setVisibility(View.GONE); 642 } 643 } 644 } 645 setupContent(ViewGroup contentPanel)646 private void setupContent(ViewGroup contentPanel) { 647 mScrollView = (ScrollView) contentPanel.findViewById(R.id.scrollView); 648 mScrollView.setFocusable(false); 649 650 // Special case for users that only want to display a String 651 mMessageView = (TextView) contentPanel.findViewById(R.id.message); 652 if (mMessageView == null) { 653 return; 654 } 655 656 if (mMessage != null) { 657 mMessageView.setText(mMessage); 658 } else { 659 mMessageView.setVisibility(View.GONE); 660 mScrollView.removeView(mMessageView); 661 662 if (mListView != null) { 663 final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent(); 664 final int childIndex = scrollParent.indexOfChild(mScrollView); 665 scrollParent.removeViewAt(childIndex); 666 scrollParent.addView(mListView, childIndex, 667 new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 668 } else { 669 contentPanel.setVisibility(View.GONE); 670 } 671 } 672 } 673 manageScrollIndicators(View v, View upIndicator, View downIndicator)674 private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) { 675 if (upIndicator != null) { 676 upIndicator.setVisibility(v.canScrollVertically(-1) ? View.VISIBLE : View.INVISIBLE); 677 } 678 if (downIndicator != null) { 679 downIndicator.setVisibility(v.canScrollVertically(1) ? View.VISIBLE : View.INVISIBLE); 680 } 681 } 682 setupButtons(ViewGroup buttonPanel)683 private void setupButtons(ViewGroup buttonPanel) { 684 int BIT_BUTTON_POSITIVE = 1; 685 int BIT_BUTTON_NEGATIVE = 2; 686 int BIT_BUTTON_NEUTRAL = 4; 687 int whichButtons = 0; 688 mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1); 689 mButtonPositive.setOnClickListener(mButtonHandler); 690 691 if (TextUtils.isEmpty(mButtonPositiveText)) { 692 mButtonPositive.setVisibility(View.GONE); 693 } else { 694 mButtonPositive.setText(mButtonPositiveText); 695 mButtonPositive.setVisibility(View.VISIBLE); 696 whichButtons = whichButtons | BIT_BUTTON_POSITIVE; 697 } 698 699 mButtonNegative = (Button) buttonPanel.findViewById(R.id.button2); 700 mButtonNegative.setOnClickListener(mButtonHandler); 701 702 if (TextUtils.isEmpty(mButtonNegativeText)) { 703 mButtonNegative.setVisibility(View.GONE); 704 } else { 705 mButtonNegative.setText(mButtonNegativeText); 706 mButtonNegative.setVisibility(View.VISIBLE); 707 708 whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; 709 } 710 711 mButtonNeutral = (Button) buttonPanel.findViewById(R.id.button3); 712 mButtonNeutral.setOnClickListener(mButtonHandler); 713 714 if (TextUtils.isEmpty(mButtonNeutralText)) { 715 mButtonNeutral.setVisibility(View.GONE); 716 } else { 717 mButtonNeutral.setText(mButtonNeutralText); 718 mButtonNeutral.setVisibility(View.VISIBLE); 719 720 whichButtons = whichButtons | BIT_BUTTON_NEUTRAL; 721 } 722 723 if (shouldCenterSingleButton(mContext)) { 724 /* 725 * If we only have 1 button it should be centered on the layout and 726 * expand to fill 50% of the available space. 727 */ 728 if (whichButtons == BIT_BUTTON_POSITIVE) { 729 centerButton(mButtonPositive); 730 } else if (whichButtons == BIT_BUTTON_NEGATIVE) { 731 centerButton(mButtonNegative); 732 } else if (whichButtons == BIT_BUTTON_NEUTRAL) { 733 centerButton(mButtonNeutral); 734 } 735 } 736 737 final boolean hasButtons = whichButtons != 0; 738 if (!hasButtons) { 739 buttonPanel.setVisibility(View.GONE); 740 } 741 } 742 centerButton(Button button)743 private void centerButton(Button button) { 744 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) button.getLayoutParams(); 745 params.gravity = Gravity.CENTER_HORIZONTAL; 746 params.weight = 0.5f; 747 button.setLayoutParams(params); 748 View leftSpacer = mWindow.findViewById(R.id.leftSpacer); 749 if (leftSpacer != null) { 750 leftSpacer.setVisibility(View.VISIBLE); 751 } 752 View rightSpacer = mWindow.findViewById(R.id.rightSpacer); 753 if (rightSpacer != null) { 754 rightSpacer.setVisibility(View.VISIBLE); 755 } 756 } 757 setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel, View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons)758 private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel, 759 View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) { 760 int fullDark = 0; 761 int topDark = 0; 762 int centerDark = 0; 763 int bottomDark = 0; 764 int fullBright = 0; 765 int topBright = 0; 766 int centerBright = 0; 767 int bottomBright = 0; 768 int bottomMedium = 0; 769 770 // If the needsDefaultBackgrounds attribute is set, we know we're 771 // inheriting from a framework style. 772 final boolean needsDefaultBackgrounds = a.getBoolean( 773 R.styleable.AlertDialog_needsDefaultBackgrounds, true); 774 if (needsDefaultBackgrounds) { 775 fullDark = R.drawable.popup_full_dark; 776 topDark = R.drawable.popup_top_dark; 777 centerDark = R.drawable.popup_center_dark; 778 bottomDark = R.drawable.popup_bottom_dark; 779 fullBright = R.drawable.popup_full_bright; 780 topBright = R.drawable.popup_top_bright; 781 centerBright = R.drawable.popup_center_bright; 782 bottomBright = R.drawable.popup_bottom_bright; 783 bottomMedium = R.drawable.popup_bottom_medium; 784 } 785 786 topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright); 787 topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark); 788 centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright); 789 centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark); 790 791 /* We now set the background of all of the sections of the alert. 792 * First collect together each section that is being displayed along 793 * with whether it is on a light or dark background, then run through 794 * them setting their backgrounds. This is complicated because we need 795 * to correctly use the full, top, middle, and bottom graphics depending 796 * on how many views they are and where they appear. 797 */ 798 799 final View[] views = new View[4]; 800 final boolean[] light = new boolean[4]; 801 View lastView = null; 802 boolean lastLight = false; 803 804 int pos = 0; 805 if (hasTitle) { 806 views[pos] = topPanel; 807 light[pos] = false; 808 pos++; 809 } 810 811 /* The contentPanel displays either a custom text message or 812 * a ListView. If it's text we should use the dark background 813 * for ListView we should use the light background. If neither 814 * are there the contentPanel will be hidden so set it as null. 815 */ 816 views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel; 817 light[pos] = mListView != null; 818 pos++; 819 820 if (hasCustomView) { 821 views[pos] = customPanel; 822 light[pos] = mForceInverseBackground; 823 pos++; 824 } 825 826 if (hasButtons) { 827 views[pos] = buttonPanel; 828 light[pos] = true; 829 } 830 831 boolean setView = false; 832 for (pos = 0; pos < views.length; pos++) { 833 final View v = views[pos]; 834 if (v == null) { 835 continue; 836 } 837 838 if (lastView != null) { 839 if (!setView) { 840 lastView.setBackgroundResource(lastLight ? topBright : topDark); 841 } else { 842 lastView.setBackgroundResource(lastLight ? centerBright : centerDark); 843 } 844 setView = true; 845 } 846 847 lastView = v; 848 lastLight = light[pos]; 849 } 850 851 if (lastView != null) { 852 if (setView) { 853 bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright); 854 bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium); 855 bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark); 856 857 // ListViews will use the Bright background, but buttons use the 858 // Medium background. 859 lastView.setBackgroundResource( 860 lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark); 861 } else { 862 fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright); 863 fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark); 864 865 lastView.setBackgroundResource(lastLight ? fullBright : fullDark); 866 } 867 } 868 869 final ListView listView = mListView; 870 if (listView != null && mAdapter != null) { 871 listView.setAdapter(mAdapter); 872 final int checkedItem = mCheckedItem; 873 if (checkedItem > -1) { 874 listView.setItemChecked(checkedItem, true); 875 listView.setSelection(checkedItem); 876 } 877 } 878 } 879 880 public static class RecycleListView extends ListView { 881 boolean mRecycleOnMeasure = true; 882 RecycleListView(Context context)883 public RecycleListView(Context context) { 884 super(context); 885 } 886 RecycleListView(Context context, AttributeSet attrs)887 public RecycleListView(Context context, AttributeSet attrs) { 888 super(context, attrs); 889 } 890 RecycleListView(Context context, AttributeSet attrs, int defStyleAttr)891 public RecycleListView(Context context, AttributeSet attrs, int defStyleAttr) { 892 super(context, attrs, defStyleAttr); 893 } 894 RecycleListView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)895 public RecycleListView( 896 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 897 super(context, attrs, defStyleAttr, defStyleRes); 898 } 899 900 @Override recycleOnMeasure()901 protected boolean recycleOnMeasure() { 902 return mRecycleOnMeasure; 903 } 904 } 905 906 public static class AlertParams { 907 public final Context mContext; 908 public final LayoutInflater mInflater; 909 910 public int mIconId = 0; 911 public Drawable mIcon; 912 public int mIconAttrId = 0; 913 public CharSequence mTitle; 914 public View mCustomTitleView; 915 public CharSequence mMessage; 916 public CharSequence mPositiveButtonText; 917 public DialogInterface.OnClickListener mPositiveButtonListener; 918 public CharSequence mNegativeButtonText; 919 public DialogInterface.OnClickListener mNegativeButtonListener; 920 public CharSequence mNeutralButtonText; 921 public DialogInterface.OnClickListener mNeutralButtonListener; 922 public boolean mCancelable; 923 public DialogInterface.OnCancelListener mOnCancelListener; 924 public DialogInterface.OnDismissListener mOnDismissListener; 925 public DialogInterface.OnKeyListener mOnKeyListener; 926 public CharSequence[] mItems; 927 public ListAdapter mAdapter; 928 public DialogInterface.OnClickListener mOnClickListener; 929 public int mViewLayoutResId; 930 public View mView; 931 public int mViewSpacingLeft; 932 public int mViewSpacingTop; 933 public int mViewSpacingRight; 934 public int mViewSpacingBottom; 935 public boolean mViewSpacingSpecified = false; 936 public boolean[] mCheckedItems; 937 public boolean mIsMultiChoice; 938 public boolean mIsSingleChoice; 939 public int mCheckedItem = -1; 940 public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener; 941 public Cursor mCursor; 942 public String mLabelColumn; 943 public String mIsCheckedColumn; 944 public boolean mForceInverseBackground; 945 public AdapterView.OnItemSelectedListener mOnItemSelectedListener; 946 public OnPrepareListViewListener mOnPrepareListViewListener; 947 public boolean mRecycleOnMeasure = true; 948 949 /** 950 * Interface definition for a callback to be invoked before the ListView 951 * will be bound to an adapter. 952 */ 953 public interface OnPrepareListViewListener { 954 955 /** 956 * Called before the ListView is bound to an adapter. 957 * @param listView The ListView that will be shown in the dialog. 958 */ onPrepareListView(ListView listView)959 void onPrepareListView(ListView listView); 960 } 961 AlertParams(Context context)962 public AlertParams(Context context) { 963 mContext = context; 964 mCancelable = true; 965 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 966 } 967 apply(AlertController dialog)968 public void apply(AlertController dialog) { 969 if (mCustomTitleView != null) { 970 dialog.setCustomTitle(mCustomTitleView); 971 } else { 972 if (mTitle != null) { 973 dialog.setTitle(mTitle); 974 } 975 if (mIcon != null) { 976 dialog.setIcon(mIcon); 977 } 978 if (mIconId != 0) { 979 dialog.setIcon(mIconId); 980 } 981 if (mIconAttrId != 0) { 982 dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId)); 983 } 984 } 985 if (mMessage != null) { 986 dialog.setMessage(mMessage); 987 } 988 if (mPositiveButtonText != null) { 989 dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText, 990 mPositiveButtonListener, null); 991 } 992 if (mNegativeButtonText != null) { 993 dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText, 994 mNegativeButtonListener, null); 995 } 996 if (mNeutralButtonText != null) { 997 dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText, 998 mNeutralButtonListener, null); 999 } 1000 if (mForceInverseBackground) { 1001 dialog.setInverseBackgroundForced(true); 1002 } 1003 // For a list, the client can either supply an array of items or an 1004 // adapter or a cursor 1005 if ((mItems != null) || (mCursor != null) || (mAdapter != null)) { 1006 createListView(dialog); 1007 } 1008 if (mView != null) { 1009 if (mViewSpacingSpecified) { 1010 dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, 1011 mViewSpacingBottom); 1012 } else { 1013 dialog.setView(mView); 1014 } 1015 } else if (mViewLayoutResId != 0) { 1016 dialog.setView(mViewLayoutResId); 1017 } 1018 1019 /* 1020 dialog.setCancelable(mCancelable); 1021 dialog.setOnCancelListener(mOnCancelListener); 1022 if (mOnKeyListener != null) { 1023 dialog.setOnKeyListener(mOnKeyListener); 1024 } 1025 */ 1026 } 1027 createListView(final AlertController dialog)1028 private void createListView(final AlertController dialog) { 1029 final RecycleListView listView = 1030 (RecycleListView) mInflater.inflate(dialog.mListLayout, null); 1031 final ListAdapter adapter; 1032 1033 if (mIsMultiChoice) { 1034 if (mCursor == null) { 1035 adapter = new ArrayAdapter<CharSequence>( 1036 mContext, dialog.mMultiChoiceItemLayout, R.id.text1, mItems) { 1037 @Override 1038 public View getView(int position, View convertView, ViewGroup parent) { 1039 View view = super.getView(position, convertView, parent); 1040 if (mCheckedItems != null) { 1041 boolean isItemChecked = mCheckedItems[position]; 1042 if (isItemChecked) { 1043 listView.setItemChecked(position, true); 1044 } 1045 } 1046 return view; 1047 } 1048 }; 1049 } else { 1050 adapter = new CursorAdapter(mContext, mCursor, false) { 1051 private final int mLabelIndex; 1052 private final int mIsCheckedIndex; 1053 1054 { 1055 final Cursor cursor = getCursor(); 1056 mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn); 1057 mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn); 1058 } 1059 1060 @Override 1061 public void bindView(View view, Context context, Cursor cursor) { 1062 CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1); 1063 text.setText(cursor.getString(mLabelIndex)); 1064 listView.setItemChecked(cursor.getPosition(), 1065 cursor.getInt(mIsCheckedIndex) == 1); 1066 } 1067 1068 @Override 1069 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1070 return mInflater.inflate(dialog.mMultiChoiceItemLayout, 1071 parent, false); 1072 } 1073 1074 }; 1075 } 1076 } else { 1077 final int layout; 1078 if (mIsSingleChoice) { 1079 layout = dialog.mSingleChoiceItemLayout; 1080 } else { 1081 layout = dialog.mListItemLayout; 1082 } 1083 1084 if (mCursor != null) { 1085 adapter = new SimpleCursorAdapter(mContext, layout, mCursor, 1086 new String[] { mLabelColumn }, new int[] { R.id.text1 }); 1087 } else if (mAdapter != null) { 1088 adapter = mAdapter; 1089 } else { 1090 adapter = new CheckedItemAdapter(mContext, layout, R.id.text1, mItems); 1091 } 1092 } 1093 1094 if (mOnPrepareListViewListener != null) { 1095 mOnPrepareListViewListener.onPrepareListView(listView); 1096 } 1097 1098 /* Don't directly set the adapter on the ListView as we might 1099 * want to add a footer to the ListView later. 1100 */ 1101 dialog.mAdapter = adapter; 1102 dialog.mCheckedItem = mCheckedItem; 1103 1104 if (mOnClickListener != null) { 1105 listView.setOnItemClickListener(new OnItemClickListener() { 1106 @Override 1107 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1108 mOnClickListener.onClick(dialog.mDialogInterface, position); 1109 if (!mIsSingleChoice) { 1110 dialog.mDialogInterface.dismiss(); 1111 } 1112 } 1113 }); 1114 } else if (mOnCheckboxClickListener != null) { 1115 listView.setOnItemClickListener(new OnItemClickListener() { 1116 @Override 1117 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1118 if (mCheckedItems != null) { 1119 mCheckedItems[position] = listView.isItemChecked(position); 1120 } 1121 mOnCheckboxClickListener.onClick( 1122 dialog.mDialogInterface, position, listView.isItemChecked(position)); 1123 } 1124 }); 1125 } 1126 1127 // Attach a given OnItemSelectedListener to the ListView 1128 if (mOnItemSelectedListener != null) { 1129 listView.setOnItemSelectedListener(mOnItemSelectedListener); 1130 } 1131 1132 if (mIsSingleChoice) { 1133 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 1134 } else if (mIsMultiChoice) { 1135 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 1136 } 1137 listView.mRecycleOnMeasure = mRecycleOnMeasure; 1138 dialog.mListView = listView; 1139 } 1140 } 1141 1142 private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> { CheckedItemAdapter(Context context, int resource, int textViewResourceId, CharSequence[] objects)1143 public CheckedItemAdapter(Context context, int resource, int textViewResourceId, 1144 CharSequence[] objects) { 1145 super(context, resource, textViewResourceId, objects); 1146 } 1147 1148 @Override hasStableIds()1149 public boolean hasStableIds() { 1150 return true; 1151 } 1152 1153 @Override getItemId(int position)1154 public long getItemId(int position) { 1155 return position; 1156 } 1157 } 1158 } 1159