如何在Android中制作垂直搜索杆?

如何在Android中制作垂直搜索杆?,android,seekbar,Android,Seekbar,SeekBar是否可以垂直?我不太擅长UI设计,因此如何使SeekBar更漂亮,请给我一些模板和示例。注意,在我看来,如果更改宽度,拇指宽度将不会正确更改。 我没有花时间把它修好,我只是为我的案子修好了。这就是我所做的。 无法确定如何联系原始创建者 public void setThumb(Drawable thumb) { if (thumb != null) { thumb.setCallback(this); // Assuming the thu

SeekBar
是否可以垂直?我不太擅长UI设计,因此如何使
SeekBar
更漂亮,请给我一些模板和示例。

注意,在我看来,如果更改宽度,拇指宽度将不会正确更改。 我没有花时间把它修好,我只是为我的案子修好了。这就是我所做的。 无法确定如何联系原始创建者

public void setThumb(Drawable thumb) {
    if (thumb != null) {
        thumb.setCallback(this);

        // Assuming the thumb drawable is symmetric, set the thumb offset
        // such that the thumb will hang halfway off either edge of the
        // progress bar.
        //This was orginally divided by 2, seems you have to adjust here when you adjust width.
        mThumbOffset = (int)thumb.getIntrinsicHeight();
    }

这是一个非常好的垂直seekbar实现。 看一看

下面是我自己基于

尝试:


  • 对于API 11和更高版本,可以使用seekbar的XML属性(android:rotation=“270”)来实现垂直效果

    <SeekBar
    android:id="@+id/seekBar1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:rotation="270"/>
    
    
    
  • 对于较旧的API级别(例如API10),仅使用Selva的答案:


  • 我使用了塞尔瓦的解决方案,但有两种问题:

    • OnSeekbarchaneListener未正常工作
    • 以编程方式设置进度无法正常工作
    我解决了这两个问题。您可以在以下位置找到解决方案(在我自己的项目包中)

    工作示例

    import android.content.Context;
    import android.graphics.Canvas;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    
    public class VerticalSeekBar extends SeekBar {
    
        public VerticalSeekBar(Context context) {
            super(context);
        }
    
        public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public VerticalSeekBar(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(h, w, oldh, oldw);
        }
    
       @Override
       public synchronized void setProgress(int progress)  // it is necessary for calling setProgress on click of a button
       {
        super.setProgress(progress);
        onSizeChanged(getWidth(), getHeight(), 0, 0); 
       }
        @Override
        protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(heightMeasureSpec, widthMeasureSpec);
            setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
        }
    
        protected void onDraw(Canvas c) {
            c.rotate(-90);
            c.translate(-getHeight(), 0);
    
            super.onDraw(c);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isEnabled()) {
                return false;
            }
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                case MotionEvent.ACTION_UP:
                    setProgress(getMax() - (int) (getMax() * event.getY() / getHeight()));
                    onSizeChanged(getWidth(), getHeight(), 0, 0);
                    break;
    
                case MotionEvent.ACTION_CANCEL:
                    break;
            }
            return true;
        }
    }
    
    在那里,粘贴代码并保存它。现在在XML布局中使用它:

    <android.widget.VerticalSeekBar
      android:id="@+id/seekBar1"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      />
    
    
    

    确保创建一个包
    android.widget
    ,并在此包下创建
    VerticalSeekBar.java

    当使用编辑文本移动拇指时,垂直Seekbar setProgress可能无法工作。以下代码可以提供帮助:

        @Override
    public synchronized void setProgress(int progress) {
        super.setProgress(progress);
        updateThumb();
    }
    
    private void updateThumb() {
        onSizeChanged(getWidth(), getHeight(), 0, 0);
    }
    
    此代码段位于以下位置:

    我们使用
    android:rotation=“270”
    制作了一个垂直搜索栏:

    
    
    相机曝光补偿屏幕截图:


    这对我来说很有效,只要把它放到你想要的任何布局中就行了

    <FrameLayout
        android:layout_width="32dp"
        android:layout_height="192dp">
    
        <SeekBar
            android:layout_width="192dp"
            android:layout_height="32dp"
            android:layout_gravity="center"
            android:rotation="270" />
    
    </FrameLayout>
    

    入门

    将这些行添加到build.gradle

    dependencies {
        compile 'com.h6ah4i.android.widget.verticalseekbar:verticalseekbar:0.7.2'
    }
    
    用法

    Java代码

    public class TestVerticalSeekbar extends AppCompatActivity {
        private SeekBar volumeControl = null;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test_vertical_seekbar);
    
            volumeControl = (SeekBar) findViewById(R.id.mySeekBar);
    
            volumeControl.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                int progressChanged = 0;
    
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    progressChanged = progress;
                }
    
                public void onStartTrackingTouch(SeekBar seekBar) {
                    // TODO Auto-generated method stub
                }
    
                public void onStopTrackingTouch(SeekBar seekBar) {
                    Toast.makeText(getApplicationContext(), "seek bar progress:" + progressChanged,
                            Toast.LENGTH_SHORT).show();
                }
            });
        }
    
    }
    
    布局XML

    <!-- This library requires pair of the VerticalSeekBar and VerticalSeekBarWrapper classes -->
    <com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper
        android:layout_width="wrap_content"
        android:layout_height="150dp">
        <com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar
            android:id="@+id/mySeekBar"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:max="100"
            android:progress="0"
            android:splitTrack="false"
            app:seekBarRotation="CW90" /> <!-- Rotation: CW90 or CW270 -->
    </com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper>
    
    
    
    注意:
    android:splitTrack=“false”
    是android N+所必需的。

    试试这个

    import android.content.Context;
    import android.graphics.Canvas;
    import android.support.annotation.NonNull;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.widget.SeekBar;
    
    /**
     * Implementation of an easy vertical SeekBar, based on the normal SeekBar.
     */
    public class VerticalSeekBar extends SeekBar {
        /**
         * The angle by which the SeekBar view should be rotated.
         */
        private static final int ROTATION_ANGLE = -90;
    
        /**
         * A change listener registrating start and stop of tracking. Need an own listener because the listener in SeekBar
         * is private.
         */
        private OnSeekBarChangeListener mOnSeekBarChangeListener;
    
        /**
         * Standard constructor to be implemented for all views.
         *
         * @param context The Context the view is running in, through which it can access the current theme, resources, etc.
         * @see android.view.View#View(Context)
         */
        public VerticalSeekBar(final Context context) {
            super(context);
        }
    
        /**
         * Standard constructor to be implemented for all views.
         *
         * @param context The Context the view is running in, through which it can access the current theme, resources, etc.
         * @param attrs   The attributes of the XML tag that is inflating the view.
         * @see android.view.View#View(Context, AttributeSet)
         */
        public VerticalSeekBar(final Context context, final AttributeSet attrs) {
            super(context, attrs);
        }
    
        /**
         * Standard constructor to be implemented for all views.
         *
         * @param context  The Context the view is running in, through which it can access the current theme, resources, etc.
         * @param attrs    The attributes of the XML tag that is inflating the view.
         * @param defStyle An attribute in the current theme that contains a reference to a style resource that supplies default
         *                 values for the view. Can be 0 to not look for defaults.
         * @see android.view.View#View(Context, AttributeSet, int)
         */
        public VerticalSeekBar(final Context context, final AttributeSet attrs, final int defStyle) {
            super(context, attrs, defStyle);
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        protected final void onSizeChanged(final int width, final int height, final int oldWidth, final int oldHeight) {
            super.onSizeChanged(height, width, oldHeight, oldWidth);
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        protected final synchronized void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
            super.onMeasure(heightMeasureSpec, widthMeasureSpec);
            setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        protected final void onDraw(@NonNull final Canvas c) {
            c.rotate(ROTATION_ANGLE);
            c.translate(-getHeight(), 0);
    
            super.onDraw(c);
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        public final void setOnSeekBarChangeListener(final OnSeekBarChangeListener listener) {
            // Do not use super for the listener, as this would not set the fromUser flag properly
            mOnSeekBarChangeListener = listener;
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        public final boolean onTouchEvent(@NonNull final MotionEvent event) {
            if (!isEnabled()) {
                return false;
            }
    
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                setProgressInternally(getMax() - (int) (getMax() * event.getY() / getHeight()), true);
                if (mOnSeekBarChangeListener != null) {
                    mOnSeekBarChangeListener.onStartTrackingTouch(this);
                }
                break;
    
            case MotionEvent.ACTION_MOVE:
                setProgressInternally(getMax() - (int) (getMax() * event.getY() / getHeight()), true);
                break;
    
            case MotionEvent.ACTION_UP:
                setProgressInternally(getMax() - (int) (getMax() * event.getY() / getHeight()), true);
                if (mOnSeekBarChangeListener != null) {
                    mOnSeekBarChangeListener.onStopTrackingTouch(this);
                }
                break;
    
            case MotionEvent.ACTION_CANCEL:
                if (mOnSeekBarChangeListener != null) {
                    mOnSeekBarChangeListener.onStopTrackingTouch(this);
                }
                break;
    
            default:
                break;
            }
    
            return true;
        }
    
        /**
         * Set the progress by the user. (Unfortunately, Seekbar.setProgressInternally(int, boolean) is not accessible.)
         *
         * @param progress the progress.
         * @param fromUser flag indicating if the change was done by the user.
         */
        public final void setProgressInternally(final int progress, final boolean fromUser) {
            if (progress != getProgress()) {
                super.setProgress(progress);
                if (mOnSeekBarChangeListener != null) {
                    mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);
                }
            }
            onSizeChanged(getWidth(), getHeight(), 0, 0);
        }
    
        /*
         * (non-Javadoc) ${see_to_overridden}
         */
        @Override
        public final void setProgress(final int progress) {
            setProgressInternally(progress, false);
        }
    }
    

    在我的例子中,我使用了一个普通的seekBar,只是翻转了布局

    seekbark_layout.xml-我的布局包含seekbar,我们需要使其垂直

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rootView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"/>
    
    </RelativeLayout>
    
    因此,我们有必要的垂直搜索杆:

    将其包装在框架布局中,这样就不会出现大小问题。


    我尝试了许多不同的方法,但对我有效的方法是。 在FrameLayout中使用Seekbar

    <FrameLayout
        android:id="@+id/VolumeLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/MuteButton"
        android:layout_below="@id/volumeText"
        android:layout_centerInParent="true">
            <SeekBar
            android:id="@+id/volume"
            android:layout_width="500dp"
            android:layout_height="60dp"
            android:layout_gravity="center"
            android:progress="50"
            android:secondaryProgress="40"
            android:progressDrawable="@drawable/seekbar_volume"
            android:secondaryProgressTint="@color/tint_neutral"
            android:thumbTint="@color/tint_neutral"
        />
    
    
    
    和代码

    在Seekbar上设置预绘制回调,您可以在其中更改Seekbar的宽度和高度 我在c#中完成了这一部分,所以我使用的代码是

                var volumeSlider = view.FindViewById<SeekBar>(Resource.Id.home_link_volume);
    
                var volumeFrameLayout = view.FindViewById<FrameLayout>(Resource.Id.linkVolumeFrameLayout);
    
                void OnPreDrawVolume(object sender, ViewTreeObserver.PreDrawEventArgs e)
                {
                    volumeSlider.ViewTreeObserver.PreDraw -= OnPreDrawVolume;
                    var h = volumeFrameLayout.Height;
                    volumeSlider.Rotation = 270.0f;
                    volumeSlider.LayoutParameters.Width = h;
                    volumeSlider.RequestLayout();
                }
    
                volumeSlider.ViewTreeObserver.PreDraw += OnPreDrawVolume;
    
    var volumeSlider=view.findviewbyd(Resource.Id.home\u link\u volume);
    var volumeFrameLayout=view.FindViewById(Resource.Id.linkVolumeFrameLayout);
    void OnPreDrawVolume(对象发送方,ViewTreeObserver.PreDrawEventArgs e)
    {
    volumeSlider.ViewTreeObserver.PreDraw-=OnPreDrawVolume;
    var h=volumeFrameLayout.高度;
    体积滑块旋转=270.0f;
    volumeSlider.LayoutParameters.Width=h;
    volumeSlider.RequestLayout();
    }
    volumeSlider.ViewTreeObserver.PreDraw+=OnPreDrawVolume;
    
    在这里,我将监听器添加到PreDraw事件中,当它被触发时,我移除PreDraw,这样它就不会进入无限循环

    因此,当执行预绘制时,我获取FrameLayout的高度并将其分配给Seekbar。并将seekbar的旋转设置为270。 因为我的seekbar在框架内布局,其重心设置为中心。我不需要担心翻译。因为Seekbar始终位于框架布局的中间


    我删除EventHandler的原因是seekbar.RequestLayout();将使此事件再次执行。

    您可以自己执行-现在非常困难。 以下是我的项目中的一个示例:

    让我们从设置(attrs.xml)开始

    
    
    以下是几个实用程序函数:

    fun <T: Comparable<T>>T.fitInRange(range: Range<T>): T =
        when {
            this < range.lower -> range.lower
            this > range.upper -> range.upper
            else -> this
        }
    
    fun Float.reduceToRange(rangeFrom: Range<Float>, rangeTo: Range<Float>): Float =
        when {
            this == rangeFrom.lower -> rangeTo.lower
            this == rangeFrom.upper -> rangeTo.upper
            else -> {
                val placeInRange = (this - rangeFrom.lower) / (rangeFrom.upper - rangeFrom.lower)
                ((rangeTo.upper - rangeTo.lower) * placeInRange) + rangeTo.lower
            }
        }
    
    fun T.fitInRange(范围:范围):T=
    什么时候{
    这range.lower
    此>range.upper->range.upper
    否则->这个
    }
    乐趣浮动。减少范围(范围从:范围,范围到:范围):浮动=
    什么时候{
    此==rangeFrom.lower->rangeTo.lower
    此==rangeFrom.upper->rangeTo.upper
    其他->{
    val placeInRange=(this-rangeFrom.lower)/(rangeFrom.upper-rangeFrom.lower)
    ((rangeTo.upper-rangeTo.lower)*placeInRange)+rangeTo.lower
    }
    }
    
    最后,但并非最不重要的是,垂直搜索栏类:

    class ExpositionBar
    @JvmOverloads
    constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    
        private val drawingRect = RectF(0f, 0f, 0f, 0f)
        private val drawingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    
        private val strokeWidth: Float
    
        @ColorInt
        private val strokeColor: Int
        @ColorInt
        private val buttonFillColor: Int
        @ColorInt
        private val buttonFillColorPressed: Int
    
        private val icon: VectorDrawable
    
        private val valuesRange: Range<Float>
    
        private var centerX = 0f
        private var minY = 0f
        private var maxY = 0f
    
        private var buttonCenterY = 0f
        private var buttonRadiusExt = 0f
        private var buttonRadiusInt = 0f
        private var buttonMinY = 0f
        private var buttonMaxY = 0f
        private var buttonCenterBoundsRange = Range(0f, 0f)
    
        private var iconTranslationX = 0f
        private var iconTranslationY = 0f
    
        private var isInDragMode = false
    
        private var onValueChangeListener: ((Float) -> Unit)? = null
    
        private var oldOutputValue = Float.MIN_VALUE
    
        init {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpositionBar)
    
            icon =  typedArray.getDrawable(R.styleable.ExpositionBar_button_icon) as VectorDrawable
            val iconSize = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_button_icon_size, 0)
            icon.setBounds(0, 0, iconSize, iconSize)
    
            strokeWidth = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_stroke_width, 0).toFloat()
            drawingPaint.strokeWidth = strokeWidth
    
            strokeColor = typedArray.getColor(R.styleable.ExpositionBar_stroke_color, Color.WHITE)
            buttonFillColor = typedArray.getColor(R.styleable.ExpositionBar_button_color, Color.BLACK)
            buttonFillColorPressed = typedArray.getColor(R.styleable.ExpositionBar_button_color_pressed, Color.BLUE)
    
            val minValue = typedArray.getFloat(R.styleable.ExpositionBar_min_value, 0f)
            val maxValue = typedArray.getFloat(R.styleable.ExpositionBar_max_value, 0f)
            valuesRange = Range(minValue, maxValue)
    
            typedArray.recycle()
        }
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
    
            drawingRect.right = width.toFloat()
            drawingRect.bottom = height.toFloat()
    
            buttonCenterY = drawingRect.centerY()
    
            recalculateDrawingValues()
        }
    
        override fun onDraw(canvas: Canvas) {
            drawingPaint.color = strokeColor
            drawingPaint.style = Paint.Style.STROKE
    
            // Draw the center line
            canvas.drawLine(centerX, minY, centerX, buttonMinY, drawingPaint)
            canvas.drawLine(centerX, buttonMaxY, centerX, maxY, drawingPaint)
    
            // Draw the button
            canvas.drawCircle(centerX, buttonCenterY, buttonRadiusExt, drawingPaint)
            drawingPaint.style = Paint.Style.FILL
            drawingPaint.color = if(isInDragMode) buttonFillColorPressed else buttonFillColor
            canvas.drawCircle(centerX, buttonCenterY, buttonRadiusInt, drawingPaint)
    
            // Draw button icon
            canvas.translate(iconTranslationX, iconTranslationY)
            icon.draw(canvas)
            canvas.translate(-iconTranslationX, -iconTranslationY)
        }
    
        @SuppressLint("ClickableViewAccessibility")
        override fun onTouchEvent(event: MotionEvent): Boolean {
            if(!isEnabled) {
                return false
            }
    
            when(event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    if(isButtonHit(event.y)){
                        isInDragMode = true
                        invalidate()
                    }
                }
                MotionEvent.ACTION_MOVE -> {
                    if(isInDragMode) {
                        buttonCenterY = event.y.fitInRange(buttonCenterBoundsRange)
                        recalculateDrawingValues()
                        invalidate()
    
                        val outputValue = buttonCenterY.reduceToRange(buttonCenterBoundsRange, valuesRange)
                        if (outputValue != oldOutputValue) {
                            onValueChangeListener?.invoke(outputValue)
                            oldOutputValue = outputValue
                        }
                    }
                }
                MotionEvent.ACTION_UP,
                MotionEvent.ACTION_CANCEL -> {
                    isInDragMode = false
                    invalidate()
                }
            }
            return true
        }
    
        fun setOnValueChangeListener(listener: ((Float) -> Unit)?) {
            onValueChangeListener = listener
        }
    
        private fun recalculateDrawingValues() {
            centerX = drawingRect.left + drawingRect.width()/2
            minY = drawingRect.top
            maxY = drawingRect.bottom
    
            buttonRadiusExt = drawingRect.width() / 2 - strokeWidth / 2
            buttonRadiusInt = buttonRadiusExt - strokeWidth / 2
            buttonMinY = buttonCenterY - buttonRadiusExt
            buttonMaxY = buttonCenterY + buttonRadiusExt
    
            val buttonCenterMinY = minY + buttonRadiusExt + strokeWidth / 2
            val buttonCenterMaxY = maxY - buttonRadiusExt - strokeWidth / 2
            buttonCenterBoundsRange = Range(buttonCenterMinY, buttonCenterMaxY)
    
            iconTranslationX = centerX - icon.bounds.width() / 2
            iconTranslationY = buttonCenterY - icon.bounds.height() / 2
        }
    
        private fun isButtonHit(y: Float): Boolean {
            return y >= buttonMinY && y <= buttonMaxY
        }
    }
    
    class ExpositionBar
    @JVM重载
    建造师(
    上下文:上下文,
    属性集?=null,
    defStyleAttr:Int=0
    ):视图(上下文、属性、defStyleAttr){
    private val drawingRect=RectF(0f,0f,0f,0f)
    私有val drawingPaint=Paint(Paint.ANTI_别名_标志)
    private val strokeWidth:Float
    @着色剂
    private val strokeColor:Int
    @着色剂
    私有val按钮填充颜色:Int
    @着色剂
    私有val按钮填充颜色按下:Int
    私有val图标:可矢量绘制
    私有值范围:范围
    专用变量centerX=0f
    私有变量minY=0f
    专用变量maxY=0f
    私有变量按钮中心=0f
    专用变量按钮radiusext=0f
    专用变量按钮radiusint=0f
    私有变量按钮最小值=0f
    私有变量ButtonMax=0f
    私有变量按钮InterboundsRange=范围(0f,0f)
    专用变量iconTranslationX=0f
    私有变量iconTranslationY=0f
    私有变量isInDragMode=false
    私有变量onValueChangeListener:((浮点)->单位)?=null
    私有变量oldOutputValue=Float.M
    
    import android.os.Bundle
    import android.support.v7.app.AppCompatActivity
    import android.widget.RelativeLayout
    import kotlinx.android.synthetic.main.seekbar_layout.*
    
    
    class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        rootView.post {
            val w = rootView.width
            val h = rootView.height
    
            rootView.rotation = 270.0f
            rootView.translationX = ((w - h) / 2).toFloat()
            rootView.translationY = ((h - w) / 2).toFloat()
    
            val lp = rootView.layoutParams as RelativeLayout.LayoutParams
            lp.height = w
            lp.width = h
            rootView.requestLayout()
        }
    }
    }
    
      <FrameLayout
                    android:layout_width="@dimen/_20dp"
                    android:layout_marginStart="@dimen/_15dp"
                    android:layout_marginEnd="@dimen/_15dp"
                    android:layout_height="match_parent"
                    android:orientation="vertical">
    
                    <SeekBar
                        android:layout_width="150dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:rotation="270" />
      </FrameLayout>
    
    <FrameLayout
        android:id="@+id/VolumeLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/MuteButton"
        android:layout_below="@id/volumeText"
        android:layout_centerInParent="true">
            <SeekBar
            android:id="@+id/volume"
            android:layout_width="500dp"
            android:layout_height="60dp"
            android:layout_gravity="center"
            android:progress="50"
            android:secondaryProgress="40"
            android:progressDrawable="@drawable/seekbar_volume"
            android:secondaryProgressTint="@color/tint_neutral"
            android:thumbTint="@color/tint_neutral"
        />
    
                var volumeSlider = view.FindViewById<SeekBar>(Resource.Id.home_link_volume);
    
                var volumeFrameLayout = view.FindViewById<FrameLayout>(Resource.Id.linkVolumeFrameLayout);
    
                void OnPreDrawVolume(object sender, ViewTreeObserver.PreDrawEventArgs e)
                {
                    volumeSlider.ViewTreeObserver.PreDraw -= OnPreDrawVolume;
                    var h = volumeFrameLayout.Height;
                    volumeSlider.Rotation = 270.0f;
                    volumeSlider.LayoutParameters.Width = h;
                    volumeSlider.RequestLayout();
                }
    
                volumeSlider.ViewTreeObserver.PreDraw += OnPreDrawVolume;
    
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="ExpositionBar">
            <attr name="button_icon" format="reference" />
            <attr name="button_icon_size" format="dimension" />
    
            <attr name="stroke_width" format="dimension" />
    
            <attr name="stroke_color" format="color" />
            <attr name="button_color" format="color" />
            <attr name="button_color_pressed" format="color" />
    
            <attr name="min_value" format="float" />
            <attr name="max_value" format="float" />
        </declare-styleable>
    </resources>
    
    fun <T: Comparable<T>>T.fitInRange(range: Range<T>): T =
        when {
            this < range.lower -> range.lower
            this > range.upper -> range.upper
            else -> this
        }
    
    fun Float.reduceToRange(rangeFrom: Range<Float>, rangeTo: Range<Float>): Float =
        when {
            this == rangeFrom.lower -> rangeTo.lower
            this == rangeFrom.upper -> rangeTo.upper
            else -> {
                val placeInRange = (this - rangeFrom.lower) / (rangeFrom.upper - rangeFrom.lower)
                ((rangeTo.upper - rangeTo.lower) * placeInRange) + rangeTo.lower
            }
        }
    
    class ExpositionBar
    @JvmOverloads
    constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    
        private val drawingRect = RectF(0f, 0f, 0f, 0f)
        private val drawingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    
        private val strokeWidth: Float
    
        @ColorInt
        private val strokeColor: Int
        @ColorInt
        private val buttonFillColor: Int
        @ColorInt
        private val buttonFillColorPressed: Int
    
        private val icon: VectorDrawable
    
        private val valuesRange: Range<Float>
    
        private var centerX = 0f
        private var minY = 0f
        private var maxY = 0f
    
        private var buttonCenterY = 0f
        private var buttonRadiusExt = 0f
        private var buttonRadiusInt = 0f
        private var buttonMinY = 0f
        private var buttonMaxY = 0f
        private var buttonCenterBoundsRange = Range(0f, 0f)
    
        private var iconTranslationX = 0f
        private var iconTranslationY = 0f
    
        private var isInDragMode = false
    
        private var onValueChangeListener: ((Float) -> Unit)? = null
    
        private var oldOutputValue = Float.MIN_VALUE
    
        init {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpositionBar)
    
            icon =  typedArray.getDrawable(R.styleable.ExpositionBar_button_icon) as VectorDrawable
            val iconSize = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_button_icon_size, 0)
            icon.setBounds(0, 0, iconSize, iconSize)
    
            strokeWidth = typedArray.getDimensionPixelSize(R.styleable.ExpositionBar_stroke_width, 0).toFloat()
            drawingPaint.strokeWidth = strokeWidth
    
            strokeColor = typedArray.getColor(R.styleable.ExpositionBar_stroke_color, Color.WHITE)
            buttonFillColor = typedArray.getColor(R.styleable.ExpositionBar_button_color, Color.BLACK)
            buttonFillColorPressed = typedArray.getColor(R.styleable.ExpositionBar_button_color_pressed, Color.BLUE)
    
            val minValue = typedArray.getFloat(R.styleable.ExpositionBar_min_value, 0f)
            val maxValue = typedArray.getFloat(R.styleable.ExpositionBar_max_value, 0f)
            valuesRange = Range(minValue, maxValue)
    
            typedArray.recycle()
        }
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
    
            drawingRect.right = width.toFloat()
            drawingRect.bottom = height.toFloat()
    
            buttonCenterY = drawingRect.centerY()
    
            recalculateDrawingValues()
        }
    
        override fun onDraw(canvas: Canvas) {
            drawingPaint.color = strokeColor
            drawingPaint.style = Paint.Style.STROKE
    
            // Draw the center line
            canvas.drawLine(centerX, minY, centerX, buttonMinY, drawingPaint)
            canvas.drawLine(centerX, buttonMaxY, centerX, maxY, drawingPaint)
    
            // Draw the button
            canvas.drawCircle(centerX, buttonCenterY, buttonRadiusExt, drawingPaint)
            drawingPaint.style = Paint.Style.FILL
            drawingPaint.color = if(isInDragMode) buttonFillColorPressed else buttonFillColor
            canvas.drawCircle(centerX, buttonCenterY, buttonRadiusInt, drawingPaint)
    
            // Draw button icon
            canvas.translate(iconTranslationX, iconTranslationY)
            icon.draw(canvas)
            canvas.translate(-iconTranslationX, -iconTranslationY)
        }
    
        @SuppressLint("ClickableViewAccessibility")
        override fun onTouchEvent(event: MotionEvent): Boolean {
            if(!isEnabled) {
                return false
            }
    
            when(event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    if(isButtonHit(event.y)){
                        isInDragMode = true
                        invalidate()
                    }
                }
                MotionEvent.ACTION_MOVE -> {
                    if(isInDragMode) {
                        buttonCenterY = event.y.fitInRange(buttonCenterBoundsRange)
                        recalculateDrawingValues()
                        invalidate()
    
                        val outputValue = buttonCenterY.reduceToRange(buttonCenterBoundsRange, valuesRange)
                        if (outputValue != oldOutputValue) {
                            onValueChangeListener?.invoke(outputValue)
                            oldOutputValue = outputValue
                        }
                    }
                }
                MotionEvent.ACTION_UP,
                MotionEvent.ACTION_CANCEL -> {
                    isInDragMode = false
                    invalidate()
                }
            }
            return true
        }
    
        fun setOnValueChangeListener(listener: ((Float) -> Unit)?) {
            onValueChangeListener = listener
        }
    
        private fun recalculateDrawingValues() {
            centerX = drawingRect.left + drawingRect.width()/2
            minY = drawingRect.top
            maxY = drawingRect.bottom
    
            buttonRadiusExt = drawingRect.width() / 2 - strokeWidth / 2
            buttonRadiusInt = buttonRadiusExt - strokeWidth / 2
            buttonMinY = buttonCenterY - buttonRadiusExt
            buttonMaxY = buttonCenterY + buttonRadiusExt
    
            val buttonCenterMinY = minY + buttonRadiusExt + strokeWidth / 2
            val buttonCenterMaxY = maxY - buttonRadiusExt - strokeWidth / 2
            buttonCenterBoundsRange = Range(buttonCenterMinY, buttonCenterMaxY)
    
            iconTranslationX = centerX - icon.bounds.width() / 2
            iconTranslationY = buttonCenterY - icon.bounds.height() / 2
        }
    
        private fun isButtonHit(y: Float): Boolean {
            return y >= buttonMinY && y <= buttonMaxY
        }
    }
    
    <com.shevelev.wizard_camera.main_activity.view.widgets.ExpositionBar
        android:id="@+id/expositionBar"
        android:layout_width="@dimen/mainButtonSize"
        android:layout_height="300dp"
        android:layout_gravity="end|center_vertical"
    
        android:layout_marginEnd="@dimen/marginNormal"
        android:layout_marginBottom="26dp"
    
        app:button_icon = "@drawable/ic_brightness"
        app:button_icon_size = "@dimen/toolButtonIconSize"
        app:stroke_width = "@dimen/strokeWidthNormal"
        app:stroke_color = "@color/mainButtonsForeground"
        app:button_color = "@color/mainButtonsBackground"
        app:button_color_pressed = "@color/mainButtonsBackgroundPressed"
        app:min_value="-100"
        app:max_value="100"
    />
    
    <com.github.rongi.rotate_layout.layout.RotateLayout
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:angle="-90"
        >
    
            <androidx.appcompat.widget.AppCompatSeekBar
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
            />
    
    </com.github.rongi.rotate_layout.layout.RotateLayout>