1 /* 2 * Copyright (C) 2018 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.settingslib.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.drawable.Drawable; 23 import android.text.TextUtils; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.View; 27 import android.widget.Button; 28 29 import androidx.annotation.DrawableRes; 30 import androidx.annotation.StringRes; 31 import androidx.preference.Preference; 32 import androidx.preference.PreferenceViewHolder; 33 34 import com.android.settingslib.utils.BuildCompatUtils; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * This preference provides a four buttons layout with Settings style. 41 * It looks like below 42 * 43 * --------------------------------------- 44 * - button1 | button2 | button3 | button4 - 45 * --------------------------------------- 46 * 47 * User can set title / icon / click listener for each button. 48 * 49 * By default, four buttons are visible. 50 * However, there are two cases which button should be invisible(View.GONE). 51 * 52 * 1. User sets invisible for button. ex: ActionButtonPreference.setButton1Visible(false) 53 * 2. User doesn't set any title or icon for button. 54 */ 55 public class ActionButtonsPreference extends Preference { 56 57 private static final String TAG = "ActionButtonPreference"; 58 private static final boolean mIsAtLeastS = BuildCompatUtils.isAtLeastS(); 59 private static final int SINGLE_BUTTON_STYLE = 1; 60 private static final int TWO_BUTTONS_STYLE = 2; 61 private static final int THREE_BUTTONS_STYLE = 3; 62 private static final int FOUR_BUTTONS_STYLE = 4; 63 64 private final ButtonInfo mButton1Info = new ButtonInfo(); 65 private final ButtonInfo mButton2Info = new ButtonInfo(); 66 private final ButtonInfo mButton3Info = new ButtonInfo(); 67 private final ButtonInfo mButton4Info = new ButtonInfo(); 68 private final List<ButtonInfo> mVisibleButtonInfos = new ArrayList<>(4); 69 private final List<Drawable> mBtnBackgroundStyle1 = new ArrayList<>(1); 70 private final List<Drawable> mBtnBackgroundStyle2 = new ArrayList<>(2); 71 private final List<Drawable> mBtnBackgroundStyle3 = new ArrayList<>(3); 72 private final List<Drawable> mBtnBackgroundStyle4 = new ArrayList<>(4); 73 74 private View mDivider1; 75 private View mDivider2; 76 private View mDivider3; 77 ActionButtonsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)78 public ActionButtonsPreference(Context context, AttributeSet attrs, 79 int defStyleAttr, int defStyleRes) { 80 super(context, attrs, defStyleAttr, defStyleRes); 81 init(); 82 } 83 ActionButtonsPreference(Context context, AttributeSet attrs, int defStyleAttr)84 public ActionButtonsPreference(Context context, AttributeSet attrs, int defStyleAttr) { 85 super(context, attrs, defStyleAttr); 86 init(); 87 } 88 ActionButtonsPreference(Context context, AttributeSet attrs)89 public ActionButtonsPreference(Context context, AttributeSet attrs) { 90 super(context, attrs); 91 init(); 92 } 93 ActionButtonsPreference(Context context)94 public ActionButtonsPreference(Context context) { 95 super(context); 96 init(); 97 } 98 init()99 private void init() { 100 setLayoutResource(R.layout.settingslib_action_buttons); 101 setSelectable(false); 102 103 final Resources res = getContext().getResources(); 104 fetchDrawableArray(mBtnBackgroundStyle1, res.obtainTypedArray(R.array.background_style1)); 105 fetchDrawableArray(mBtnBackgroundStyle2, res.obtainTypedArray(R.array.background_style2)); 106 fetchDrawableArray(mBtnBackgroundStyle3, res.obtainTypedArray(R.array.background_style3)); 107 fetchDrawableArray(mBtnBackgroundStyle4, res.obtainTypedArray(R.array.background_style4)); 108 } 109 fetchDrawableArray(List<Drawable> drawableList, TypedArray typedArray)110 private void fetchDrawableArray(List<Drawable> drawableList, TypedArray typedArray) { 111 for (int i = 0; i < typedArray.length(); i++) { 112 drawableList.add( 113 getContext().getDrawable(typedArray.getResourceId(i, 0 /* defValue */))); 114 } 115 } 116 117 @Override onBindViewHolder(PreferenceViewHolder holder)118 public void onBindViewHolder(PreferenceViewHolder holder) { 119 super.onBindViewHolder(holder); 120 121 holder.setDividerAllowedAbove(!mIsAtLeastS); 122 holder.setDividerAllowedBelow(!mIsAtLeastS); 123 124 mButton1Info.mButton = (Button) holder.findViewById(R.id.button1); 125 mButton2Info.mButton = (Button) holder.findViewById(R.id.button2); 126 mButton3Info.mButton = (Button) holder.findViewById(R.id.button3); 127 mButton4Info.mButton = (Button) holder.findViewById(R.id.button4); 128 129 mDivider1 = holder.findViewById(R.id.divider1); 130 mDivider2 = holder.findViewById(R.id.divider2); 131 mDivider3 = holder.findViewById(R.id.divider3); 132 133 mButton1Info.setUpButton(); 134 mButton2Info.setUpButton(); 135 mButton3Info.setUpButton(); 136 mButton4Info.setUpButton(); 137 138 // Clear info list to avoid duplicate setup. 139 if (!mVisibleButtonInfos.isEmpty()) { 140 mVisibleButtonInfos.clear(); 141 } 142 updateLayout(); 143 } 144 145 @Override notifyChanged()146 protected void notifyChanged() { 147 super.notifyChanged(); 148 149 // Update buttons background and layout when notified and visible button list exist. 150 if (!mVisibleButtonInfos.isEmpty()) { 151 mVisibleButtonInfos.clear(); 152 updateLayout(); 153 } 154 } 155 updateLayout()156 private void updateLayout() { 157 // Add visible button into list only when platform version is newer than S. 158 if (mButton1Info.isVisible() && mIsAtLeastS) { 159 mVisibleButtonInfos.add(mButton1Info); 160 } 161 if (mButton2Info.isVisible() && mIsAtLeastS) { 162 mVisibleButtonInfos.add(mButton2Info); 163 } 164 if (mButton3Info.isVisible() && mIsAtLeastS) { 165 mVisibleButtonInfos.add(mButton3Info); 166 } 167 if (mButton4Info.isVisible() && mIsAtLeastS) { 168 mVisibleButtonInfos.add(mButton4Info); 169 } 170 171 final boolean isRtl = getContext().getResources().getConfiguration() 172 .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 173 switch (mVisibleButtonInfos.size()) { 174 case SINGLE_BUTTON_STYLE : 175 if (isRtl) { 176 setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); 177 } else { 178 setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); 179 } 180 break; 181 case TWO_BUTTONS_STYLE : 182 if (isRtl) { 183 setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); 184 } else { 185 setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); 186 } 187 break; 188 case THREE_BUTTONS_STYLE : 189 if (isRtl) { 190 setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); 191 } else { 192 setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); 193 } 194 break; 195 case FOUR_BUTTONS_STYLE : 196 if (isRtl) { 197 setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); 198 } else { 199 setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); 200 } 201 break; 202 default: 203 Log.e(TAG, "No visible buttons info, skip background settings."); 204 break; 205 } 206 207 setupDivider1(); 208 setupDivider2(); 209 setupDivider3(); 210 } 211 setupBackgrounds( List<ButtonInfo> buttonInfoList, List<Drawable> buttonBackgroundStyles)212 private void setupBackgrounds( 213 List<ButtonInfo> buttonInfoList, List<Drawable> buttonBackgroundStyles) { 214 for (int i = 0; i < buttonBackgroundStyles.size(); i++) { 215 buttonInfoList.get(i).mButton.setBackground(buttonBackgroundStyles.get(i)); 216 } 217 } 218 setupRtlBackgrounds( List<ButtonInfo> buttonInfoList, List<Drawable> buttonBackgroundStyles)219 private void setupRtlBackgrounds( 220 List<ButtonInfo> buttonInfoList, List<Drawable> buttonBackgroundStyles) { 221 for (int i = buttonBackgroundStyles.size() - 1; i >= 0; i--) { 222 buttonInfoList.get(buttonBackgroundStyles.size() - 1 - i) 223 .mButton.setBackground(buttonBackgroundStyles.get(i)); 224 } 225 } 226 227 /** 228 * Set the visibility state of button1. 229 */ setButton1Visible(boolean isVisible)230 public ActionButtonsPreference setButton1Visible(boolean isVisible) { 231 if (isVisible != mButton1Info.mIsVisible) { 232 mButton1Info.mIsVisible = isVisible; 233 notifyChanged(); 234 } 235 return this; 236 } 237 238 /** 239 * Sets the text to be displayed in button1. 240 */ setButton1Text(@tringRes int textResId)241 public ActionButtonsPreference setButton1Text(@StringRes int textResId) { 242 final String newText = getContext().getString(textResId); 243 if (!TextUtils.equals(newText, mButton1Info.mText)) { 244 mButton1Info.mText = newText; 245 notifyChanged(); 246 } 247 return this; 248 } 249 250 /** 251 * Sets the drawable to be displayed above of text in button1. 252 */ setButton1Icon(@rawableRes int iconResId)253 public ActionButtonsPreference setButton1Icon(@DrawableRes int iconResId) { 254 if (iconResId == 0) { 255 return this; 256 } 257 258 final Drawable icon; 259 try { 260 icon = getContext().getDrawable(iconResId); 261 mButton1Info.mIcon = icon; 262 notifyChanged(); 263 } catch (Resources.NotFoundException exception) { 264 Log.e(TAG, "Resource does not exist: " + iconResId); 265 } 266 return this; 267 } 268 269 /** 270 * Set the enabled state of button1. 271 */ setButton1Enabled(boolean isEnabled)272 public ActionButtonsPreference setButton1Enabled(boolean isEnabled) { 273 if (isEnabled != mButton1Info.mIsEnabled) { 274 mButton1Info.mIsEnabled = isEnabled; 275 notifyChanged(); 276 } 277 return this; 278 } 279 280 /** 281 * Register a callback to be invoked when button1 is clicked. 282 */ setButton1OnClickListener( View.OnClickListener listener)283 public ActionButtonsPreference setButton1OnClickListener( 284 View.OnClickListener listener) { 285 if (listener != mButton1Info.mListener) { 286 mButton1Info.mListener = listener; 287 notifyChanged(); 288 } 289 return this; 290 } 291 292 /** 293 * Set the visibility state of button2. 294 */ setButton2Visible(boolean isVisible)295 public ActionButtonsPreference setButton2Visible(boolean isVisible) { 296 if (isVisible != mButton2Info.mIsVisible) { 297 mButton2Info.mIsVisible = isVisible; 298 notifyChanged(); 299 } 300 return this; 301 } 302 303 /** 304 * Sets the text to be displayed in button2. 305 */ setButton2Text(@tringRes int textResId)306 public ActionButtonsPreference setButton2Text(@StringRes int textResId) { 307 final String newText = getContext().getString(textResId); 308 if (!TextUtils.equals(newText, mButton2Info.mText)) { 309 mButton2Info.mText = newText; 310 notifyChanged(); 311 } 312 return this; 313 } 314 315 /** 316 * Sets the drawable to be displayed above of text in button2. 317 */ setButton2Icon(@rawableRes int iconResId)318 public ActionButtonsPreference setButton2Icon(@DrawableRes int iconResId) { 319 if (iconResId == 0) { 320 return this; 321 } 322 323 final Drawable icon; 324 try { 325 icon = getContext().getDrawable(iconResId); 326 mButton2Info.mIcon = icon; 327 notifyChanged(); 328 } catch (Resources.NotFoundException exception) { 329 Log.e(TAG, "Resource does not exist: " + iconResId); 330 } 331 return this; 332 } 333 334 /** 335 * Set the enabled state of button2. 336 */ setButton2Enabled(boolean isEnabled)337 public ActionButtonsPreference setButton2Enabled(boolean isEnabled) { 338 if (isEnabled != mButton2Info.mIsEnabled) { 339 mButton2Info.mIsEnabled = isEnabled; 340 notifyChanged(); 341 } 342 return this; 343 } 344 345 /** 346 * Register a callback to be invoked when button2 is clicked. 347 */ setButton2OnClickListener( View.OnClickListener listener)348 public ActionButtonsPreference setButton2OnClickListener( 349 View.OnClickListener listener) { 350 if (listener != mButton2Info.mListener) { 351 mButton2Info.mListener = listener; 352 notifyChanged(); 353 } 354 return this; 355 } 356 357 /** 358 * Set the visibility state of button3. 359 */ setButton3Visible(boolean isVisible)360 public ActionButtonsPreference setButton3Visible(boolean isVisible) { 361 if (isVisible != mButton3Info.mIsVisible) { 362 mButton3Info.mIsVisible = isVisible; 363 notifyChanged(); 364 } 365 return this; 366 } 367 368 /** 369 * Sets the text to be displayed in button3. 370 */ setButton3Text(@tringRes int textResId)371 public ActionButtonsPreference setButton3Text(@StringRes int textResId) { 372 final String newText = getContext().getString(textResId); 373 if (!TextUtils.equals(newText, mButton3Info.mText)) { 374 mButton3Info.mText = newText; 375 notifyChanged(); 376 } 377 return this; 378 } 379 380 /** 381 * Sets the drawable to be displayed above of text in button3. 382 */ setButton3Icon(@rawableRes int iconResId)383 public ActionButtonsPreference setButton3Icon(@DrawableRes int iconResId) { 384 if (iconResId == 0) { 385 return this; 386 } 387 388 final Drawable icon; 389 try { 390 icon = getContext().getDrawable(iconResId); 391 mButton3Info.mIcon = icon; 392 notifyChanged(); 393 } catch (Resources.NotFoundException exception) { 394 Log.e(TAG, "Resource does not exist: " + iconResId); 395 } 396 return this; 397 } 398 399 /** 400 * Set the enabled state of button3. 401 */ setButton3Enabled(boolean isEnabled)402 public ActionButtonsPreference setButton3Enabled(boolean isEnabled) { 403 if (isEnabled != mButton3Info.mIsEnabled) { 404 mButton3Info.mIsEnabled = isEnabled; 405 notifyChanged(); 406 } 407 return this; 408 } 409 410 /** 411 * Register a callback to be invoked when button3 is clicked. 412 */ setButton3OnClickListener( View.OnClickListener listener)413 public ActionButtonsPreference setButton3OnClickListener( 414 View.OnClickListener listener) { 415 if (listener != mButton3Info.mListener) { 416 mButton3Info.mListener = listener; 417 notifyChanged(); 418 } 419 return this; 420 } 421 422 /** 423 * Set the visibility state of button4. 424 */ setButton4Visible(boolean isVisible)425 public ActionButtonsPreference setButton4Visible(boolean isVisible) { 426 if (isVisible != mButton4Info.mIsVisible) { 427 mButton4Info.mIsVisible = isVisible; 428 notifyChanged(); 429 } 430 return this; 431 } 432 433 /** 434 * Sets the text to be displayed in button4. 435 */ setButton4Text(@tringRes int textResId)436 public ActionButtonsPreference setButton4Text(@StringRes int textResId) { 437 final String newText = getContext().getString(textResId); 438 if (!TextUtils.equals(newText, mButton4Info.mText)) { 439 mButton4Info.mText = newText; 440 notifyChanged(); 441 } 442 return this; 443 } 444 445 /** 446 * Sets the drawable to be displayed above of text in button4. 447 */ setButton4Icon(@rawableRes int iconResId)448 public ActionButtonsPreference setButton4Icon(@DrawableRes int iconResId) { 449 if (iconResId == 0) { 450 return this; 451 } 452 453 final Drawable icon; 454 try { 455 icon = getContext().getDrawable(iconResId); 456 mButton4Info.mIcon = icon; 457 notifyChanged(); 458 } catch (Resources.NotFoundException exception) { 459 Log.e(TAG, "Resource does not exist: " + iconResId); 460 } 461 return this; 462 } 463 464 /** 465 * Set the enabled state of button4. 466 */ setButton4Enabled(boolean isEnabled)467 public ActionButtonsPreference setButton4Enabled(boolean isEnabled) { 468 if (isEnabled != mButton4Info.mIsEnabled) { 469 mButton4Info.mIsEnabled = isEnabled; 470 notifyChanged(); 471 } 472 return this; 473 } 474 475 /** 476 * Register a callback to be invoked when button4 is clicked. 477 */ setButton4OnClickListener( View.OnClickListener listener)478 public ActionButtonsPreference setButton4OnClickListener( 479 View.OnClickListener listener) { 480 if (listener != mButton4Info.mListener) { 481 mButton4Info.mListener = listener; 482 notifyChanged(); 483 } 484 return this; 485 } 486 setupDivider1()487 private void setupDivider1() { 488 // Display divider1 only if button1 and button2 is visible 489 if (mDivider1 != null && mButton1Info.isVisible() && mButton2Info.isVisible()) { 490 mDivider1.setVisibility(View.VISIBLE); 491 } 492 } 493 setupDivider2()494 private void setupDivider2() { 495 // Display divider2 only if button3 is visible and button2 or button3 is visible 496 if (mDivider2 != null && mButton3Info.isVisible() 497 && (mButton1Info.isVisible() || mButton2Info.isVisible())) { 498 mDivider2.setVisibility(View.VISIBLE); 499 } 500 } 501 setupDivider3()502 private void setupDivider3() { 503 // Display divider3 only if button4 is visible and 2 visible buttons at least 504 if (mDivider3 != null && mVisibleButtonInfos.size() > 1 && mButton4Info.isVisible()) { 505 mDivider3.setVisibility(View.VISIBLE); 506 } 507 } 508 509 static class ButtonInfo { 510 private Button mButton; 511 private CharSequence mText; 512 private Drawable mIcon; 513 private View.OnClickListener mListener; 514 private boolean mIsEnabled = true; 515 private boolean mIsVisible = true; 516 setUpButton()517 void setUpButton() { 518 mButton.setText(mText); 519 mButton.setOnClickListener(mListener); 520 mButton.setEnabled(mIsEnabled); 521 mButton.setCompoundDrawablesWithIntrinsicBounds( 522 null /* left */, mIcon /* top */, null /* right */, null /* bottom */); 523 524 if (shouldBeVisible()) { 525 mButton.setVisibility(View.VISIBLE); 526 } else { 527 mButton.setVisibility(View.GONE); 528 } 529 } 530 isVisible()531 boolean isVisible() { 532 return mButton.getVisibility() == View.VISIBLE; 533 } 534 535 /** 536 * By default, four buttons are visible. 537 * However, there are two cases which button should be invisible. 538 * 539 * 1. User set invisible for this button. ex: mIsVisible = false. 540 * 2. User didn't set any title or icon. 541 */ shouldBeVisible()542 private boolean shouldBeVisible() { 543 return mIsVisible && (!TextUtils.isEmpty(mText) || mIcon != null); 544 } 545 } 546 } 547