1 /* 2 * Copyright (C) 2023 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.DeviceAsWebcam.view; 18 19 import android.animation.ObjectAnimator; 20 import android.content.Context; 21 import android.util.AttributeSet; 22 import android.util.Range; 23 import android.view.Gravity; 24 import android.view.LayoutInflater; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.accessibility.AccessibilityEvent; 28 import android.view.animation.AccelerateDecelerateInterpolator; 29 import android.widget.FrameLayout; 30 import android.widget.SeekBar; 31 import android.widget.SeekBar.OnSeekBarChangeListener; 32 import android.widget.TextView; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 import androidx.core.util.Preconditions; 37 38 import com.android.DeviceAsWebcam.R; 39 40 import java.math.BigDecimal; 41 import java.math.RoundingMode; 42 import java.text.DecimalFormat; 43 44 /** 45 * A custom zoom controller to allow users to adjust their preferred zoom ratio setting. 46 */ 47 public class ZoomController extends FrameLayout { 48 /** 49 * Zoom UI toggle mode. 50 */ 51 public static final int ZOOM_UI_TOGGLE_MODE = 0; 52 /** 53 * Zoom UI seek bar mode. 54 */ 55 public static final int ZOOM_UI_SEEK_BAR_MODE = 1; 56 /** 57 * The max zoom progress of the controller. 58 */ 59 private static final int MAX_ZOOM_PROGRESS = 100000; 60 /** 61 * The toggle UI auto-show duration in ms. 62 */ 63 private static final int TOGGLE_UI_AUTO_SHOW_DURATION_MS = 1000; 64 private static final int TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS = 7000; 65 /** 66 * The invalid x position used when translating the motion events to the seek bar progress. 67 */ 68 private static final float INVALID_X_POSITION = -1.0f; 69 /** 70 * Current zoom UI mode. 71 */ 72 private int mZoomUiMode = ZOOM_UI_TOGGLE_MODE; 73 private View mToggleUiOptions; 74 private View mTouchOverlay; 75 private View mToggleUiBackground; 76 private View mToggleButtonSelected; 77 private SeekBar mSeekBar; 78 private ZoomKnob mZoomKnob; 79 /** 80 * TextView of the low sticky zoom ratio value option item. 81 */ 82 private TextView mToggleOptionLow; 83 /** 84 * TextView of the middle sticky zoom ratio value option item. 85 */ 86 private TextView mToggleOptionMiddle; 87 /** 88 * TextView of the high sticky zoom ratio value option item. 89 */ 90 private TextView mToggleOptionHigh; 91 /** 92 * Default low sticky zoom ratio value. 93 */ 94 private float mDefaultLowStickyZoomRatio; 95 /** 96 * Default middle sticky zoom ratio value. 97 */ 98 private float mDefaultMiddleStickyZoomRatio = 1.0f; 99 /** 100 * Default high sticky zoom ratio value. 101 */ 102 private float mDefaultHighStickyZoomRatio; 103 /** 104 * Current low sticky zoom ratio value. 105 */ 106 private float mCurrentLowStickyZoomRatio; 107 /** 108 * Current middle sticky zoom ratio value. 109 */ 110 private float mCurrentMiddleStickyZoomRatio; 111 /** 112 * Current high sticky zoom ratio value. 113 */ 114 private float mCurrentHighStickyZoomRatio; 115 /** 116 * The min supported zoom ratio value. 117 */ 118 private float mMinZoomRatio; 119 /** 120 * The max supported zoom ratio value. 121 */ 122 private float mMaxZoomRatio; 123 /** 124 * Current zoom ratio value. 125 */ 126 private float mCurrentZoomRatio; 127 /** 128 * Current toggle option count. 129 */ 130 private int mToggleOptionCount = 3; 131 private final Runnable mToggleUiAutoShowRunnable = () -> switchZoomUiMode(ZOOM_UI_TOGGLE_MODE); 132 /** 133 * The registered zoom ratio updated listener. 134 */ 135 private OnZoomRatioUpdatedListener mOnZoomRatioUpdatedListener = null; 136 private boolean mFirstPositionSkipped = false; 137 private float mPreviousXPosition = INVALID_X_POSITION; 138 139 /** 140 * Timeout for toggling between slider and buttons. This is 141 * {@link #TOGGLE_UI_AUTO_SHOW_DURATION_MS} normally, and increases to 142 * {@link #TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS} when accessibility services are 143 * enabled. 144 */ 145 private int mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_MS; 146 ZoomController(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)147 public ZoomController(@NonNull Context context, @Nullable AttributeSet attrs, 148 int defStyleAttr) { 149 super(context, attrs, defStyleAttr); 150 } 151 ZoomController(@onNull Context context, @Nullable AttributeSet attrs)152 public ZoomController(@NonNull Context context, @Nullable AttributeSet attrs) { 153 super(context, attrs); 154 } 155 ZoomController(@onNull Context context)156 public ZoomController(@NonNull Context context) { 157 super(context); 158 } 159 160 /** 161 * Initializes the controller. 162 * 163 * @param layoutInflater to inflate the zoom ui layout 164 * @param zoomRatioRange the supported zoom ratio range 165 */ init(LayoutInflater layoutInflater, Range<Float> zoomRatioRange)166 public void init(LayoutInflater layoutInflater, Range<Float> zoomRatioRange) { 167 removeAllViews(); 168 addView(layoutInflater.inflate(R.layout.zoom_controller, null)); 169 170 mToggleUiOptions = findViewById(R.id.zoom_ui_toggle_options); 171 mTouchOverlay = findViewById(R.id.zoom_ui_overlay); 172 mToggleUiBackground = findViewById(R.id.zoom_ui_toggle_background); 173 mToggleButtonSelected = findViewById(R.id.zoom_ui_toggle_btn_selected); 174 mSeekBar = findViewById(R.id.zoom_ui_seekbar_slider); 175 mZoomKnob = findViewById(R.id.zoom_ui_knob); 176 mToggleOptionLow = findViewById(R.id.zoom_ui_toggle_option_low); 177 mToggleOptionMiddle = findViewById(R.id.zoom_ui_toggle_option_middle); 178 mToggleOptionHigh = findViewById(R.id.zoom_ui_toggle_option_high); 179 180 switchZoomUiMode(mZoomUiMode); 181 182 mSeekBar.setMax(MAX_ZOOM_PROGRESS); 183 mZoomKnob.initialize(mSeekBar, MAX_ZOOM_PROGRESS); 184 setSupportedZoomRatioRange(zoomRatioRange); 185 186 // Monitors the touch events on the toggle UI to update the zoom ratio value. 187 mTouchOverlay.setOnTouchListener((v, event) -> { 188 if (mZoomUiMode == ZOOM_UI_TOGGLE_MODE) { 189 updateSelectedZoomToggleOptionByMotionEvent(event); 190 } else { 191 updateSeekBarProgressByMotionEvent(event); 192 } 193 return false; 194 }); 195 196 mTouchOverlay.setOnClickListener(v -> { 197 // Empty click listener to ensure none of the elements underneath 198 // the overlay receive an event. 199 }); 200 // Long click events will trigger to switch the zoom ui mode 201 mTouchOverlay.setOnLongClickListener(v -> { 202 switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE); 203 return false; 204 }); 205 206 mToggleOptionLow.setOnClickListener((v) -> 207 setToggleUiZoomRatio(mCurrentLowStickyZoomRatio, 0)); 208 mToggleOptionMiddle.setOnClickListener((v) -> 209 setToggleUiZoomRatio(mCurrentMiddleStickyZoomRatio, 1)); 210 mToggleOptionHigh.setOnClickListener((v) -> 211 setToggleUiZoomRatio(mCurrentHighStickyZoomRatio, 2)); 212 213 mToggleUiOptions.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); 214 mToggleOptionLow.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); 215 mToggleOptionMiddle.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); 216 mToggleOptionHigh.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); 217 218 mSeekBar.setOnSeekBarChangeListener( 219 new OnSeekBarChangeListener() { 220 @Override 221 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 222 if (!fromUser) { 223 return; 224 } 225 updateZoomKnobByProgress(progress); 226 setZoomRatioInternal(convertProgressToZoomRatio(progress), true); 227 resetToggleUiAutoShowRunnable(); 228 } 229 230 @Override 231 public void onStartTrackingTouch(SeekBar seekBar) { 232 mZoomKnob.setElevated(true); 233 removeToggleUiAutoShowRunnable(); 234 } 235 236 @Override 237 public void onStopTrackingTouch(SeekBar seekBar) { 238 mZoomKnob.setElevated(false); 239 resetToggleUiAutoShowRunnable(); 240 } 241 } 242 ); 243 } 244 245 @Override setEnabled(boolean enabled)246 public void setEnabled(boolean enabled) { 247 super.setEnabled(enabled); 248 // Force disable these controls so that AccessibilityTraversalAfter won't take effects on 249 // these controls when they are disabled. 250 mToggleUiOptions.setEnabled(enabled); 251 mToggleOptionLow.setEnabled(enabled); 252 mToggleOptionMiddle.setEnabled(enabled); 253 mToggleOptionHigh.setEnabled(enabled); 254 } 255 256 /** 257 * Sets the supported zoom ratio range to the controller. 258 */ setSupportedZoomRatioRange(Range<Float> zoomRatioRange)259 public void setSupportedZoomRatioRange(Range<Float> zoomRatioRange) { 260 Preconditions.checkArgument(zoomRatioRange.getLower() > 0, 261 "The minimal zoom ratio must be positive."); 262 mMinZoomRatio = zoomRatioRange.getLower(); 263 mMaxZoomRatio = zoomRatioRange.getUpper(); 264 mCurrentZoomRatio = 1.0f; 265 266 // The default low sticky value will always be the min supported zoom ratio 267 mDefaultLowStickyZoomRatio = mMinZoomRatio; 268 269 // Supports 3 toggle options if min supported zoom ratio is smaller than 1.0f and the max 270 // supported zoom ratio is larger than 2.0f after rounding 271 if (mMinZoomRatio < 0.95f && mMaxZoomRatio >= 2.05f) { 272 transformToggleUiByOptionCount(3); 273 // Sets the high sticky zoom ratio as 2.0f 274 mDefaultHighStickyZoomRatio = 2.0f; 275 } else { 276 transformToggleUiByOptionCount(2); 277 // Sets the high sticky zoom ratio as 2.0f if the max supported zoom ratio is larger 278 // than 2.0f after rounding. Otherwise, sets it as the max supported zoom ratio value. 279 mDefaultHighStickyZoomRatio = mMaxZoomRatio >= 2.05f ? 2.0f : mMaxZoomRatio; 280 } 281 updateToggleOptionValues(); 282 removeToggleUiAutoShowRunnable(); 283 switchZoomUiMode(ZOOM_UI_TOGGLE_MODE); 284 } 285 286 /** 287 * Updates the toggle option values according to current zoom ratio value. 288 * 289 * <p>If the camera device supports the min zoom ratio smaller than 1.0 and the max zoom ratio 290 * larger than 2.0, three toggle options are supported: 291 * - In the beginning, three sticky value options [smallest zoom ratio value, 1.0, 2.0] 292 * will be provided. 293 * - After end users change the zoom ratio: 294 * - If the new zoom ratio setting is smaller than 1.0, the sticky value options will 295 * become [new zoom ratio value, 1.0, 2.0]. 296 * - If the new zoom ratio setting is ">= 1.0" and "< 2.0", the sticky value options will 297 * become [smallest zoom ratio value, new zoom ratio value, 2.0]. 298 * - If the new zoom ratio setting is ">= 2.0", the sticky value options will become 299 * [smallest zoom ratio value, 1.0, new zoom ratio value]. 300 * 301 * <p>Otherwise, two toggle options are supported: 302 * - In the beginning, two sticky value options [smallest zoom ratio value, 303 * min(2.0, largest zoom ratio value)] will be provided. 304 * - After end users change the zoom ratio: 305 * - If the new zoom ratio setting is ">= smallest zoom ratio value" and 306 * "< min(2.0, largest zoom ratio value)", the sticky value options will become 307 * [new zoom ratio value, min(2.0, largest zoom ratio value)]. 308 * - If the new zoom ratio setting is ">= min92.0, largest zoom ratio value)", the sticky 309 * value options will become [smallest zoom ratio value, new zoom ratio value] 310 */ updateToggleOptionValues()311 private void updateToggleOptionValues() { 312 if (mToggleOptionCount == 3) { 313 mToggleOptionMiddle.setText(convertZoomRatioToString(mCurrentMiddleStickyZoomRatio)); 314 if (mCurrentZoomRatio < (mDefaultMiddleStickyZoomRatio - 0.05f)) { 315 setSelectedZoomToggleOption(0); 316 mCurrentLowStickyZoomRatio = mCurrentZoomRatio; 317 mCurrentMiddleStickyZoomRatio = mDefaultMiddleStickyZoomRatio; 318 mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; 319 } else if (mCurrentZoomRatio >= (mDefaultMiddleStickyZoomRatio - 0.05f) 320 && mCurrentZoomRatio < (mDefaultHighStickyZoomRatio - 0.05f)) { 321 setSelectedZoomToggleOption(1); 322 mCurrentLowStickyZoomRatio = roundZoomRatio(mMinZoomRatio); 323 mCurrentMiddleStickyZoomRatio = mCurrentZoomRatio; 324 mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; 325 } else { 326 setSelectedZoomToggleOption(2); 327 mCurrentLowStickyZoomRatio = roundZoomRatio(mMinZoomRatio); 328 mCurrentMiddleStickyZoomRatio = mDefaultMiddleStickyZoomRatio; 329 mCurrentHighStickyZoomRatio = mCurrentZoomRatio; 330 } 331 mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); 332 mToggleOptionMiddle.setText(convertZoomRatioToString(mCurrentMiddleStickyZoomRatio)); 333 mToggleOptionHigh.setText(convertZoomRatioToString(mCurrentHighStickyZoomRatio)); 334 } else { 335 mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); 336 if (mCurrentZoomRatio < (mDefaultHighStickyZoomRatio - 0.05f)) { 337 setSelectedZoomToggleOption(0); 338 mCurrentLowStickyZoomRatio = mCurrentZoomRatio; 339 mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; 340 } else { 341 setSelectedZoomToggleOption(2); 342 mCurrentLowStickyZoomRatio = mDefaultLowStickyZoomRatio; 343 mCurrentHighStickyZoomRatio = mCurrentZoomRatio; 344 } 345 mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); 346 mToggleOptionHigh.setText(convertZoomRatioToString(mCurrentHighStickyZoomRatio)); 347 } 348 } 349 350 /** 351 * Sets the text display rotation of the text in the controller. 352 */ setTextDisplayRotation(int rotation, int animationDurationMs)353 public void setTextDisplayRotation(int rotation, int animationDurationMs) { 354 ObjectAnimator anim1 = ObjectAnimator.ofFloat(mToggleOptionLow, 355 /*propertyName=*/"rotation", rotation) 356 .setDuration(animationDurationMs); 357 anim1.setInterpolator(new AccelerateDecelerateInterpolator()); 358 anim1.start(); 359 ObjectAnimator anim2 = ObjectAnimator.ofFloat(mToggleOptionMiddle, 360 /*propertyName=*/"rotation", rotation) 361 .setDuration(animationDurationMs); 362 anim2.setInterpolator(new AccelerateDecelerateInterpolator()); 363 anim2.start(); 364 ObjectAnimator anim3 = ObjectAnimator.ofFloat(mToggleOptionHigh, 365 /*propertyName=*/"rotation", rotation) 366 .setDuration(animationDurationMs); 367 anim3.setInterpolator(new AccelerateDecelerateInterpolator()); 368 anim3.start(); 369 ObjectAnimator animZoomKnob = ObjectAnimator.ofFloat(mZoomKnob, 370 /*propertyName=*/"rotation", rotation) 371 .setDuration(animationDurationMs); 372 animZoomKnob.setInterpolator(new AccelerateDecelerateInterpolator()); 373 animZoomKnob.start(); 374 } 375 376 /** 377 * Sets zoom ratio value to the controller. 378 */ setZoomRatio(float zoomRatio, int zoomUiMode)379 public void setZoomRatio(float zoomRatio, int zoomUiMode) { 380 setZoomRatioInternal(zoomRatio, false); 381 updateZoomKnobByZoomRatio(zoomRatio); 382 mSeekBar.setProgress(convertZoomRatioToProgress(zoomRatio)); 383 switchZoomUiMode(zoomUiMode); 384 resetToggleUiAutoShowRunnable(); 385 } 386 387 /** 388 * Sets zoom ratio value and notify the zoom ratio change to the listener according to the 389 * input notifyZoomRatioChange value. 390 */ setZoomRatioInternal(float zoomRatio, boolean notifyZoomRatioChange)391 private void setZoomRatioInternal(float zoomRatio, boolean notifyZoomRatioChange) { 392 float roundedZoomRatio = roundZoomRatio( 393 Math.max(mMinZoomRatio, Math.min(zoomRatio, mMaxZoomRatio))); 394 395 if (mCurrentZoomRatio != roundedZoomRatio && notifyZoomRatioChange 396 && mOnZoomRatioUpdatedListener != null) { 397 mOnZoomRatioUpdatedListener.onValueChanged(roundedZoomRatio); 398 } 399 400 boolean sendAccessibilityEvent = roundedZoomRatio != mCurrentZoomRatio && 401 (int) 402 (Math.floor(roundedZoomRatio) - 403 Math.floor(mCurrentZoomRatio)) != 0; 404 405 mCurrentZoomRatio = roundedZoomRatio; 406 updateToggleOptionValues(); 407 mSeekBar.setStateDescription(Float.toString(mCurrentZoomRatio)); 408 mToggleUiOptions.setStateDescription(Float.toString(mCurrentZoomRatio)); 409 if (sendAccessibilityEvent) { 410 mSeekBar.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); 411 } 412 } 413 414 /** 415 * Sets an {@link OnZoomRatioUpdatedListener} to receive zoom ratio changes from the controller. 416 */ setOnZoomRatioUpdatedListener(OnZoomRatioUpdatedListener listener)417 public void setOnZoomRatioUpdatedListener(OnZoomRatioUpdatedListener listener) { 418 mOnZoomRatioUpdatedListener = listener; 419 } 420 421 /** 422 * Method to be called if Accessibility Services are enabled/disabled. This should 423 * be called by the parent activity/fragment to ensure that ZoomController is more 424 * more functional when used with Accessibility Services. 425 * 426 * @param enabled whether accessibility services are enabled or not 427 */ onAccessibilityServicesEnabled(boolean enabled)428 public void onAccessibilityServicesEnabled(boolean enabled) { 429 if (mTouchOverlay == null) { 430 return; 431 } 432 433 // Hide the overlay as touch events don't work well with Accessibility Services 434 // When Accessibility Services are enabled, we provide a somewhat less refined, 435 // but more accessible UX flow for changing zoom. 436 if (enabled) { 437 mTouchOverlay.setVisibility(View.GONE); 438 mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS; 439 } else { 440 mTouchOverlay.setVisibility(View.VISIBLE); 441 mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_MS; 442 } 443 } 444 445 /** 446 * Converts the input float zoom ratio value to string which is rounded with 447 * RoundingMode.HALF_UP to one decimal digit. 448 */ convertZoomRatioToString(float zoomRatio)449 static String convertZoomRatioToString(float zoomRatio) { 450 DecimalFormat zoomRatioDf = new DecimalFormat("0.0"); 451 zoomRatioDf.setRoundingMode(RoundingMode.HALF_UP); 452 return zoomRatioDf.format(roundZoomRatio(zoomRatio)); 453 } 454 455 /** 456 * Rounds the input float zoom ratio value with RoundingMode.HALF_UP to one decimal digit. 457 */ roundZoomRatio(float zoomRatio)458 static float roundZoomRatio(float zoomRatio) { 459 // Keep one decimal digit since; we also follow the same in convertZoomRatioToString() 460 BigDecimal bigDec = new BigDecimal(zoomRatio); 461 return bigDec.setScale(1, RoundingMode.HALF_UP).floatValue(); 462 } 463 464 /** 465 * Switches the UI to the toggle or seek bar mode. 466 */ switchZoomUiMode(int zoomUiMode)467 private boolean switchZoomUiMode(int zoomUiMode) { 468 mZoomUiMode = zoomUiMode; 469 int toggleUiVisibility = (zoomUiMode == ZOOM_UI_TOGGLE_MODE) ? View.VISIBLE : View.GONE; 470 mToggleUiOptions.setVisibility(toggleUiVisibility); 471 mToggleButtonSelected.setVisibility(toggleUiVisibility); 472 mToggleUiBackground.setVisibility(toggleUiVisibility); 473 474 int seekBarUiVisibility = (zoomUiMode == ZOOM_UI_SEEK_BAR_MODE) ? View.VISIBLE : View.GONE; 475 mSeekBar.setVisibility(seekBarUiVisibility); 476 mZoomKnob.setVisibility(seekBarUiVisibility); 477 478 return false; 479 } 480 481 /** 482 * Transforms the toggle button UI layout for the desired option count. 483 * 484 * <p>The medium toggle option will be hidden when toggle option count is 2. The layout width 485 * will also be shorten to only keep the space for two toggle buttons. 486 * 487 * @param toggleOptionCount only 2 or 3 toggle option count is supported. 488 */ transformToggleUiByOptionCount(int toggleOptionCount)489 private void transformToggleUiByOptionCount(int toggleOptionCount) { 490 mToggleOptionCount = toggleOptionCount; 491 int layoutWidth; 492 493 switch (toggleOptionCount) { 494 case 2 -> { 495 layoutWidth = getResources().getDimensionPixelSize( 496 R.dimen.zoom_ui_toggle_two_options_layout_width); 497 mToggleOptionMiddle.setVisibility(View.GONE); 498 setSelectedZoomToggleOption(0); 499 } 500 case 3 -> { 501 layoutWidth = getResources().getDimensionPixelSize( 502 R.dimen.zoom_ui_toggle_three_options_layout_width); 503 mToggleOptionMiddle.setVisibility(View.VISIBLE); 504 setSelectedZoomToggleOption(1); 505 } 506 default -> throw new IllegalArgumentException("Unsupported toggle option count!"); 507 } 508 509 LayoutParams lp = (LayoutParams) mToggleUiOptions.getLayoutParams(); 510 lp.width = layoutWidth; 511 mToggleUiOptions.setLayoutParams(lp); 512 513 lp = (LayoutParams) mToggleUiBackground.getLayoutParams(); 514 lp.width = layoutWidth; 515 mToggleUiBackground.setLayoutParams(lp); 516 } 517 518 /** 519 * Updates the selected zoom toggle option by the motion events. 520 * 521 * <p>Mark the toggle option as selected when the motion event is in their own layout range. 522 */ updateSelectedZoomToggleOptionByMotionEvent(MotionEvent event)523 private void updateSelectedZoomToggleOptionByMotionEvent(MotionEvent event) { 524 float updatedZoomRatio; 525 int toggleOption; 526 527 int zoomToggleUiWidth = mToggleUiOptions.getWidth(); 528 if (event.getX() <= zoomToggleUiWidth / mToggleOptionCount) { 529 toggleOption = 0; 530 updatedZoomRatio = mCurrentLowStickyZoomRatio; 531 } else if (event.getX() 532 > zoomToggleUiWidth * (mToggleOptionCount - 1) / mToggleOptionCount) { 533 toggleOption = 2; 534 updatedZoomRatio = mCurrentHighStickyZoomRatio; 535 } else { 536 toggleOption = 1; 537 updatedZoomRatio = mCurrentMiddleStickyZoomRatio; 538 } 539 540 setToggleUiZoomRatio(updatedZoomRatio, toggleOption); 541 } 542 setToggleUiZoomRatio(float currentStickyZoomRatio, int currentZoomToggleOption)543 private void setToggleUiZoomRatio(float currentStickyZoomRatio, 544 int currentZoomToggleOption) { 545 setSelectedZoomToggleOption(currentZoomToggleOption); 546 // Updates the knob seek bar and zoom ratio value according to the newly selected option. 547 if (currentStickyZoomRatio != mCurrentZoomRatio) { 548 updateZoomKnobByZoomRatio(currentStickyZoomRatio); 549 mSeekBar.setProgress(convertZoomRatioToProgress(currentStickyZoomRatio)); 550 setZoomRatioInternal(currentStickyZoomRatio, true); 551 } 552 } 553 554 /** 555 * Sets the specific zoom toggle option UI as selected. 556 */ setSelectedZoomToggleOption(int optionIndex)557 private void setSelectedZoomToggleOption(int optionIndex) { 558 mToggleOptionLow.setStateDescription(null); 559 mToggleOptionMiddle.setStateDescription(null); 560 mToggleOptionHigh.setStateDescription(null); 561 562 String stateDesc = mContext.getString(R.string.zoom_ratio_button_current_description); 563 LayoutParams lp = (LayoutParams) mToggleButtonSelected.getLayoutParams(); 564 switch (optionIndex) { 565 case 0 -> { 566 lp.leftMargin = getResources().getDimensionPixelSize( 567 R.dimen.zoom_ui_toggle_padding); 568 lp.rightMargin = 0; 569 lp.gravity = Gravity.CENTER_VERTICAL | Gravity.LEFT; 570 mToggleOptionLow.setStateDescription(stateDesc); 571 } 572 case 1 -> { 573 lp.leftMargin = 0; 574 lp.rightMargin = 0; 575 lp.gravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 576 mToggleOptionMiddle.setStateDescription(stateDesc); 577 } 578 case 2 -> { 579 lp.leftMargin = 0; 580 lp.rightMargin = getResources().getDimensionPixelSize( 581 R.dimen.zoom_ui_toggle_padding); 582 lp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; 583 mToggleOptionHigh.setStateDescription(stateDesc); 584 } 585 default -> throw new IllegalArgumentException("Unsupported toggle option index!"); 586 } 587 mToggleButtonSelected.setLayoutParams(lp); 588 } 589 590 /** 591 * Updates the seek bar progress by the motion events. 592 * 593 * <p>The seek bar is disabled until the end users long-click on the toggle button options and 594 * a ACTION_UP motion event is received. When the seek bar is disabled, the motion events will 595 * be translated to the new progress values and updated to the knob and seek bar. 596 */ updateSeekBarProgressByMotionEvent(MotionEvent event)597 private void updateSeekBarProgressByMotionEvent(MotionEvent event) { 598 if (mPreviousXPosition == INVALID_X_POSITION) { 599 if (!mFirstPositionSkipped) { 600 mFirstPositionSkipped = true; 601 } else { 602 mPreviousXPosition = event.getX(); 603 } 604 return; 605 } 606 607 mZoomKnob.setElevated(event.getAction() != MotionEvent.ACTION_UP); 608 609 int seekBarWidth = mSeekBar.getWidth(); 610 float zoomRatio = roundZoomRatio(mCurrentZoomRatio 611 + (mMaxZoomRatio - mMinZoomRatio) * (event.getX() - mPreviousXPosition) 612 / (float) seekBarWidth); 613 zoomRatio = Math.max(mMinZoomRatio, Math.min(zoomRatio, mMaxZoomRatio)); 614 updateZoomKnobByZoomRatio(zoomRatio); 615 mSeekBar.setProgress(convertZoomRatioToProgress(zoomRatio)); 616 setZoomRatioInternal(zoomRatio, true); 617 618 if (event.getAction() == MotionEvent.ACTION_UP) { 619 mFirstPositionSkipped = false; 620 mPreviousXPosition = INVALID_X_POSITION; 621 resetToggleUiAutoShowRunnable(); 622 } else { 623 mPreviousXPosition = event.getX(); 624 } 625 } 626 627 /** 628 * Updates the zoom knob by the progress value. 629 */ updateZoomKnobByProgress(int progress)630 private void updateZoomKnobByProgress(int progress) { 631 mZoomKnob.updateZoomProgress(progress, convertProgressToZoomRatio(progress)); 632 } 633 634 /** 635 * Converts the progress value to the zoom ratio value. 636 */ convertProgressToZoomRatio(int progress)637 private float convertProgressToZoomRatio(int progress) { 638 return roundZoomRatio( 639 mMinZoomRatio + (mMaxZoomRatio - mMinZoomRatio) * progress / MAX_ZOOM_PROGRESS); 640 } 641 642 /** 643 * Updates the zoom knob by the zoom ratio value. 644 */ updateZoomKnobByZoomRatio(float zoomRatio)645 private void updateZoomKnobByZoomRatio(float zoomRatio) { 646 mZoomKnob.updateZoomProgress(convertZoomRatioToProgress(zoomRatio), zoomRatio); 647 } 648 649 /** 650 * Converts the zoom ratio to the progress value value. 651 */ convertZoomRatioToProgress(float zoomRatio)652 private int convertZoomRatioToProgress(float zoomRatio) { 653 return (int) ((zoomRatio - mMinZoomRatio) / (mMaxZoomRatio - mMinZoomRatio) 654 * MAX_ZOOM_PROGRESS); 655 } 656 657 /** 658 * Resets the toggle UI auto-show runnable. 659 */ resetToggleUiAutoShowRunnable()660 private void resetToggleUiAutoShowRunnable() { 661 removeToggleUiAutoShowRunnable(); 662 postDelayed(mToggleUiAutoShowRunnable, mToggleAutoShowDurationMs); 663 } 664 665 /** 666 * Removes the toggle UI auto-show runnable. 667 */ removeToggleUiAutoShowRunnable()668 private void removeToggleUiAutoShowRunnable() { 669 removeCallbacks(mToggleUiAutoShowRunnable); 670 } 671 672 /** 673 * The listener to monitor the value change of the zoom controller. 674 */ 675 public interface OnZoomRatioUpdatedListener { 676 677 /** 678 * Invoked when the zoom ratio value is changed. 679 * 680 * @param value the updated zoom ratio value. 681 */ onValueChanged(float value)682 void onValueChanged(float value); 683 } 684 } 685