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 com.android.internal.R; 20 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.drawable.Drawable; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.util.AttributeSet; 28 import android.view.Gravity; 29 import android.view.ViewDebug; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 33 /** 34 * <p> 35 * A button with two states, checked and unchecked. When the button is pressed 36 * or clicked, the state changes automatically. 37 * </p> 38 * 39 * <p><strong>XML attributes</strong></p> 40 * <p> 41 * See {@link android.R.styleable#CompoundButton 42 * CompoundButton Attributes}, {@link android.R.styleable#Button Button 43 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link 44 * android.R.styleable#View View Attributes} 45 * </p> 46 */ 47 public abstract class CompoundButton extends Button implements Checkable { 48 private boolean mChecked; 49 private int mButtonResource; 50 private boolean mBroadcasting; 51 private Drawable mButtonDrawable; 52 private OnCheckedChangeListener mOnCheckedChangeListener; 53 private OnCheckedChangeListener mOnCheckedChangeWidgetListener; 54 55 private static final int[] CHECKED_STATE_SET = { 56 R.attr.state_checked 57 }; 58 CompoundButton(Context context)59 public CompoundButton(Context context) { 60 this(context, null); 61 } 62 CompoundButton(Context context, AttributeSet attrs)63 public CompoundButton(Context context, AttributeSet attrs) { 64 this(context, attrs, 0); 65 } 66 CompoundButton(Context context, AttributeSet attrs, int defStyle)67 public CompoundButton(Context context, AttributeSet attrs, int defStyle) { 68 super(context, attrs, defStyle); 69 70 TypedArray a = 71 context.obtainStyledAttributes( 72 attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0); 73 74 Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); 75 if (d != null) { 76 setButtonDrawable(d); 77 } 78 79 boolean checked = a 80 .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false); 81 setChecked(checked); 82 83 a.recycle(); 84 } 85 toggle()86 public void toggle() { 87 setChecked(!mChecked); 88 } 89 90 @Override performClick()91 public boolean performClick() { 92 /* 93 * XXX: These are tiny, need some surrounding 'expanded touch area', 94 * which will need to be implemented in Button if we only override 95 * performClick() 96 */ 97 98 /* When clicked, toggle the state */ 99 toggle(); 100 return super.performClick(); 101 } 102 103 @ViewDebug.ExportedProperty isChecked()104 public boolean isChecked() { 105 return mChecked; 106 } 107 108 /** 109 * <p>Changes the checked state of this button.</p> 110 * 111 * @param checked true to check the button, false to uncheck it 112 */ setChecked(boolean checked)113 public void setChecked(boolean checked) { 114 if (mChecked != checked) { 115 mChecked = checked; 116 refreshDrawableState(); 117 notifyAccessibilityStateChanged(); 118 119 // Avoid infinite recursions if setChecked() is called from a listener 120 if (mBroadcasting) { 121 return; 122 } 123 124 mBroadcasting = true; 125 if (mOnCheckedChangeListener != null) { 126 mOnCheckedChangeListener.onCheckedChanged(this, mChecked); 127 } 128 if (mOnCheckedChangeWidgetListener != null) { 129 mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); 130 } 131 132 mBroadcasting = false; 133 } 134 } 135 136 /** 137 * Register a callback to be invoked when the checked state of this button 138 * changes. 139 * 140 * @param listener the callback to call on checked state change 141 */ setOnCheckedChangeListener(OnCheckedChangeListener listener)142 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { 143 mOnCheckedChangeListener = listener; 144 } 145 146 /** 147 * Register a callback to be invoked when the checked state of this button 148 * changes. This callback is used for internal purpose only. 149 * 150 * @param listener the callback to call on checked state change 151 * @hide 152 */ setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener)153 void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { 154 mOnCheckedChangeWidgetListener = listener; 155 } 156 157 /** 158 * Interface definition for a callback to be invoked when the checked state 159 * of a compound button changed. 160 */ 161 public static interface OnCheckedChangeListener { 162 /** 163 * Called when the checked state of a compound button has changed. 164 * 165 * @param buttonView The compound button view whose state has changed. 166 * @param isChecked The new checked state of buttonView. 167 */ onCheckedChanged(CompoundButton buttonView, boolean isChecked)168 void onCheckedChanged(CompoundButton buttonView, boolean isChecked); 169 } 170 171 /** 172 * Set the background to a given Drawable, identified by its resource id. 173 * 174 * @param resid the resource id of the drawable to use as the background 175 */ setButtonDrawable(int resid)176 public void setButtonDrawable(int resid) { 177 if (resid != 0 && resid == mButtonResource) { 178 return; 179 } 180 181 mButtonResource = resid; 182 183 Drawable d = null; 184 if (mButtonResource != 0) { 185 d = getResources().getDrawable(mButtonResource); 186 } 187 setButtonDrawable(d); 188 } 189 190 /** 191 * Set the background to a given Drawable 192 * 193 * @param d The Drawable to use as the background 194 */ setButtonDrawable(Drawable d)195 public void setButtonDrawable(Drawable d) { 196 if (d != null) { 197 if (mButtonDrawable != null) { 198 mButtonDrawable.setCallback(null); 199 unscheduleDrawable(mButtonDrawable); 200 } 201 d.setCallback(this); 202 d.setState(getDrawableState()); 203 d.setVisible(getVisibility() == VISIBLE, false); 204 mButtonDrawable = d; 205 mButtonDrawable.setState(null); 206 setMinHeight(mButtonDrawable.getIntrinsicHeight()); 207 } 208 209 refreshDrawableState(); 210 } 211 212 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)213 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 214 super.onInitializeAccessibilityEvent(event); 215 event.setClassName(CompoundButton.class.getName()); 216 event.setChecked(mChecked); 217 } 218 219 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)220 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 221 super.onInitializeAccessibilityNodeInfo(info); 222 info.setClassName(CompoundButton.class.getName()); 223 info.setCheckable(true); 224 info.setChecked(mChecked); 225 } 226 227 @Override getCompoundPaddingLeft()228 public int getCompoundPaddingLeft() { 229 int padding = super.getCompoundPaddingLeft(); 230 if (!isLayoutRtl()) { 231 final Drawable buttonDrawable = mButtonDrawable; 232 if (buttonDrawable != null) { 233 padding += buttonDrawable.getIntrinsicWidth(); 234 } 235 } 236 return padding; 237 } 238 239 @Override getCompoundPaddingRight()240 public int getCompoundPaddingRight() { 241 int padding = super.getCompoundPaddingRight(); 242 if (isLayoutRtl()) { 243 final Drawable buttonDrawable = mButtonDrawable; 244 if (buttonDrawable != null) { 245 padding += buttonDrawable.getIntrinsicWidth(); 246 } 247 } 248 return padding; 249 } 250 251 /** 252 * @hide 253 */ 254 @Override getHorizontalOffsetForDrawables()255 public int getHorizontalOffsetForDrawables() { 256 final Drawable buttonDrawable = mButtonDrawable; 257 return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0; 258 } 259 260 @Override onDraw(Canvas canvas)261 protected void onDraw(Canvas canvas) { 262 super.onDraw(canvas); 263 264 final Drawable buttonDrawable = mButtonDrawable; 265 if (buttonDrawable != null) { 266 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 267 final int drawableHeight = buttonDrawable.getIntrinsicHeight(); 268 final int drawableWidth = buttonDrawable.getIntrinsicWidth(); 269 270 int top = 0; 271 switch (verticalGravity) { 272 case Gravity.BOTTOM: 273 top = getHeight() - drawableHeight; 274 break; 275 case Gravity.CENTER_VERTICAL: 276 top = (getHeight() - drawableHeight) / 2; 277 break; 278 } 279 int bottom = top + drawableHeight; 280 int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; 281 int right = isLayoutRtl() ? getWidth() : drawableWidth; 282 283 buttonDrawable.setBounds(left, top, right, bottom); 284 buttonDrawable.draw(canvas); 285 } 286 } 287 288 @Override onCreateDrawableState(int extraSpace)289 protected int[] onCreateDrawableState(int extraSpace) { 290 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 291 if (isChecked()) { 292 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 293 } 294 return drawableState; 295 } 296 297 @Override drawableStateChanged()298 protected void drawableStateChanged() { 299 super.drawableStateChanged(); 300 301 if (mButtonDrawable != null) { 302 int[] myDrawableState = getDrawableState(); 303 304 // Set the state of the Drawable 305 mButtonDrawable.setState(myDrawableState); 306 307 invalidate(); 308 } 309 } 310 311 @Override verifyDrawable(Drawable who)312 protected boolean verifyDrawable(Drawable who) { 313 return super.verifyDrawable(who) || who == mButtonDrawable; 314 } 315 316 @Override jumpDrawablesToCurrentState()317 public void jumpDrawablesToCurrentState() { 318 super.jumpDrawablesToCurrentState(); 319 if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState(); 320 } 321 322 static class SavedState extends BaseSavedState { 323 boolean checked; 324 325 /** 326 * Constructor called from {@link CompoundButton#onSaveInstanceState()} 327 */ SavedState(Parcelable superState)328 SavedState(Parcelable superState) { 329 super(superState); 330 } 331 332 /** 333 * Constructor called from {@link #CREATOR} 334 */ SavedState(Parcel in)335 private SavedState(Parcel in) { 336 super(in); 337 checked = (Boolean)in.readValue(null); 338 } 339 340 @Override writeToParcel(Parcel out, int flags)341 public void writeToParcel(Parcel out, int flags) { 342 super.writeToParcel(out, flags); 343 out.writeValue(checked); 344 } 345 346 @Override toString()347 public String toString() { 348 return "CompoundButton.SavedState{" 349 + Integer.toHexString(System.identityHashCode(this)) 350 + " checked=" + checked + "}"; 351 } 352 353 public static final Parcelable.Creator<SavedState> CREATOR 354 = new Parcelable.Creator<SavedState>() { 355 public SavedState createFromParcel(Parcel in) { 356 return new SavedState(in); 357 } 358 359 public SavedState[] newArray(int size) { 360 return new SavedState[size]; 361 } 362 }; 363 } 364 365 @Override onSaveInstanceState()366 public Parcelable onSaveInstanceState() { 367 // Force our ancestor class to save its state 368 setFreezesText(true); 369 Parcelable superState = super.onSaveInstanceState(); 370 371 SavedState ss = new SavedState(superState); 372 373 ss.checked = isChecked(); 374 return ss; 375 } 376 377 @Override onRestoreInstanceState(Parcelable state)378 public void onRestoreInstanceState(Parcelable state) { 379 SavedState ss = (SavedState) state; 380 381 super.onRestoreInstanceState(ss.getSuperState()); 382 setChecked(ss.checked); 383 requestLayout(); 384 } 385 } 386