CheckedTextView.java revision 94a6d15ede149189bba9e5f474ed853c98230e75
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 mCheckMarkResource = resId; 156 157 Drawable d = null; 158 if (mCheckMarkResource != 0) { 159 d = getContext().getDrawable(mCheckMarkResource); 160 } 161 setCheckMarkDrawable(d); 162 } 163 164 /** 165 * Set the check mark to the specified drawable. 166 * <p> 167 * When this view is checked, the drawable's state set will include 168 * {@link android.R.attr#state_checked}. 169 * 170 * @param d the drawable to use for the check mark 171 * @attr ref android.R.styleable#CheckedTextView_checkMark 172 * @see #setCheckMarkDrawable(int) 173 * @see #getCheckMarkDrawable() 174 */ 175 public void setCheckMarkDrawable(Drawable d) { 176 if (mCheckMarkDrawable != null) { 177 mCheckMarkDrawable.setCallback(null); 178 unscheduleDrawable(mCheckMarkDrawable); 179 } 180 mNeedRequestlayout = (d != mCheckMarkDrawable); 181 if (d != null) { 182 d.setCallback(this); 183 d.setVisible(getVisibility() == VISIBLE, false); 184 d.setState(CHECKED_STATE_SET); 185 setMinHeight(d.getIntrinsicHeight()); 186 187 mCheckMarkWidth = d.getIntrinsicWidth(); 188 d.setState(getDrawableState()); 189 applyCheckMarkTint(); 190 } else { 191 mCheckMarkWidth = 0; 192 } 193 mCheckMarkDrawable = d; 194 195 // Do padding resolution. This will call internalSetPadding() and do a 196 // requestLayout() if needed. 197 resolvePadding(); 198 } 199 200 /** 201 * Applies a tint to the check mark drawable. Does not modify the 202 * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 203 * <p> 204 * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will 205 * automatically mutate the drawable and apply the specified tint and 206 * tint mode using 207 * {@link Drawable#setTintList(ColorStateList)}. 208 * 209 * @param tint the tint to apply, may be {@code null} to clear tint 210 * 211 * @attr ref android.R.styleable#CheckedTextView_checkMarkTint 212 * @see #getCheckMarkTintList() 213 * @see Drawable#setTintList(ColorStateList) 214 */ 215 public void setCheckMarkTintList(@Nullable ColorStateList tint) { 216 mCheckMarkTintList = tint; 217 mHasCheckMarkTint = true; 218 219 applyCheckMarkTint(); 220 } 221 222 /** 223 * Returns the tint applied to the check mark drawable, if specified. 224 * 225 * @return the tint applied to the check mark drawable 226 * @attr ref android.R.styleable#CheckedTextView_checkMarkTint 227 * @see #setCheckMarkTintList(ColorStateList) 228 */ 229 @Nullable 230 public ColorStateList getCheckMarkTintList() { 231 return mCheckMarkTintList; 232 } 233 234 /** 235 * Specifies the blending mode used to apply the tint specified by 236 * {@link #setCheckMarkTintList(ColorStateList)} to the check mark 237 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 238 * 239 * @param tintMode the blending mode used to apply the tint, may be 240 * {@code null} to clear tint 241 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 242 * @see #setCheckMarkTintList(ColorStateList) 243 * @see Drawable#setTintMode(PorterDuff.Mode) 244 */ 245 public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) { 246 mCheckMarkTintMode = tintMode; 247 mHasCheckMarkTintMode = true; 248 249 applyCheckMarkTint(); 250 } 251 252 /** 253 * Returns the blending mode used to apply the tint to the check mark 254 * drawable, if specified. 255 * 256 * @return the blending mode used to apply the tint to the check mark 257 * drawable 258 * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode 259 * @see #setCheckMarkTintMode(PorterDuff.Mode) 260 */ 261 @Nullable 262 public PorterDuff.Mode getCheckMarkTintMode() { 263 return mCheckMarkTintMode; 264 } 265 266 private void applyCheckMarkTint() { 267 if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) { 268 mCheckMarkDrawable = mCheckMarkDrawable.mutate(); 269 270 if (mHasCheckMarkTint) { 271 mCheckMarkDrawable.setTintList(mCheckMarkTintList); 272 } 273 274 if (mHasCheckMarkTintMode) { 275 mCheckMarkDrawable.setTintMode(mCheckMarkTintMode); 276 } 277 278 // The drawable (or one of its children) may not have been 279 // stateful before applying the tint, so let's try again. 280 if (mCheckMarkDrawable.isStateful()) { 281 mCheckMarkDrawable.setState(getDrawableState()); 282 } 283 } 284 } 285 286 @RemotableViewMethod 287 @Override 288 public void setVisibility(int visibility) { 289 super.setVisibility(visibility); 290 291 if (mCheckMarkDrawable != null) { 292 mCheckMarkDrawable.setVisible(visibility == VISIBLE, false); 293 } 294 } 295 296 @Override 297 public void jumpDrawablesToCurrentState() { 298 super.jumpDrawablesToCurrentState(); 299 300 if (mCheckMarkDrawable != null) { 301 mCheckMarkDrawable.jumpToCurrentState(); 302 } 303 } 304 305 @Override 306 protected boolean verifyDrawable(Drawable who) { 307 return who == mCheckMarkDrawable || super.verifyDrawable(who); 308 } 309 310 /** 311 * Gets the checkmark drawable 312 * 313 * @return The drawable use to represent the checkmark, if any. 314 * 315 * @see #setCheckMarkDrawable(Drawable) 316 * @see #setCheckMarkDrawable(int) 317 * 318 * @attr ref android.R.styleable#CheckedTextView_checkMark 319 */ 320 public Drawable getCheckMarkDrawable() { 321 return mCheckMarkDrawable; 322 } 323 324 /** 325 * @hide 326 */ 327 @Override 328 protected void internalSetPadding(int left, int top, int right, int bottom) { 329 super.internalSetPadding(left, top, right, bottom); 330 setBasePadding(isCheckMarkAtStart()); 331 } 332 333 @Override 334 public void onRtlPropertiesChanged(int layoutDirection) { 335 super.onRtlPropertiesChanged(layoutDirection); 336 updatePadding(); 337 } 338 339 private void updatePadding() { 340 resetPaddingToInitialValues(); 341 int newPadding = (mCheckMarkDrawable != null) ? 342 mCheckMarkWidth + mBasePadding : mBasePadding; 343 if (isCheckMarkAtStart()) { 344 mNeedRequestlayout |= (mPaddingLeft != newPadding); 345 mPaddingLeft = newPadding; 346 } else { 347 mNeedRequestlayout |= (mPaddingRight != newPadding); 348 mPaddingRight = newPadding; 349 } 350 if (mNeedRequestlayout) { 351 requestLayout(); 352 mNeedRequestlayout = false; 353 } 354 } 355 356 private void setBasePadding(boolean checkmarkAtStart) { 357 if (checkmarkAtStart) { 358 mBasePadding = mPaddingLeft; 359 } else { 360 mBasePadding = mPaddingRight; 361 } 362 } 363 364 private boolean isCheckMarkAtStart() { 365 final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection()); 366 final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 367 return hgrav == Gravity.LEFT; 368 } 369 370 @Override 371 protected void onDraw(Canvas canvas) { 372 super.onDraw(canvas); 373 374 final Drawable checkMarkDrawable = mCheckMarkDrawable; 375 if (checkMarkDrawable != null) { 376 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 377 final int height = checkMarkDrawable.getIntrinsicHeight(); 378 379 int y = 0; 380 381 switch (verticalGravity) { 382 case Gravity.BOTTOM: 383 y = getHeight() - height; 384 break; 385 case Gravity.CENTER_VERTICAL: 386 y = (getHeight() - height) / 2; 387 break; 388 } 389 390 final boolean checkMarkAtStart = isCheckMarkAtStart(); 391 final int width = getWidth(); 392 final int top = y; 393 final int bottom = top + height; 394 final int left; 395 final int right; 396 if (checkMarkAtStart) { 397 left = mBasePadding; 398 right = left + mCheckMarkWidth; 399 } else { 400 right = width - mBasePadding; 401 left = right - mCheckMarkWidth; 402 } 403 checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom); 404 checkMarkDrawable.draw(canvas); 405 406 final Drawable background = getBackground(); 407 if (background != null) { 408 background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom); 409 } 410 } 411 } 412 413 @Override 414 protected int[] onCreateDrawableState(int extraSpace) { 415 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 416 if (isChecked()) { 417 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 418 } 419 return drawableState; 420 } 421 422 @Override 423 protected void drawableStateChanged() { 424 super.drawableStateChanged(); 425 426 if (mCheckMarkDrawable != null) { 427 int[] myDrawableState = getDrawableState(); 428 429 // Set the state of the Drawable 430 mCheckMarkDrawable.setState(myDrawableState); 431 432 invalidate(); 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