1 /* 2 * Copyright (C) 2007 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 android.widget; 18 19 import static android.view.accessibility.Flags.triStateChecked; 20 21 import android.annotation.DrawableRes; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.compat.annotation.UnsupportedAppUsage; 25 import android.content.Context; 26 import android.content.res.ColorStateList; 27 import android.content.res.TypedArray; 28 import android.graphics.BlendMode; 29 import android.graphics.Canvas; 30 import android.graphics.PorterDuff; 31 import android.graphics.drawable.Drawable; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.util.AttributeSet; 35 import android.view.Gravity; 36 import android.view.RemotableViewMethod; 37 import android.view.ViewDebug; 38 import android.view.ViewHierarchyEncoder; 39 import android.view.accessibility.AccessibilityEvent; 40 import android.view.accessibility.AccessibilityNodeInfo; 41 import android.view.inspector.InspectableProperty; 42 43 import com.android.internal.R; 44 45 /** 46 * An extension to {@link TextView} that supports the {@link Checkable} 47 * interface and displays. 48 * <p> 49 * This is useful when used in a {@link android.widget.ListView ListView} where 50 * the {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has 51 * been set to something other than 52 * {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}. 53 * 54 * @attr ref android.R.styleable#CheckedTextView_checked 55 * @attr ref android.R.styleable#CheckedTextView_checkMark 56 */ 57 public class CheckedTextView extends TextView implements Checkable { 58 private boolean mChecked; 59 60 private int mCheckMarkResource; 61 @UnsupportedAppUsage 62 private Drawable mCheckMarkDrawable; 63 private ColorStateList mCheckMarkTintList = null; 64 private BlendMode mCheckMarkBlendMode = null; 65 private boolean mHasCheckMarkTint = false; 66 private boolean mHasCheckMarkTintMode = false; 67 68 private int mBasePadding; 69 private int mCheckMarkWidth; 70 @UnsupportedAppUsage 71 private int mCheckMarkGravity = Gravity.END; 72 73 private boolean mNeedRequestlayout; 74 75 private static final int[] CHECKED_STATE_SET = { 76 R.attr.state_checked 77 }; 78 CheckedTextView(Context context)79 public CheckedTextView(Context context) { 80 this(context, null); 81 } 82 CheckedTextView(Context context, AttributeSet attrs)83 public CheckedTextView(Context context, AttributeSet attrs) { 84 this(context, attrs, R.attr.checkedTextViewStyle); 85 } 86 CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr)87 public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) { 88 this(context, attrs, defStyleAttr, 0); 89 } 90 CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)91 public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 92 super(context, attrs, defStyleAttr, defStyleRes); 93 94 final TypedArray a = context.obtainStyledAttributes( 95 attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes); 96 saveAttributeDataForStyleable(context, R.styleable.CheckedTextView, 97 attrs, a, defStyleAttr, defStyleRes); 98 99 final Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark); 100 if (d != null) { 101 setCheckMarkDrawable(d); 102 } 103 104 if (a.hasValue(R.styleable.CheckedTextView_checkMarkTintMode)) { 105 mCheckMarkBlendMode = Drawable.parseBlendMode(a.getInt( 106 R.styleable.CheckedTextView_checkMarkTintMode, -1), 107 mCheckMarkBlendMode); 108 mHasCheckMarkTintMode = true; 109 } 110 111 if (a.hasValue(R.styleable.CheckedTextView_checkMarkTint)) { 112 mCheckMarkTintList = a.getColorStateList(R.styleable.CheckedTextView_checkMarkTint); 113 mHasCheckMarkTint = true; 114 } 115 116 mCheckMarkGravity = a.getInt(R.styleable.CheckedTextView_checkMarkGravity, Gravity.END); 117 118 final boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false); 119 setChecked(checked); 120 121 a.recycle(); 122 123 applyCheckMarkTint(); 124 } 125 toggle()126 public void toggle() { 127 setChecked(!mChecked); 128 } 129 130 @ViewDebug.ExportedProperty 131 @InspectableProperty isChecked()132 public boolean isChecked() { 133 return mChecked; 134 } 135 136 /** 137 * Sets the checked state of this view. 138 * 139 * @param checked {@code true} set the state to checked, {@code false} to 140 * uncheck 141 */ setChecked(boolean checked)142 public void setChecked(boolean checked) { 143 if (mChecked != checked) { 144 mChecked = checked; 145 refreshDrawableState(); 146 if (triStateChecked()) { 147 notifyViewAccessibilityStateChangedIfNeeded( 148 AccessibilityEvent.CONTENT_CHANGE_TYPE_CHECKED); 149 } 150 notifyViewAccessibilityStateChangedIfNeeded( 151 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); 152 } 153 } 154 155 /** 156 * Sets the check mark to the drawable with the specified resource ID. 157 * <p> 158 * When this view is checked, the drawable's state set will include 159 * {@link android.R.attr#state_checked}. 160 * 161 * @param resId the resource identifier of drawable to use as the check 162 * mark 163 * @attr ref android.R.styleable#CheckedTextView_checkMark 164 * @see #setCheckMarkDrawable(Drawable) 165 * @see #getCheckMarkDrawable() 166 */ setCheckMarkDrawable(@rawableRes int resId)167 public void setCheckMarkDrawable(@DrawableRes int resId) { 168 if (resId != 0 && resId == mCheckMarkResource) { 169 return; 170 } 171 172 final Drawable d = resId != 0 ? getContext().getDrawable(resId) : null; 173 setCheckMarkDrawableInternal(d, resId); 174 } 175 176 /** 177 * Set the check mark to the specified drawable. 178 * <p> 179 * When this view is checked, the drawable's state set will include 180 * {@link android.R.attr#state_checked}. 181 * 182 * @param d the drawable to use for the check mark 183 * @attr ref android.R.styleable#CheckedTextView_checkMark 184 * @see #setCheckMarkDrawable(int) 185 * @see #getCheckMarkDrawable() 186 */ setCheckMarkDrawable(@ullable Drawable d)187 public void setCheckMarkDrawable(@Nullable Drawable d) { 188 setCheckMarkDrawableInternal(d, 0); 189 } 190 setCheckMarkDrawableInternal(@ullable Drawable d, @DrawableRes int resId)191 private void setCheckMarkDrawableInternal(@Nullable Drawable d, @DrawableRes int resId) { 192 if (mCheckMarkDrawable != null) { 193 mCheckMarkDrawable.setCallback(null); 194 unscheduleDrawable(mCheckMarkDrawable); 195 } 196 197 mNeedRequestlayout = (d != mCheckMarkDrawable); 198 199 if (d != null) { 200 d.setCallback(this); 201 d.setVisible(getVisibility() == VISIBLE, false); 202 d.setState(CHECKED_STATE_SET); 203 204 // Record the intrinsic dimensions when in "checked" state. 205 setMinHeight(d.getIntrinsicHeight()); 206 mCheckMarkWidth = d.getIntrinsicWidth(); 207 208 d.setState(getDrawableState()); 209 } else { 210 mCheckMarkWidth = 0; 211 } 212 213 mCheckMarkDrawable = d; 214 mCheckMarkResource = resId; 215 216 applyCheckMarkTint(); 217 218 // Do padding resolution. This will call internalSetPadding() and do a 219 // requestLayout() if needed. 220 resolvePadding(); 221 } 222 223 /** 224 * Applies a tint to the check mark drawable. Does not modify the 225 * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 226 * <p> 227 * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will 228 * automatically mutate the drawable and apply the specified tint and 229 * tint mode using 230 * {@link Drawable#setTintList(ColorStateList)}. 231 * 232 * @param tint the tint to apply, may be {@code null} to clear tint 233 * 234 * @attr ref android.R.styleable#CheckedTextView_checkMarkTint 235 * @see #getCheckMarkTintList() 236 * @see Drawable#setTintList(ColorStateList) 237 */ setCheckMarkTintList(@ullable ColorStateList tint)238 public void setCheckMarkTintList(@Nullable ColorStateList tint) { 239 mCheckMarkTintList = tint; 240 mHasCheckMarkTint = true; 241 242 applyCheckMarkTint(); 243 } 244 245 /** 246 * Returns the tint applied to the check mark drawable, if specified. 247 * 248 * @return the tint applied to the check mark drawable 249 * @attr ref android.R.styleable#CheckedTextView_checkMarkTint 250 * @see #setCheckMarkTintList(ColorStateList) 251 */ 252 @InspectableProperty(name = "checkMarkTint") 253 @Nullable getCheckMarkTintList()254 public ColorStateList getCheckMarkTintList() { 255 return mCheckMarkTintList; 256 } 257 258 /** 259 * Specifies the blending mode used to apply the tint specified by 260 * {@link #setCheckMarkTintList(ColorStateList)} to the check mark 261 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 262 * 263 * @param tintMode the blending mode used to apply the tint, may be 264 * {@code null} to clear tint 265 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 266 * @see #setCheckMarkTintList(ColorStateList) 267 * @see Drawable#setTintMode(PorterDuff.Mode) 268 */ setCheckMarkTintMode(@ullable PorterDuff.Mode tintMode)269 public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) { 270 setCheckMarkTintBlendMode(tintMode != null 271 ? BlendMode.fromValue(tintMode.nativeInt) : null); 272 } 273 274 /** 275 * Specifies the blending mode used to apply the tint specified by 276 * {@link #setCheckMarkTintList(ColorStateList)} to the check mark 277 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 278 * 279 * @param tintMode the blending mode used to apply the tint, may be 280 * {@code null} to clear tint 281 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 282 * @see #setCheckMarkTintList(ColorStateList) 283 * @see Drawable#setTintBlendMode(BlendMode) 284 */ setCheckMarkTintBlendMode(@ullable BlendMode tintMode)285 public void setCheckMarkTintBlendMode(@Nullable BlendMode tintMode) { 286 mCheckMarkBlendMode = tintMode; 287 mHasCheckMarkTintMode = true; 288 289 applyCheckMarkTint(); 290 } 291 292 /** 293 * Returns the blending mode used to apply the tint to the check mark 294 * drawable, if specified. 295 * 296 * @return the blending mode used to apply the tint to the check mark 297 * drawable 298 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 299 * @see #setCheckMarkTintMode(PorterDuff.Mode) 300 */ 301 @InspectableProperty 302 @Nullable getCheckMarkTintMode()303 public PorterDuff.Mode getCheckMarkTintMode() { 304 return mCheckMarkBlendMode != null 305 ? BlendMode.blendModeToPorterDuffMode(mCheckMarkBlendMode) : null; 306 } 307 308 /** 309 * Returns the blending mode used to apply the tint to the check mark 310 * drawable, if specified. 311 * 312 * @return the blending mode used to apply the tint to the check mark 313 * drawable 314 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 315 * @see #setCheckMarkTintMode(PorterDuff.Mode) 316 */ 317 @InspectableProperty(attributeId = android.R.styleable.CheckedTextView_checkMarkTintMode) 318 @Nullable getCheckMarkTintBlendMode()319 public BlendMode getCheckMarkTintBlendMode() { 320 return mCheckMarkBlendMode; 321 } 322 applyCheckMarkTint()323 private void applyCheckMarkTint() { 324 if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) { 325 mCheckMarkDrawable = mCheckMarkDrawable.mutate(); 326 327 if (mHasCheckMarkTint) { 328 mCheckMarkDrawable.setTintList(mCheckMarkTintList); 329 } 330 331 if (mHasCheckMarkTintMode) { 332 mCheckMarkDrawable.setTintBlendMode(mCheckMarkBlendMode); 333 } 334 335 // The drawable (or one of its children) may not have been 336 // stateful before applying the tint, so let's try again. 337 if (mCheckMarkDrawable.isStateful()) { 338 mCheckMarkDrawable.setState(getDrawableState()); 339 } 340 } 341 } 342 343 @RemotableViewMethod 344 @Override setVisibility(int visibility)345 public void setVisibility(int visibility) { 346 super.setVisibility(visibility); 347 348 if (mCheckMarkDrawable != null) { 349 mCheckMarkDrawable.setVisible(visibility == VISIBLE, false); 350 } 351 } 352 353 @Override jumpDrawablesToCurrentState()354 public void jumpDrawablesToCurrentState() { 355 super.jumpDrawablesToCurrentState(); 356 357 if (mCheckMarkDrawable != null) { 358 mCheckMarkDrawable.jumpToCurrentState(); 359 } 360 } 361 362 @Override verifyDrawable(@onNull Drawable who)363 protected boolean verifyDrawable(@NonNull Drawable who) { 364 return who == mCheckMarkDrawable || super.verifyDrawable(who); 365 } 366 367 /** 368 * Gets the checkmark drawable 369 * 370 * @return The drawable use to represent the checkmark, if any. 371 * 372 * @see #setCheckMarkDrawable(Drawable) 373 * @see #setCheckMarkDrawable(int) 374 * 375 * @attr ref android.R.styleable#CheckedTextView_checkMark 376 */ 377 @InspectableProperty(name = "checkMark") getCheckMarkDrawable()378 public Drawable getCheckMarkDrawable() { 379 return mCheckMarkDrawable; 380 } 381 382 /** 383 * @hide 384 */ 385 @Override internalSetPadding(int left, int top, int right, int bottom)386 protected void internalSetPadding(int left, int top, int right, int bottom) { 387 super.internalSetPadding(left, top, right, bottom); 388 setBasePadding(isCheckMarkAtStart()); 389 } 390 391 @Override onRtlPropertiesChanged(int layoutDirection)392 public void onRtlPropertiesChanged(int layoutDirection) { 393 super.onRtlPropertiesChanged(layoutDirection); 394 updatePadding(); 395 } 396 updatePadding()397 private void updatePadding() { 398 resetPaddingToInitialValues(); 399 int newPadding = (mCheckMarkDrawable != null) ? 400 mCheckMarkWidth + mBasePadding : mBasePadding; 401 if (isCheckMarkAtStart()) { 402 mNeedRequestlayout |= (mPaddingLeft != newPadding); 403 mPaddingLeft = newPadding; 404 } else { 405 mNeedRequestlayout |= (mPaddingRight != newPadding); 406 mPaddingRight = newPadding; 407 } 408 if (mNeedRequestlayout) { 409 requestLayout(); 410 mNeedRequestlayout = false; 411 } 412 } 413 setBasePadding(boolean checkmarkAtStart)414 private void setBasePadding(boolean checkmarkAtStart) { 415 if (checkmarkAtStart) { 416 mBasePadding = mPaddingLeft; 417 } else { 418 mBasePadding = mPaddingRight; 419 } 420 } 421 isCheckMarkAtStart()422 private boolean isCheckMarkAtStart() { 423 final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection()); 424 final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 425 return hgrav == Gravity.LEFT; 426 } 427 428 @Override onDraw(Canvas canvas)429 protected void onDraw(Canvas canvas) { 430 super.onDraw(canvas); 431 432 final Drawable checkMarkDrawable = mCheckMarkDrawable; 433 if (checkMarkDrawable != null) { 434 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 435 final int height = checkMarkDrawable.getIntrinsicHeight(); 436 437 int y = 0; 438 439 switch (verticalGravity) { 440 case Gravity.BOTTOM: 441 y = getHeight() - height; 442 break; 443 case Gravity.CENTER_VERTICAL: 444 y = (getHeight() - height) / 2; 445 break; 446 } 447 448 final boolean checkMarkAtStart = isCheckMarkAtStart(); 449 final int width = getWidth(); 450 final int top = y; 451 final int bottom = top + height; 452 final int left; 453 final int right; 454 if (checkMarkAtStart) { 455 left = mBasePadding; 456 right = left + mCheckMarkWidth; 457 } else { 458 right = width - mBasePadding; 459 left = right - mCheckMarkWidth; 460 } 461 checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom); 462 checkMarkDrawable.draw(canvas); 463 464 final Drawable background = getBackground(); 465 if (background != null) { 466 background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom); 467 } 468 } 469 } 470 471 @Override onCreateDrawableState(int extraSpace)472 protected int[] onCreateDrawableState(int extraSpace) { 473 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 474 if (isChecked()) { 475 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 476 } 477 return drawableState; 478 } 479 480 @Override drawableStateChanged()481 protected void drawableStateChanged() { 482 super.drawableStateChanged(); 483 484 final Drawable checkMarkDrawable = mCheckMarkDrawable; 485 if (checkMarkDrawable != null && checkMarkDrawable.isStateful() 486 && checkMarkDrawable.setState(getDrawableState())) { 487 invalidateDrawable(checkMarkDrawable); 488 } 489 } 490 491 @Override drawableHotspotChanged(float x, float y)492 public void drawableHotspotChanged(float x, float y) { 493 super.drawableHotspotChanged(x, y); 494 495 if (mCheckMarkDrawable != null) { 496 mCheckMarkDrawable.setHotspot(x, y); 497 } 498 } 499 500 @Override getAccessibilityClassName()501 public CharSequence getAccessibilityClassName() { 502 return CheckedTextView.class.getName(); 503 } 504 505 static class SavedState extends BaseSavedState { 506 boolean checked; 507 508 /** 509 * Constructor called from {@link CheckedTextView#onSaveInstanceState()} 510 */ SavedState(Parcelable superState)511 SavedState(Parcelable superState) { 512 super(superState); 513 } 514 515 /** 516 * Constructor called from {@link #CREATOR} 517 */ SavedState(Parcel in)518 private SavedState(Parcel in) { 519 super(in); 520 checked = (Boolean)in.readValue(null); 521 } 522 523 @Override writeToParcel(Parcel out, int flags)524 public void writeToParcel(Parcel out, int flags) { 525 super.writeToParcel(out, flags); 526 out.writeValue(checked); 527 } 528 529 @Override toString()530 public String toString() { 531 return "CheckedTextView.SavedState{" 532 + Integer.toHexString(System.identityHashCode(this)) 533 + " checked=" + checked + "}"; 534 } 535 536 public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR 537 = new Parcelable.Creator<SavedState>() { 538 public SavedState createFromParcel(Parcel in) { 539 return new SavedState(in); 540 } 541 542 public SavedState[] newArray(int size) { 543 return new SavedState[size]; 544 } 545 }; 546 } 547 548 @Override onSaveInstanceState()549 public Parcelable onSaveInstanceState() { 550 Parcelable superState = super.onSaveInstanceState(); 551 552 SavedState ss = new SavedState(superState); 553 554 ss.checked = isChecked(); 555 return ss; 556 } 557 558 @Override onRestoreInstanceState(Parcelable state)559 public void onRestoreInstanceState(Parcelable state) { 560 SavedState ss = (SavedState) state; 561 562 super.onRestoreInstanceState(ss.getSuperState()); 563 setChecked(ss.checked); 564 requestLayout(); 565 } 566 567 /** @hide */ 568 @Override onInitializeAccessibilityEventInternal(AccessibilityEvent event)569 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { 570 super.onInitializeAccessibilityEventInternal(event); 571 event.setChecked(mChecked); 572 } 573 574 /** @hide */ 575 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)576 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 577 super.onInitializeAccessibilityNodeInfoInternal(info); 578 info.setCheckable(true); 579 if (triStateChecked()) { 580 info.setChecked(mChecked ? AccessibilityNodeInfo.CHECKED_STATE_TRUE : 581 AccessibilityNodeInfo.CHECKED_STATE_FALSE); 582 } else { 583 info.setChecked(mChecked); 584 } 585 } 586 587 /** @hide */ 588 @Override encodeProperties(@onNull ViewHierarchyEncoder stream)589 protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) { 590 super.encodeProperties(stream); 591 stream.addProperty("text:checked", isChecked()); 592 } 593 } 594