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.incall.impl;
18 
19 import android.animation.AnimatorInflater;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.PorterDuff.Mode;
23 import android.graphics.drawable.Drawable;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.support.annotation.DrawableRes;
27 import android.support.annotation.StringRes;
28 import android.text.TextUtils.TruncateAt;
29 import android.util.AttributeSet;
30 import android.view.Gravity;
31 import android.view.SoundEffectConstants;
32 import android.widget.Checkable;
33 import android.widget.ImageView;
34 import android.widget.ImageView.ScaleType;
35 import android.widget.LinearLayout;
36 import android.widget.TextView;
37 
38 /** A button to show on the incall screen */
39 public class CheckableLabeledButton extends LinearLayout implements Checkable {
40 
41   private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
42   private static final float DISABLED_STATE_OPACITY = .3f;
43   private boolean broadcasting;
44   private boolean isChecked;
45   private OnCheckedChangeListener onCheckedChangeListener;
46   private ImageView iconView;
47   private TextView labelView;
48   private Drawable background;
49   private Drawable backgroundMore;
50 
CheckableLabeledButton(Context context, AttributeSet attrs)51   public CheckableLabeledButton(Context context, AttributeSet attrs) {
52     super(context, attrs);
53     init(context, attrs);
54   }
55 
CheckableLabeledButton(Context context)56   public CheckableLabeledButton(Context context) {
57     this(context, null);
58   }
59 
init(Context context, AttributeSet attrs)60   private void init(Context context, AttributeSet attrs) {
61     setOrientation(VERTICAL);
62     setGravity(Gravity.CENTER_HORIZONTAL);
63     Drawable icon;
64     CharSequence labelText;
65     boolean enabled;
66 
67     backgroundMore = getResources().getDrawable(R.drawable.incall_button_background_more, null);
68     background = getResources().getDrawable(R.drawable.incall_button_background, null);
69 
70     TypedArray typedArray =
71         context.obtainStyledAttributes(attrs, R.styleable.CheckableLabeledButton);
72     icon = typedArray.getDrawable(R.styleable.CheckableLabeledButton_incall_icon);
73     labelText = typedArray.getString(R.styleable.CheckableLabeledButton_incall_labelText);
74     enabled = typedArray.getBoolean(R.styleable.CheckableLabeledButton_android_enabled, true);
75     typedArray.recycle();
76 
77     int paddingSize = getResources().getDimensionPixelOffset(R.dimen.incall_button_padding);
78     setPadding(paddingSize, paddingSize, paddingSize, paddingSize);
79 
80     int iconSize = getResources().getDimensionPixelSize(R.dimen.incall_labeled_button_size);
81 
82     iconView = new ImageView(context, null, android.R.style.Widget_Material_Button_Colored);
83     LayoutParams iconParams = generateDefaultLayoutParams();
84     iconParams.width = iconSize;
85     iconParams.height = iconSize;
86     iconView.setLayoutParams(iconParams);
87     iconView.setScaleType(ScaleType.CENTER_INSIDE);
88     iconView.setImageDrawable(icon);
89     iconView.setImageTintMode(Mode.SRC_IN);
90     iconView.setImageTintList(getResources().getColorStateList(R.color.incall_button_icon, null));
91     iconView.setBackground(getResources().getDrawable(R.drawable.incall_button_background, null));
92     iconView.setDuplicateParentStateEnabled(true);
93     iconView.setElevation(getResources().getDimension(R.dimen.incall_button_elevation));
94     iconView.setStateListAnimator(
95         AnimatorInflater.loadStateListAnimator(context, R.animator.incall_button_elevation));
96     addView(iconView);
97 
98     labelView = new TextView(context);
99     LayoutParams labelParams = generateDefaultLayoutParams();
100     labelParams.width = LayoutParams.WRAP_CONTENT;
101     labelParams.height = LayoutParams.WRAP_CONTENT;
102     labelParams.topMargin =
103         context.getResources().getDimensionPixelOffset(R.dimen.incall_button_label_margin);
104     labelView.setLayoutParams(labelParams);
105     labelView.setTextAppearance(R.style.Dialer_Incall_TextAppearance_Label);
106     labelView.setText(labelText);
107     labelView.setSingleLine();
108     labelView.setMaxEms(9);
109     labelView.setEllipsize(TruncateAt.END);
110     labelView.setGravity(Gravity.CENTER);
111     labelView.setDuplicateParentStateEnabled(true);
112     addView(labelView);
113 
114     setFocusable(true);
115     setClickable(true);
116     setEnabled(enabled);
117     setOutlineProvider(null);
118   }
119 
120   @Override
refreshDrawableState()121   public void refreshDrawableState() {
122     super.refreshDrawableState();
123     iconView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
124     labelView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
125   }
126 
setIconDrawable(@rawableRes int drawableRes)127   public void setIconDrawable(@DrawableRes int drawableRes) {
128     iconView.setImageResource(drawableRes);
129   }
130 
setLabelText(@tringRes int stringRes)131   public void setLabelText(@StringRes int stringRes) {
132     labelView.setText(stringRes);
133   }
134 
135   /** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
setShouldShowMoreIndicator(boolean shouldShow)136   public void setShouldShowMoreIndicator(boolean shouldShow) {
137     iconView.setBackground(shouldShow ? backgroundMore : background);
138   }
139 
140   @Override
isChecked()141   public boolean isChecked() {
142     return isChecked;
143   }
144 
145   @Override
setChecked(boolean checked)146   public void setChecked(boolean checked) {
147     performSetChecked(checked);
148   }
149 
150   @Override
toggle()151   public void toggle() {
152     userRequestedSetChecked(!isChecked());
153   }
154 
155   @Override
onCreateDrawableState(int extraSpace)156   public int[] onCreateDrawableState(int extraSpace) {
157     final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
158     if (isChecked()) {
159       mergeDrawableStates(drawableState, CHECKED_STATE_SET);
160     }
161     return drawableState;
162   }
163 
164   @Override
drawableStateChanged()165   protected void drawableStateChanged() {
166     super.drawableStateChanged();
167     invalidate();
168   }
169 
setOnCheckedChangeListener(OnCheckedChangeListener listener)170   public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
171     this.onCheckedChangeListener = listener;
172   }
173 
174   @Override
performClick()175   public boolean performClick() {
176     if (!isCheckable()) {
177       return super.performClick();
178     }
179 
180     toggle();
181     final boolean handled = super.performClick();
182     if (!handled) {
183       // View only makes a sound effect if the onClickListener was
184       // called, so we'll need to make one here instead.
185       playSoundEffect(SoundEffectConstants.CLICK);
186     }
187     return handled;
188   }
189 
isCheckable()190   private boolean isCheckable() {
191     return onCheckedChangeListener != null;
192   }
193 
194   @Override
onRestoreInstanceState(Parcelable state)195   protected void onRestoreInstanceState(Parcelable state) {
196     SavedState savedState = (SavedState) state;
197     super.onRestoreInstanceState(savedState.getSuperState());
198     performSetChecked(savedState.isChecked);
199     requestLayout();
200   }
201 
202   @Override
onSaveInstanceState()203   protected Parcelable onSaveInstanceState() {
204     return new SavedState(isChecked(), super.onSaveInstanceState());
205   }
206 
207   /**
208    * Called when the state of the button should be updated, this should not be the result of user
209    * interaction.
210    *
211    * @param checked {@code true} if the button should be in the checked state, {@code false}
212    *     otherwise.
213    */
performSetChecked(boolean checked)214   private void performSetChecked(boolean checked) {
215     if (isChecked() == checked) {
216       return;
217     }
218     isChecked = checked;
219     refreshDrawableState();
220   }
221 
222   /**
223    * Called when the user interacts with a button. This should not result in the button updating
224    * state, rather the request should be propagated to the associated listener.
225    *
226    * @param checked {@code true} if the button should be in the checked state, {@code false}
227    *     otherwise.
228    */
userRequestedSetChecked(boolean checked)229   private void userRequestedSetChecked(boolean checked) {
230     if (isChecked() == checked) {
231       return;
232     }
233     if (broadcasting) {
234       return;
235     }
236     broadcasting = true;
237     if (onCheckedChangeListener != null) {
238       onCheckedChangeListener.onCheckedChanged(this, checked);
239     }
240     broadcasting = false;
241   }
242 
243   /** Callback interface to notify when the button's checked state has changed */
244   public interface OnCheckedChangeListener {
245 
onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked)246     void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked);
247   }
248 
249   private static class SavedState extends BaseSavedState {
250 
251     public static final Creator<SavedState> CREATOR =
252         new Creator<SavedState>() {
253           @Override
254           public SavedState createFromParcel(Parcel in) {
255             return new SavedState(in);
256           }
257 
258           @Override
259           public SavedState[] newArray(int size) {
260             return new SavedState[size];
261           }
262         };
263     public final boolean isChecked;
264 
SavedState(boolean isChecked, Parcelable superState)265     private SavedState(boolean isChecked, Parcelable superState) {
266       super(superState);
267       this.isChecked = isChecked;
268     }
269 
SavedState(Parcel in)270     protected SavedState(Parcel in) {
271       super(in);
272       isChecked = in.readByte() != 0;
273     }
274 
275     @Override
describeContents()276     public int describeContents() {
277       return 0;
278     }
279 
280     @Override
writeToParcel(Parcel dest, int flags)281     public void writeToParcel(Parcel dest, int flags) {
282       super.writeToParcel(dest, flags);
283       dest.writeByte((byte) (isChecked ? 1 : 0));
284     }
285   }
286 }
287