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