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