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