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