创建Kotlin Android旋钮

本文介绍了如何为Android应用程序设计和实现一个旋钮控件,以模仿物理设备的旋钮功能。作者通过创建一个自定义的 RotaryKnobView 类,实现了在XML中创建视图、检测用户手势、计算旋转角度等功能,使得旋钮控件既节省屏幕空间又易于操作,为用户提供了一种独特的交互体验。
摘要由CSDN通过智能技术生成

Recently I created an Android Metronome app. My initial implementation used a SeekBar to control BPM (Beats per Minute) — the rate at which the metronome ticks. However, as the project progressed, I wanted to make it resemble a physical digital unit, as used by many musicians in the real physical world.

最近,我创建了一个Android Metronome应用程序 。 我最初的实现使用SeekBar来控制BPM(每分钟节拍数)-节拍器滴答声的速率。 但是,随着项目的进行,我想使其像一个物理数字单元,就像现实世界中许多音乐家所使用的那样。

Physical units do not have a “SeekBar View”, and I wanted to mimic the rotary knob an actual unit might have. Rotary knobs are very useful UI controls. They are much like a slider or SeekBar, usable in many situations. Here are some of their advantages:

物理单位没有“ SeekBar视图”,我想模拟实际单位可能具有的旋钮。 旋钮是非常有用的UI控件。 它们很像滑块或SeekBar,可在许多情况下使用。 以下是它们的一些优点:

  • They consume very little real estate in your app

    他们在您的应用中消耗的房地产很少
  • They can be used to control continuous or discrete ranges of values

    它们可用于控制值的连续或离散范围
  • They are immediately recognizable by users from real world applications

    用户可以立即从实际应用程序中识别它们
  • They are not standard Android controls and thus bestow your app with a unique “custom” feel

    它们不是标准的Android控件,因此赋予您的应用程序以独特的“自定义”感觉

While a few open source knob libraries for Android exist, I didn’t find quite what I was looking for in any of them. Many were an overkill for my modest needs, with functionality such as setting background images or handling taps for two or more mode operations, etc. Some did not have the customizability I wanted to fit my project and came with their own knob image.Others assumed a discrete range of values or positions.Many of them seemed much more complex than needed.

尽管存在一些适用于Android的开放源代码旋钮库,但是我在任何一个库中都找不到我想要的东西。 许多功能对于满足我的适度需求来说是一个过大的杀伤力,其功能包括设置背景图像或处理两个或多个模式操作的水龙头等。一些功能没有我想要适合我的项目的可定制性,并且带有自己的旋钮图像。离散的值或位置范围。其中许多似乎比所需的复杂得多。

So I decided to design one myself — which turned into a fun little project in itself.

因此,我决定自己设计一个-本身变成了一个有趣的小项目。

In this article I’ll discuss how I built it.

在本文中,我将讨论如何构建它。

So let’s see how we can create a rotary knob.

因此,让我们看看如何创建一个旋钮。

设计旋钮 (Designing a knob)

The first step was to create the graphic for the knob itself. I’m no designer by any means, but it occurred to me that the key to creating a sense of “depth” and movement in a knob control would be to use an off center radial gradient, in order to create the illusion of a depressed surface and light reflection.

第一步是为旋钮本身创建图形。 我绝对不是设计师,但是我想到在旋钮控件中产生“深度”感和运动感的关键是使用偏心的径向渐变,以产生压抑的幻觉。表面和光反射。

I used Sketch to draw the knob, then exported it to svg, and imported back into android studio as a drawable. You can find the knob drawable in the github project at the link at the bottom of this article.

我使用Sketch绘制了旋钮,然后将其导出到svg,并作为可绘制对象重新导入了android studio。 您可以在github项目中位于本文底部的链接中找到可绘制的旋钮。

Image for post

在xml中创建视图 (Creating the view in xml)

The first step in creating the View is creating a layout xml file in the res/layout folder.

创建视图的第一步是在res / layout文件夹中创建一个xml布局文件。

The view can be completely created in code, but a good reusable View in Android should be created in xml.Notice the <merge> tag — we use that since we’ll be extending an existing Android Layout class and this layout will be the inner structure of that layout.We’ll use an ImageView for the knob, which we’ll rotate as the user moves it.

该视图可以完全用代码创建,但是在Android中应使用xml创建一个可重用的视图。注意<merge>标记-我们将使用它,因为我们将扩展现有的Android Layout类,并且此布局将是内部布局的结构。我们将为旋钮使用ImageView,并随着用户移动它进行旋转。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">


    <ImageView
        android:id="@+id/knobImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
</merge>

To make the knob configurable by xml, we create attributes for the range of values the knob will return, as well as for the drawable it will use for visuals.We’ll create an attrs.xml file under res/values.

为了使该旋钮可通过xml配置,我们为该旋钮将返回的值的范围以及将用于视觉效果的drawable创建属性。我们将在res / values下创建一个attrs.xml文件。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RotaryKnobView">
        <attr name="minValue" format="integer" />
        <attr name="maxValue" format="integer" />
        <attr name="initialValue" format="integer" />
        <attr name="knobDrawable" format="reference" />
    </declare-styleable>
</resources>

Next, Create a new Kotlin class file, RotaryKnobView that extends RelativeLayout and implements the interface GestureDetector.OnGestureListener.

接下来,创建一个新的Kotlin类文件RotaryKnobView,该文件扩展了RelativeLayout并实现了接口GestureDetector.OnGestureListener。

We’ll use RelativeLayout as the parent container for the control, and implement OnGestureListener to handle the knob’s movement gestures.

我们将使用RelativeLayout作为控件的父容器,并实现OnGestureListener来处理旋钮的移动手势。

@JvmOverloads is just a shortcut to overriding all three flavors of the View constructor.

@JvmOverloads只是覆盖View构造函数的所有三种形式的快捷方式。

We’ll initialize some default values and define class members.

我们将初始化一些默认值并定义类成员。

class RotaryKnobView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr), GestureDetector.OnGestureListener {
    private val gestureDetector: GestureDetectorCompat
    private var maxValue = 99
    private var minValue = 0
    var listener: RotaryKnobListener? = null
    var value = 50
    private var knobDrawable: Drawable? = null
    private var divider = 300f / (maxValue - minValue)

A note about the divider variable — I wanted the knob to have a starting and end positions, rather than being able to rotate indefinitely, much like a volume knob on a stereo system. I set the start and end points at -150 and 150 degrees respectively. So the effective motion for the knob is only 300 degrees. We’ll use the divider to distribute the range of values we want our knob to return upon these available 300 degrees — so that we can calculate the actual value based on the knob’s position angle.

关于分频器变量的注释-我希望旋钮具有开始和结束位置,而不是能够无限旋转,就像立体声系统上的音量旋钮一样。 我将起点和终点分别设置为-150度和150度。 因此,旋钮的有效运动仅为300度。 我们将使用除法器来分配希望旋钮在这些可用的300度上返回的值的范围-以便我们可以根据旋钮的位置角度来计算实际值。

Next, we initialize the component:Inflate the layout. Read the attributes into variables. Update the divider (to support the passed in minimum and maximum values. Set the image.

接下来,我们初始化组件:填充布局。 将属性读入变量。 更新除法器(以支持传入的最小值和最大值。设置图像。

init {
        this.maxValue = maxValue + 1


        LayoutInflater.from(context)
            .inflate(R.layout.rotary_knob_view, this, true)


        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.RotaryKnobView,
            0,
            0
        ).apply {
            try {
                minValue = getInt(R.styleable.RotaryKnobView_minValue, 0)
                maxValue = getInt(R.styleable.RotaryKnobView_maxValue, 100) + 1
                divider = 300f / (maxValue - minValue)
                value = getInt(R.styleable.RotaryKnobView_initialValue, 50)
                knobDrawable = getDrawable(R.styleable.RotaryKnobView_knobDrawable)
                knobImageView.setImageDrawable(knobDrawable)
            } finally {
                recycle()
            }
        }
        gestureDetector = GestureDetectorCompat(context, this)
    }

The class will not compile just yet, as we need to implement OnGestureListener’s functions. Lets handle that now.

由于我们需要实现OnGestureListener的功能,因此该类尚未编译。 让我们现在处理。

检测用户手势 (Detecting user gestures)

The OnGestureListener interface requires that we implement six functions:onScroll, onTouchEvent, onDown, onSingleTapUp, onFling, onLongPress, onShowPress.

OnGestureListener接口要求我们实现六个功能:onScroll,onTouchEvent,onDown,onSingleTapUp,onFling,onLongPress,onShowPress。

Of these, we need to consume (return true) on onDown and onTouchEvent, and implement the movement login in onScroll.

其中,我们需要在onDown和onTouchEvent上使用(返回true),并在onScroll中实现移动登录。

override fun onTouchEvent(event: MotionEvent): Boolean {
        return if (gestureDetector.onTouchEvent(event))
            true
        else
            super.onTouchEvent(event)
    }


    override fun onDown(event: MotionEvent): Boolean {
        return true
    }


    override fun onSingleTapUp(e: MotionEvent): Boolean {
        return false
    }


    override fun onFling(arg0: MotionEvent, arg1: MotionEvent, arg2: Float, arg3: Float)
            : Boolean {
        return false
    }


    override fun onLongPress(e: MotionEvent) {}


    override fun onShowPress(e: MotionEvent) {}

Here is the implementation of onScroll, we’ll fill the missing parts in the following paragraph.

这是onScroll的实现,我们将在以下段落中填充缺少的部分。

override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float)
            : Boolean {


        val rotationDegrees = calculateAngle(e2.x, e2.y)
        // use only -150 to 150 range (knob min/max points
        if (rotationDegrees >= -150 && rotationDegrees <= 150) {
            setKnobPosition(rotationDegrees)


            // Calculate rotary value
            // The range is the 300 degrees between -150 and 150, so we'll add 150 to adjust the
            // range to 0 - 300
            val valueRangeDegrees = rotationDegrees + 150
                value = ((valueRangeDegrees / divider) + minValue).toInt()
                if (listener != null) listener!!.onRotate(value)
        }
        return true
    }

onScroll receives two coordinate sets, e1 and e2, representing the start and end movements of the scroll that triggered the event.We’re only interested in the e2 — the new position of the knob — so we can animate it to position and calculate the value.

onScroll接收两个坐标集e1和e2,分别代表触发事件的滚动的开始和结束运动。我们仅对e2(旋钮的新位置)感兴趣,因此我们可以对其进行动画处理以定位和计算值。

I am using a function we’ll review in the next section to calculate the angle of rotation.

我使用的功能将在下一部分中进行回顾,以计算旋转角度。

As mentioned earlier, we’re only using 300 degrees from the knobs start point to its end point, so here we also calculate what value should the knob’s position represent using the divider.

如前所述,我们仅使用从旋钮起点到终点的300度,因此在这里我们还计算了旋钮位置应使用分频器代表的值。

计算旋转角度 (Calculating the rotation angle)

Now let’s write the calculateAngle function.

现在,让我们编写calculateAngle函数。

private fun calculateAngle(x: Float, y: Float): Float {
        val px = (x / width.toFloat()) - 0.5
        val py = ( 1 - y / height.toFloat()) - 0.5
        var angle = -(Math.toDegrees(atan2(py, px)))
            .toFloat() + 90
        if (angle > 180) angle -= 360
        return angle
    }

This function calls for a bit of explanation and some 8th grade math.

该函数需要一些解释和一些八年级数学。

The purpose of this function is the calculate the position of the knob in angles, based on the passed coordinates. I opted to treat the 12 o’clock position of the knob as zero, and then increase its position to positive degrees when turning clockwise, and reducing to negative degrees when turning counterclockwise from 12 o’clock.

此功能的目的是根据传递的坐标以角度计算旋钮的位置。 我选择将旋钮的12点钟位置视为零,然后在顺时针旋转时将其位置增加到正数,从12点逆时针旋转时将其位置减少到负数。

Image for post

We get the x, y coordinates from the onScroll function, indicating the position within the view where the movement ended (for that event).X and y represent a point on a cartesian coordinate system. We can convert that point representation to a polar coordinate system, representing the point by the angle above or below the x axis and the distance of the point from the pole.

我们从onScroll函数获得x,y坐标,指示视图在该运动结束处的位置(针对该事件).X和y代表笛卡尔坐标系上的一个点。 我们可以将该点表示转换为极坐标系,以x轴上方或下方的角度以及该点与极点的距离来表示该点。

Converting between the two coordinate systems can be achieved by the atan2 function. Luckily for us, the Kotlin math library provides us with an implementation of atan2, as do most Math libraries.

可以通过atan2函数在两个坐标系之间进行转换。 对我们来说幸运的是,与大多数数学库一样,Kotlin数学库为我们提供了atan2的实现。

We do however, need to account for a few differences between our knob model and the naive math implementation.

但是,我们确实需要考虑旋钮模型和朴素的数学实现之间的一些差异。

  1. The (0,0) coordinates represent the top right corner of the view and not the middle. And while the x coordinate progresses in the right direction — growing as we move to the right — the y coordinate is backwards — 0 is the top of the view, while the value of our view’s height is the lowest pixel line in the view.

    (0,0)坐标表示视图的右上角,而不是中间。 并且,当x坐标朝正确的方向前进时(随着向右移动而增长),y坐标向后倾斜,0表示视图的顶部,而视图高度的值是该视图中的最低像素线。

    To accommodate that we divide x and y by the respective width and height of the view to get them on a normalized scale of 0–1.

    为了适应这种情况,我们将x和y分别除以视图的宽度和高度,以使其归一化为0–1。

    Then we subtract 0.5 from both to move the 0,0 point to the middle.

    然后我们从两者中减去0.5,将0,0点移到中间。

    And lastly, we subtract y’s value from 1, to reverse its direction.

    最后,我们从1减去y的值以反转其方向。

  2. The polar coordinate system is in reverse direction to what we need. The degrees value rises as we turn counter clockwise. So we add a minus sign to reverse the result of the atan2 function.

    极坐标系与我们所需的方向相反。 随着我们逆时针旋转,度数值会增加。 因此,我们添加一个负号以反转atan2函数的结果。
  3. We want the 0 degrees value to point north, otherwise passing 9 o’clock, the value will jump from 0 to 359.

    我们希望0度的值指向北,否则经过9点,该值将从0跳到359。

    So we add 90 to the result, taking care to reduce the value by 360 once the angle is larger than 180 (so we get a -180 < angle < 180 range rather than a 0 < x < 360 range)

    因此,我们将结果加90,请注意在角度大于180时将值减少360(因此,我们得到-180 <angle <180范围,而不是0 <x <360范围)

The next step is to animate the rotation of the knob. We use Matrix to transform the coordinates of the ImageView.

下一步是对旋钮的旋转进行动画处理。 我们使用Matrix来变换ImageView的坐标。

We just need to pay attention to dividing the view’s height and width by 2 so the rotation axis is the middle of the knob.

我们只需要注意将视图的高度和宽度除以2,以便旋转轴位于旋钮的中间。

private fun setKnobPosition(angle: Float) {
        val matrix = Matrix()
        knobImageView.scaleType = ScaleType.MATRIX
        matrix.postRotate(angle, width.toFloat() / 2, height.toFloat() / 2)
        knobImageView.imageMatrix = matrix
    }

And last but not least, let’s expose an interface for the consuming Activity or Fragment to listen to rotation events:

最后但并非最不重要的一点,让我们为使用中的Activity或Fragment提供一个接口,以监听旋转事件:

interface RotaryKnobListener {
        fun onRotate(value: Int)
    }

使用旋钮 (Using the knob)

Now, let’s create a simple implementation to test our knob.

现在,让我们创建一个简单的实现来测试我们的旋钮。

In the main activity, lets create a TextView and drag a view from the containers list. When presented with the view selection, select RotaryKnobView.

在主要活动中,让我们创建一个TextView并将一个视图从容器列表中拖出。 显示视图选择后,选择RotaryKnobView。

Image for post

Edit the activity’s layout xml file, and set the minimum, maximum and initial values as well as the drawable to use.

编辑活动的布局xml文件,并设置最小值,最大值和初始值以及要使用的可绘制对象。

<geva.oren.rotaryknobdemo.RotaryKnobView
        android:id="@+id/knob"
        class="geva.oren.rotaryknobdemo.RotaryKnobView"
        android:layout_width="@dimen/knob_width"
        android:layout_height="@dimen/knob_height"
        android:layout_marginBottom="312dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView"
        app:knobDrawable="@drawable/ic_rotary_knob"
        app:initialValue="50"
        app:maxValue="100"
        app:minValue="0" />

Finally, in our MainActivity class, inflate the layout and implement the RotaryKnobListener interface to update the value of the TextField.

最后,在我们的MainActivity类中,为布局充气并实现RotaryKnobListener接口以更新TextField的值。

package geva.oren.rotaryknobdemo


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity(), RotaryKnobView.RotaryKnobListener {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        knob.listener = this
        textView.text = knob.value.toString()
    }


    override fun onRotate(value: Int) {
        textView.text = value.toString()
    }
}

This example project is available on github as well as the original metronome project.The Android Metronome app is also available on Google’s play store.

这个示例项目可以在github上以及原始的节拍器项目中找到 。AndroidMetronome应用程序也可以在Google的Play商店中找到

翻译自: https://medium.com/swlh/creating-a-kotlin-android-rotary-knob-b3a16d02e346

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值