1 /* 2 * Copyright (C) 2016 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 com.android.incallui.rtt.impl; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.os.Parcel; 22 import android.os.Parcelable; 23 import android.util.AttributeSet; 24 import android.view.SoundEffectConstants; 25 import android.widget.Button; 26 import android.widget.Checkable; 27 28 /** Image button that maintains a checked state. */ 29 public class RttCheckableButton extends Button implements Checkable { 30 31 private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; 32 33 /** Callback interface to notify when the button's checked state has changed */ 34 public interface OnCheckedChangeListener { 35 onCheckedChanged(RttCheckableButton button, boolean isChecked)36 void onCheckedChanged(RttCheckableButton button, boolean isChecked); 37 } 38 39 private boolean broadcasting; 40 private boolean isChecked; 41 private OnCheckedChangeListener onCheckedChangeListener; 42 private CharSequence contentDescriptionChecked; 43 private CharSequence contentDescriptionUnchecked; 44 RttCheckableButton(Context context)45 public RttCheckableButton(Context context) { 46 this(context, null); 47 } 48 RttCheckableButton(Context context, AttributeSet attrs)49 public RttCheckableButton(Context context, AttributeSet attrs) { 50 this(context, attrs, android.R.attr.imageButtonStyle); 51 } 52 RttCheckableButton(Context context, AttributeSet attrs, int defStyleAttr)53 public RttCheckableButton(Context context, AttributeSet attrs, int defStyleAttr) { 54 this(context, attrs, defStyleAttr, 0); 55 } 56 RttCheckableButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)57 public RttCheckableButton( 58 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 59 super(context, attrs, defStyleAttr, defStyleRes); 60 init(context, attrs); 61 } 62 init(Context context, AttributeSet attrs)63 private void init(Context context, AttributeSet attrs) { 64 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RttCheckableButton); 65 setChecked(typedArray.getBoolean(R.styleable.RttCheckableButton_android_checked, false)); 66 contentDescriptionChecked = 67 typedArray.getText(R.styleable.RttCheckableButton_contentDescriptionChecked); 68 contentDescriptionUnchecked = 69 typedArray.getText(R.styleable.RttCheckableButton_contentDescriptionUnchecked); 70 typedArray.recycle(); 71 72 updateContentDescription(); 73 setClickable(true); 74 setFocusable(true); 75 } 76 77 @Override setChecked(boolean checked)78 public void setChecked(boolean checked) { 79 performSetChecked(checked); 80 } 81 82 /** 83 * Called when the state of the button should be updated, this should not be the result of user 84 * interaction. 85 * 86 * @param checked {@code true} if the button should be in the checked state, {@code false} 87 * otherwise. 88 */ performSetChecked(boolean checked)89 private void performSetChecked(boolean checked) { 90 if (isChecked() == checked) { 91 return; 92 } 93 isChecked = checked; 94 CharSequence contentDescription = updateContentDescription(); 95 announceForAccessibility(contentDescription); 96 refreshDrawableState(); 97 } 98 updateContentDescription()99 private CharSequence updateContentDescription() { 100 CharSequence contentDescription = 101 isChecked ? contentDescriptionChecked : contentDescriptionUnchecked; 102 setContentDescription(contentDescription); 103 return contentDescription; 104 } 105 106 /** 107 * Called when the user interacts with a button. This should not result in the button updating 108 * state, rather the request should be propagated to the associated listener. 109 * 110 * @param checked {@code true} if the button should be in the checked state, {@code false} 111 * otherwise. 112 */ userRequestedSetChecked(boolean checked)113 private void userRequestedSetChecked(boolean checked) { 114 if (isChecked() == checked) { 115 return; 116 } 117 if (broadcasting) { 118 return; 119 } 120 broadcasting = true; 121 if (onCheckedChangeListener != null) { 122 onCheckedChangeListener.onCheckedChanged(this, checked); 123 } 124 broadcasting = false; 125 } 126 127 @Override isChecked()128 public boolean isChecked() { 129 return isChecked; 130 } 131 132 @Override toggle()133 public void toggle() { 134 userRequestedSetChecked(!isChecked()); 135 } 136 137 @Override onCreateDrawableState(int extraSpace)138 public int[] onCreateDrawableState(int extraSpace) { 139 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 140 if (isChecked()) { 141 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 142 } 143 return drawableState; 144 } 145 146 @Override drawableStateChanged()147 protected void drawableStateChanged() { 148 super.drawableStateChanged(); 149 invalidate(); 150 } 151 setOnCheckedChangeListener(OnCheckedChangeListener listener)152 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { 153 this.onCheckedChangeListener = listener; 154 } 155 156 @Override performClick()157 public boolean performClick() { 158 if (!isCheckable()) { 159 return super.performClick(); 160 } 161 162 toggle(); 163 final boolean handled = super.performClick(); 164 if (!handled) { 165 // View only makes a sound effect if the onClickListener was 166 // called, so we'll need to make one here instead. 167 playSoundEffect(SoundEffectConstants.CLICK); 168 } 169 return handled; 170 } 171 isCheckable()172 private boolean isCheckable() { 173 return onCheckedChangeListener != null; 174 } 175 176 @Override onRestoreInstanceState(Parcelable state)177 public void onRestoreInstanceState(Parcelable state) { 178 SavedState savedState = (SavedState) state; 179 super.onRestoreInstanceState(savedState.getSuperState()); 180 performSetChecked(savedState.isChecked); 181 requestLayout(); 182 } 183 184 @Override onSaveInstanceState()185 public Parcelable onSaveInstanceState() { 186 return new SavedState(isChecked(), super.onSaveInstanceState()); 187 } 188 189 private static class SavedState extends BaseSavedState { 190 191 public final boolean isChecked; 192 SavedState(boolean isChecked, Parcelable superState)193 private SavedState(boolean isChecked, Parcelable superState) { 194 super(superState); 195 this.isChecked = isChecked; 196 } 197 SavedState(Parcel in)198 protected SavedState(Parcel in) { 199 super(in); 200 isChecked = in.readByte() != 0; 201 } 202 203 public static final Creator<SavedState> CREATOR = 204 new Creator<SavedState>() { 205 @Override 206 public SavedState createFromParcel(Parcel in) { 207 return new SavedState(in); 208 } 209 210 @Override 211 public SavedState[] newArray(int size) { 212 return new SavedState[size]; 213 } 214 }; 215 216 @Override describeContents()217 public int describeContents() { 218 return 0; 219 } 220 221 @Override writeToParcel(Parcel dest, int flags)222 public void writeToParcel(Parcel dest, int flags) { 223 super.writeToParcel(dest, flags); 224 dest.writeByte((byte) (isChecked ? 1 : 0)); 225 } 226 } 227 } 228