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