/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deviceaswebcam.view; import android.animation.ObjectAnimator; import android.content.Context; import android.util.AttributeSet; import android.util.Range; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Preconditions; import com.android.DeviceAsWebcam.R; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; /** * A custom zoom controller to allow users to adjust their preferred zoom ratio setting. */ public class ZoomController extends FrameLayout { /** * Zoom UI toggle mode. */ public static final int ZOOM_UI_TOGGLE_MODE = 0; /** * Zoom UI seek bar mode. */ public static final int ZOOM_UI_SEEK_BAR_MODE = 1; /** * The max zoom progress of the controller. */ private static final int MAX_ZOOM_PROGRESS = 100000; /** * The toggle UI auto-show duration in ms. */ private static final int TOGGLE_UI_AUTO_SHOW_DURATION_MS = 1000; private static final int TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS = 7000; /** * The invalid x position used when translating the motion events to the seek bar progress. */ private static final float INVALID_X_POSITION = -1.0f; /** * Current zoom UI mode. */ private int mZoomUiMode = ZOOM_UI_TOGGLE_MODE; private View mToggleUiOptions; private View mTouchOverlay; private View mToggleUiBackground; private View mToggleButtonSelected; private SeekBar mSeekBar; private ZoomKnob mZoomKnob; /** * TextView of the low sticky zoom ratio value option item. */ private TextView mToggleOptionLow; /** * TextView of the middle sticky zoom ratio value option item. */ private TextView mToggleOptionMiddle; /** * TextView of the high sticky zoom ratio value option item. */ private TextView mToggleOptionHigh; /** * Default low sticky zoom ratio value. */ private float mDefaultLowStickyZoomRatio; /** * Default middle sticky zoom ratio value. */ private float mDefaultMiddleStickyZoomRatio = 1.0f; /** * Default high sticky zoom ratio value. */ private float mDefaultHighStickyZoomRatio; /** * Current low sticky zoom ratio value. */ private float mCurrentLowStickyZoomRatio; /** * Current middle sticky zoom ratio value. */ private float mCurrentMiddleStickyZoomRatio; /** * Current high sticky zoom ratio value. */ private float mCurrentHighStickyZoomRatio; /** * The min supported zoom ratio value. */ private float mMinZoomRatio; /** * The max supported zoom ratio value. */ private float mMaxZoomRatio; /** * Current zoom ratio value. */ private float mCurrentZoomRatio; /** * Current toggle option count. */ private int mToggleOptionCount = 3; private final Runnable mToggleUiAutoShowRunnable = () -> switchZoomUiMode(ZOOM_UI_TOGGLE_MODE); /** * The registered zoom ratio updated listener. */ private OnZoomRatioUpdatedListener mOnZoomRatioUpdatedListener = null; private boolean mFirstPositionSkipped = false; private float mPreviousXPosition = INVALID_X_POSITION; /** * Timeout for toggling between slider and buttons. This is * {@link #TOGGLE_UI_AUTO_SHOW_DURATION_MS} normally, and increases to * {@link #TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS} when accessibility services are * enabled. */ private int mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_MS; public ZoomController(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ZoomController(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public ZoomController(@NonNull Context context) { super(context); } /** * Initializes the controller. * * @param layoutInflater to inflate the zoom ui layout * @param zoomRatioRange the supported zoom ratio range */ public void init(LayoutInflater layoutInflater, Range zoomRatioRange) { removeAllViews(); addView(layoutInflater.inflate(R.layout.zoom_controller, null)); mToggleUiOptions = findViewById(R.id.zoom_ui_toggle_options); mTouchOverlay = findViewById(R.id.zoom_ui_overlay); mToggleUiBackground = findViewById(R.id.zoom_ui_toggle_background); mToggleButtonSelected = findViewById(R.id.zoom_ui_toggle_btn_selected); mSeekBar = findViewById(R.id.zoom_ui_seekbar_slider); mZoomKnob = findViewById(R.id.zoom_ui_knob); mToggleOptionLow = findViewById(R.id.zoom_ui_toggle_option_low); mToggleOptionMiddle = findViewById(R.id.zoom_ui_toggle_option_middle); mToggleOptionHigh = findViewById(R.id.zoom_ui_toggle_option_high); switchZoomUiMode(mZoomUiMode); mSeekBar.setMax(MAX_ZOOM_PROGRESS); mZoomKnob.initialize(mSeekBar, MAX_ZOOM_PROGRESS); setSupportedZoomRatioRange(zoomRatioRange); // Monitors the touch events on the toggle UI to update the zoom ratio value. mTouchOverlay.setOnTouchListener((v, event) -> { if (mZoomUiMode == ZOOM_UI_TOGGLE_MODE) { updateSelectedZoomToggleOptionByMotionEvent(event); } else { updateSeekBarProgressByMotionEvent(event); } return false; }); mTouchOverlay.setOnClickListener(v -> { // Empty click listener to ensure none of the elements underneath // the overlay receive an event. }); // Long click events will trigger to switch the zoom ui mode mTouchOverlay.setOnLongClickListener(v -> { switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE); return false; }); mToggleOptionLow.setOnClickListener((v) -> setToggleUiZoomRatio(mCurrentLowStickyZoomRatio, 0)); mToggleOptionMiddle.setOnClickListener((v) -> setToggleUiZoomRatio(mCurrentMiddleStickyZoomRatio, 1)); mToggleOptionHigh.setOnClickListener((v) -> setToggleUiZoomRatio(mCurrentHighStickyZoomRatio, 2)); mToggleUiOptions.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); mToggleOptionLow.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); mToggleOptionMiddle.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); mToggleOptionHigh.setOnLongClickListener(v -> switchZoomUiMode(ZOOM_UI_SEEK_BAR_MODE)); mSeekBar.setOnSeekBarChangeListener( new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (!fromUser) { return; } updateZoomKnobByProgress(progress); setZoomRatioInternal(convertProgressToZoomRatio(progress), true); resetToggleUiAutoShowRunnable(); } @Override public void onStartTrackingTouch(SeekBar seekBar) { mZoomKnob.setElevated(true); removeToggleUiAutoShowRunnable(); } @Override public void onStopTrackingTouch(SeekBar seekBar) { mZoomKnob.setElevated(false); resetToggleUiAutoShowRunnable(); } } ); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); // Force disable these controls so that AccessibilityTraversalAfter won't take effects on // these controls when they are disabled. mToggleUiOptions.setEnabled(enabled); mToggleOptionLow.setEnabled(enabled); mToggleOptionMiddle.setEnabled(enabled); mToggleOptionHigh.setEnabled(enabled); } /** * Sets the supported zoom ratio range to the controller. */ public void setSupportedZoomRatioRange(Range zoomRatioRange) { Preconditions.checkArgument(zoomRatioRange.getLower() > 0, "The minimal zoom ratio must be positive."); mMinZoomRatio = zoomRatioRange.getLower(); mMaxZoomRatio = zoomRatioRange.getUpper(); mCurrentZoomRatio = 1.0f; // The default low sticky value will always be the min supported zoom ratio mDefaultLowStickyZoomRatio = mMinZoomRatio; // Supports 3 toggle options if min supported zoom ratio is smaller than 1.0f and the max // supported zoom ratio is larger than 2.0f after rounding if (mMinZoomRatio < 0.95f && mMaxZoomRatio >= 2.05f) { transformToggleUiByOptionCount(3); // Sets the high sticky zoom ratio as 2.0f mDefaultHighStickyZoomRatio = 2.0f; } else { transformToggleUiByOptionCount(2); // Sets the high sticky zoom ratio as 2.0f if the max supported zoom ratio is larger // than 2.0f after rounding. Otherwise, sets it as the max supported zoom ratio value. mDefaultHighStickyZoomRatio = mMaxZoomRatio >= 2.05f ? 2.0f : mMaxZoomRatio; } updateToggleOptionValues(); removeToggleUiAutoShowRunnable(); switchZoomUiMode(ZOOM_UI_TOGGLE_MODE); } /** * Updates the toggle option values according to current zoom ratio value. * *

If the camera device supports the min zoom ratio smaller than 1.0 and the max zoom ratio * larger than 2.0, three toggle options are supported: * - In the beginning, three sticky value options [smallest zoom ratio value, 1.0, 2.0] * will be provided. * - After end users change the zoom ratio: * - If the new zoom ratio setting is smaller than 1.0, the sticky value options will * become [new zoom ratio value, 1.0, 2.0]. * - If the new zoom ratio setting is ">= 1.0" and "< 2.0", the sticky value options will * become [smallest zoom ratio value, new zoom ratio value, 2.0]. * - If the new zoom ratio setting is ">= 2.0", the sticky value options will become * [smallest zoom ratio value, 1.0, new zoom ratio value]. * *

Otherwise, two toggle options are supported: * - In the beginning, two sticky value options [smallest zoom ratio value, * min(2.0, largest zoom ratio value)] will be provided. * - After end users change the zoom ratio: * - If the new zoom ratio setting is ">= smallest zoom ratio value" and * "< min(2.0, largest zoom ratio value)", the sticky value options will become * [new zoom ratio value, min(2.0, largest zoom ratio value)]. * - If the new zoom ratio setting is ">= min92.0, largest zoom ratio value)", the sticky * value options will become [smallest zoom ratio value, new zoom ratio value] */ private void updateToggleOptionValues() { if (mToggleOptionCount == 3) { mToggleOptionMiddle.setText(convertZoomRatioToString(mCurrentMiddleStickyZoomRatio)); if (mCurrentZoomRatio < (mDefaultMiddleStickyZoomRatio - 0.05f)) { setSelectedZoomToggleOption(0); mCurrentLowStickyZoomRatio = mCurrentZoomRatio; mCurrentMiddleStickyZoomRatio = mDefaultMiddleStickyZoomRatio; mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; } else if (mCurrentZoomRatio >= (mDefaultMiddleStickyZoomRatio - 0.05f) && mCurrentZoomRatio < (mDefaultHighStickyZoomRatio - 0.05f)) { setSelectedZoomToggleOption(1); mCurrentLowStickyZoomRatio = roundZoomRatio(mMinZoomRatio); mCurrentMiddleStickyZoomRatio = mCurrentZoomRatio; mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; } else { setSelectedZoomToggleOption(2); mCurrentLowStickyZoomRatio = roundZoomRatio(mMinZoomRatio); mCurrentMiddleStickyZoomRatio = mDefaultMiddleStickyZoomRatio; mCurrentHighStickyZoomRatio = mCurrentZoomRatio; } mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); mToggleOptionMiddle.setText(convertZoomRatioToString(mCurrentMiddleStickyZoomRatio)); mToggleOptionHigh.setText(convertZoomRatioToString(mCurrentHighStickyZoomRatio)); } else { mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); if (mCurrentZoomRatio < (mDefaultHighStickyZoomRatio - 0.05f)) { setSelectedZoomToggleOption(0); mCurrentLowStickyZoomRatio = mCurrentZoomRatio; mCurrentHighStickyZoomRatio = mDefaultHighStickyZoomRatio; } else { setSelectedZoomToggleOption(2); mCurrentLowStickyZoomRatio = mDefaultLowStickyZoomRatio; mCurrentHighStickyZoomRatio = mCurrentZoomRatio; } mToggleOptionLow.setText(convertZoomRatioToString(mCurrentLowStickyZoomRatio)); mToggleOptionHigh.setText(convertZoomRatioToString(mCurrentHighStickyZoomRatio)); } } /** * Sets the text display rotation of the text in the controller. */ public void setTextDisplayRotation(int rotation, int animationDurationMs) { ObjectAnimator anim1 = ObjectAnimator.ofFloat(mToggleOptionLow, /*propertyName=*/"rotation", rotation) .setDuration(animationDurationMs); anim1.setInterpolator(new AccelerateDecelerateInterpolator()); anim1.start(); ObjectAnimator anim2 = ObjectAnimator.ofFloat(mToggleOptionMiddle, /*propertyName=*/"rotation", rotation) .setDuration(animationDurationMs); anim2.setInterpolator(new AccelerateDecelerateInterpolator()); anim2.start(); ObjectAnimator anim3 = ObjectAnimator.ofFloat(mToggleOptionHigh, /*propertyName=*/"rotation", rotation) .setDuration(animationDurationMs); anim3.setInterpolator(new AccelerateDecelerateInterpolator()); anim3.start(); ObjectAnimator animZoomKnob = ObjectAnimator.ofFloat(mZoomKnob, /*propertyName=*/"rotation", rotation) .setDuration(animationDurationMs); animZoomKnob.setInterpolator(new AccelerateDecelerateInterpolator()); animZoomKnob.start(); } /** * Sets zoom ratio value to the controller. */ public void setZoomRatio(float zoomRatio, int zoomUiMode) { setZoomRatioInternal(zoomRatio, false); updateZoomKnobByZoomRatio(zoomRatio); mSeekBar.setProgress(convertZoomRatioToProgress(zoomRatio)); switchZoomUiMode(zoomUiMode); resetToggleUiAutoShowRunnable(); } /** * Sets zoom ratio value and notify the zoom ratio change to the listener according to the * input notifyZoomRatioChange value. */ private void setZoomRatioInternal(float zoomRatio, boolean notifyZoomRatioChange) { float roundedZoomRatio = roundZoomRatio( Math.max(mMinZoomRatio, Math.min(zoomRatio, mMaxZoomRatio))); if (mCurrentZoomRatio != roundedZoomRatio && notifyZoomRatioChange && mOnZoomRatioUpdatedListener != null) { mOnZoomRatioUpdatedListener.onValueChanged(roundedZoomRatio); } boolean sendAccessibilityEvent = roundedZoomRatio != mCurrentZoomRatio && (int) (Math.floor(roundedZoomRatio) - Math.floor(mCurrentZoomRatio)) != 0; mCurrentZoomRatio = roundedZoomRatio; updateToggleOptionValues(); mSeekBar.setStateDescription(Float.toString(mCurrentZoomRatio)); mToggleUiOptions.setStateDescription(Float.toString(mCurrentZoomRatio)); if (sendAccessibilityEvent) { mSeekBar.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); } } /** * Sets an {@link OnZoomRatioUpdatedListener} to receive zoom ratio changes from the controller. */ public void setOnZoomRatioUpdatedListener(OnZoomRatioUpdatedListener listener) { mOnZoomRatioUpdatedListener = listener; } /** * Method to be called if Accessibility Services are enabled/disabled. This should * be called by the parent activity/fragment to ensure that ZoomController is more * more functional when used with Accessibility Services. * * @param enabled whether accessibility services are enabled or not */ public void onAccessibilityServicesEnabled(boolean enabled) { if (mTouchOverlay == null) { return; } // Hide the overlay as touch events don't work well with Accessibility Services // When Accessibility Services are enabled, we provide a somewhat less refined, // but more accessible UX flow for changing zoom. if (enabled) { mTouchOverlay.setVisibility(View.GONE); mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_ACCESSIBILITY_MS; } else { mTouchOverlay.setVisibility(View.VISIBLE); mToggleAutoShowDurationMs = TOGGLE_UI_AUTO_SHOW_DURATION_MS; } } /** * Converts the input float zoom ratio value to string which is rounded with * RoundingMode.HALF_UP to one decimal digit. */ static String convertZoomRatioToString(float zoomRatio) { DecimalFormat zoomRatioDf = new DecimalFormat("0.0"); zoomRatioDf.setRoundingMode(RoundingMode.HALF_UP); return zoomRatioDf.format(roundZoomRatio(zoomRatio)); } /** * Rounds the input float zoom ratio value with RoundingMode.HALF_UP to one decimal digit. */ static float roundZoomRatio(float zoomRatio) { // Keep one decimal digit since; we also follow the same in convertZoomRatioToString() BigDecimal bigDec = new BigDecimal(zoomRatio); return bigDec.setScale(1, RoundingMode.HALF_UP).floatValue(); } /** * Switches the UI to the toggle or seek bar mode. */ private boolean switchZoomUiMode(int zoomUiMode) { mZoomUiMode = zoomUiMode; int toggleUiVisibility = (zoomUiMode == ZOOM_UI_TOGGLE_MODE) ? View.VISIBLE : View.GONE; mToggleUiOptions.setVisibility(toggleUiVisibility); mToggleButtonSelected.setVisibility(toggleUiVisibility); mToggleUiBackground.setVisibility(toggleUiVisibility); int seekBarUiVisibility = (zoomUiMode == ZOOM_UI_SEEK_BAR_MODE) ? View.VISIBLE : View.GONE; mSeekBar.setVisibility(seekBarUiVisibility); mZoomKnob.setVisibility(seekBarUiVisibility); return false; } /** * Transforms the toggle button UI layout for the desired option count. * *

The medium toggle option will be hidden when toggle option count is 2. The layout width * will also be shorten to only keep the space for two toggle buttons. * * @param toggleOptionCount only 2 or 3 toggle option count is supported. */ private void transformToggleUiByOptionCount(int toggleOptionCount) { mToggleOptionCount = toggleOptionCount; int layoutWidth; switch (toggleOptionCount) { case 2 -> { layoutWidth = getResources().getDimensionPixelSize( R.dimen.zoom_ui_toggle_two_options_layout_width); mToggleOptionMiddle.setVisibility(View.GONE); setSelectedZoomToggleOption(0); } case 3 -> { layoutWidth = getResources().getDimensionPixelSize( R.dimen.zoom_ui_toggle_three_options_layout_width); mToggleOptionMiddle.setVisibility(View.VISIBLE); setSelectedZoomToggleOption(1); } default -> throw new IllegalArgumentException("Unsupported toggle option count!"); } LayoutParams lp = (LayoutParams) mToggleUiOptions.getLayoutParams(); lp.width = layoutWidth; mToggleUiOptions.setLayoutParams(lp); lp = (LayoutParams) mToggleUiBackground.getLayoutParams(); lp.width = layoutWidth; mToggleUiBackground.setLayoutParams(lp); } /** * Updates the selected zoom toggle option by the motion events. * *

Mark the toggle option as selected when the motion event is in their own layout range. */ private void updateSelectedZoomToggleOptionByMotionEvent(MotionEvent event) { float updatedZoomRatio; int toggleOption; int zoomToggleUiWidth = mToggleUiOptions.getWidth(); if (event.getX() <= zoomToggleUiWidth / mToggleOptionCount) { toggleOption = 0; updatedZoomRatio = mCurrentLowStickyZoomRatio; } else if (event.getX() > zoomToggleUiWidth * (mToggleOptionCount - 1) / mToggleOptionCount) { toggleOption = 2; updatedZoomRatio = mCurrentHighStickyZoomRatio; } else { toggleOption = 1; updatedZoomRatio = mCurrentMiddleStickyZoomRatio; } setToggleUiZoomRatio(updatedZoomRatio, toggleOption); } private void setToggleUiZoomRatio(float currentStickyZoomRatio, int currentZoomToggleOption) { setSelectedZoomToggleOption(currentZoomToggleOption); // Updates the knob seek bar and zoom ratio value according to the newly selected option. if (currentStickyZoomRatio != mCurrentZoomRatio) { updateZoomKnobByZoomRatio(currentStickyZoomRatio); mSeekBar.setProgress(convertZoomRatioToProgress(currentStickyZoomRatio)); setZoomRatioInternal(currentStickyZoomRatio, true); } } /** * Sets the specific zoom toggle option UI as selected. */ private void setSelectedZoomToggleOption(int optionIndex) { mToggleOptionLow.setStateDescription(null); mToggleOptionMiddle.setStateDescription(null); mToggleOptionHigh.setStateDescription(null); String stateDesc = mContext.getString(R.string.zoom_ratio_button_current_description); LayoutParams lp = (LayoutParams) mToggleButtonSelected.getLayoutParams(); switch (optionIndex) { case 0 -> { lp.leftMargin = getResources().getDimensionPixelSize( R.dimen.zoom_ui_toggle_padding); lp.rightMargin = 0; lp.gravity = Gravity.CENTER_VERTICAL | Gravity.LEFT; mToggleOptionLow.setStateDescription(stateDesc); } case 1 -> { lp.leftMargin = 0; lp.rightMargin = 0; lp.gravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; mToggleOptionMiddle.setStateDescription(stateDesc); } case 2 -> { lp.leftMargin = 0; lp.rightMargin = getResources().getDimensionPixelSize( R.dimen.zoom_ui_toggle_padding); lp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; mToggleOptionHigh.setStateDescription(stateDesc); } default -> throw new IllegalArgumentException("Unsupported toggle option index!"); } mToggleButtonSelected.setLayoutParams(lp); } /** * Updates the seek bar progress by the motion events. * *

The seek bar is disabled until the end users long-click on the toggle button options and * a ACTION_UP motion event is received. When the seek bar is disabled, the motion events will * be translated to the new progress values and updated to the knob and seek bar. */ private void updateSeekBarProgressByMotionEvent(MotionEvent event) { if (mPreviousXPosition == INVALID_X_POSITION) { if (!mFirstPositionSkipped) { mFirstPositionSkipped = true; } else { mPreviousXPosition = event.getX(); } return; } mZoomKnob.setElevated(event.getAction() != MotionEvent.ACTION_UP); int seekBarWidth = mSeekBar.getWidth(); float zoomRatio = roundZoomRatio(mCurrentZoomRatio + (mMaxZoomRatio - mMinZoomRatio) * (event.getX() - mPreviousXPosition) / (float) seekBarWidth); zoomRatio = Math.max(mMinZoomRatio, Math.min(zoomRatio, mMaxZoomRatio)); updateZoomKnobByZoomRatio(zoomRatio); mSeekBar.setProgress(convertZoomRatioToProgress(zoomRatio)); setZoomRatioInternal(zoomRatio, true); if (event.getAction() == MotionEvent.ACTION_UP) { mFirstPositionSkipped = false; mPreviousXPosition = INVALID_X_POSITION; resetToggleUiAutoShowRunnable(); } else { mPreviousXPosition = event.getX(); } } /** * Updates the zoom knob by the progress value. */ private void updateZoomKnobByProgress(int progress) { mZoomKnob.updateZoomProgress(progress, convertProgressToZoomRatio(progress)); } /** * Converts the progress value to the zoom ratio value. */ private float convertProgressToZoomRatio(int progress) { return roundZoomRatio( mMinZoomRatio + (mMaxZoomRatio - mMinZoomRatio) * progress / MAX_ZOOM_PROGRESS); } /** * Updates the zoom knob by the zoom ratio value. */ private void updateZoomKnobByZoomRatio(float zoomRatio) { mZoomKnob.updateZoomProgress(convertZoomRatioToProgress(zoomRatio), zoomRatio); } /** * Converts the zoom ratio to the progress value value. */ private int convertZoomRatioToProgress(float zoomRatio) { return (int) ((zoomRatio - mMinZoomRatio) / (mMaxZoomRatio - mMinZoomRatio) * MAX_ZOOM_PROGRESS); } /** * Resets the toggle UI auto-show runnable. */ private void resetToggleUiAutoShowRunnable() { removeToggleUiAutoShowRunnable(); postDelayed(mToggleUiAutoShowRunnable, mToggleAutoShowDurationMs); } /** * Removes the toggle UI auto-show runnable. */ private void removeToggleUiAutoShowRunnable() { removeCallbacks(mToggleUiAutoShowRunnable); } /** * The listener to monitor the value change of the zoom controller. */ public interface OnZoomRatioUpdatedListener { /** * Invoked when the zoom ratio value is changed. * * @param value the updated zoom ratio value. */ void onValueChanged(float value); } }