When my son's piano teacher told him he should use a metronome to practice timing, I took it as an opportunity to learn Kotlin. I decided to learn the language and Android's ecosystem so I could build a Metronome app.
当我儿子的钢琴老师告诉他应该使用节拍器练习计时时,我借此机会学习了Kotlin。 我决定学习语言和Android的生态系统,以便构建Metronome应用程序。
My initial implementation used a SeekBar to control BPM (Beats per Minute) — the rate at which the metronome ticks.
我最初的实现使用SeekBar来控制BPM(每分钟节拍数)-节拍器滴答声的速率。
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.
但是,随着项目的进展,我希望使其类似于一个物理数字单元,这是现实世界中许多音乐家使用的。
Physical units do not have a “SeekBar View”, and I wanted to mimic the rotary knob an actual unit might have.
物理单位没有“ SeekBar视图”,我想模拟实际单位可能具有的旋钮。
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:
旋钮是非常有用的UI控件。 它们很像滑块或SeekBar,可在许多情况下使用。 以下是它们的一些优点:
While a few open source knob libraries for Android exist, I didn’t find quite what I was looking for in any of them.
尽管存在一些适用于Android的开放源代码旋钮库,但是我在任何一个库中都找不到我想要的东西。
Many were 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.
对于我的适度需求而言,许多功能都有些过分,例如设置背景图像或处理两个或多个模式操作的水龙头等功能。有些不具备我想适合我的项目的可定制性,并带有自己的旋钮图像。
Still others assumed a discrete range of values or positions. And many of them seemed much more complex than needed.
还有一些假设值或位置的离散范围。 其中许多似乎比需要的要复杂得多。
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.
因此,让我们看看如何创建一个旋钮。
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. This would allow me to create the illusion of a depressed surface and light reflection.
第一步是为旋钮本身创建图形。 我绝不是设计师,但是我想到在旋钮控件中营造“深度”和运动感的关键是使用偏心的径向渐变。 这将使我产生表面凹陷和光反射的错觉。
I used Sketch to draw the knob, then exported it to svg. Then I imported it back into Android studio as a drawable.
我使用Sketch绘制了旋钮,然后将其导出到svg。 然后,将其作为可绘制对象导入Android Studio。
You can find the knob drawable in the GitHub project link at the bottom of this article.
您可以在本文底部的GitHub项目链接中找到可绘制的旋钮。
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.
可以使用代码完全创建视图,但是应该使用xml创建Android中良好的可重用视图。
Notice the
注意
We’ll use an ImageView for the knob, which we’ll rotate as the user moves it.
我们将为旋钮使用ImageView,并随着用户移动它进行旋转。
To make the knob configurable by xml, we'll create attributes for the range of values the knob will return, as well as for the drawable it will use for visuals.
为了使该旋钮可通过xml配置,我们将为该旋钮将返回的值的范围以及用于视觉效果的drawable创建属性。
We’ll create an attrs.xml file under res/values.
我们将在res / values下创建一个attrs.xml文件。
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构造函数的所有三种形式的快捷方式。
Next we’ll initialize some default values and define class members.
接下来,我们将初始化一些默认值并定义类成员。
A note about the divider variable — I wanted the knob to have start 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.
关于分频器变量的注释-我希望旋钮具有开始和结束位置,而不是能够无限旋转,就像立体声系统上的音量旋钮一样。 我将起点和终点分别设置为-150度和150度。 因此,旋钮的有效运动仅为300度。
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.
我们将使用除法器来分配希望旋钮在这些可用的300度上返回的值的范围-以便我们可以根据旋钮的位置角度来计算实际值。
Next, we initialize the component:
接下来,我们初始化组件:
The class will not compile just yet, as we need to implement OnGestureListener’s functions. Let's handle that now.
由于我们需要实现OnGestureListener的功能,因此该类尚未编译。 让我们现在处理。
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中实现移动登录。
Here is the implementation of onScroll. We’ll fill in the missing parts in the following paragraph.
这是onScroll的实现。 我们将在以下段落中填写缺失的部分。
onScroll receives two coordinate sets, e1 and e2, representing the start and end movements of the scroll that triggered the event.
onScroll接收两个坐标集e1和e2,分别代表触发事件的滚动的开始和结束运动。
We’re only interested in the e2 — the new position of the knob — so we can animate it to position and calculate the value.
我们只对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 knob's start point to its end point, so here we also calculate what value the knob’s position should represent using the divider.
如前所述,我们仅使用从旋钮的起点到终点的300度,因此在这里,我们还使用分频器计算旋钮的位置应代表什么值。
Now let’s write the calculateAngle function.
现在,让我们编写calculateAngle函数。
This function calls for a bit of explanation and some 8th grade math.
该函数需要一些解释和一些八年级数学。
The purpose of this function is to 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点逆时针旋转时减少到负数。
We get the x, y coordinates from the onScroll function, indicating the position within the view where the movement ended (for that event).
我们从onScroll函数获取x,y坐标,指示视图在该运动中结束的位置(针对该事件)。
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.
X和y代表笛卡尔坐标系上的一个点。 我们可以将该点表示转换为极坐标系,以x轴上方或下方的角度以及该点与极点的距离来表示该点。
Converting between the two coordinate systems can be done with 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 naïve math implementation.
但是,我们确实需要考虑旋钮模型和简单的数学实现之间的一些差异。
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的值以反转其方向。
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
The next step is to animate the rotation of the knob. We'll 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,以便旋转轴位于旋钮的中间。
And last but not least, let’s expose an interface for the consuming Activity or Fragment to listen to rotation events:
最后但并非最不重要的一点,让我们为使用中的Activity或Fragment提供一个接口,以监听旋转事件:
Now, let’s create a simple implementation to test our knob.
现在,让我们创建一个简单的实现来测试我们的旋钮。
In the main activity, let's create a TextView and drag a view from the containers list. When presented with the view selection, select RotaryKnobView.
在主要活动中,我们创建一个TextView并将一个视图从容器列表中拖动。 显示视图选择后,选择RotaryKnobView。
Edit the activity’s layout xml file, and set the minimum, maximum, and initial values as well as the drawable to use.
编辑活动的布局xml文件,并设置最小值,最大值和初始值以及要使用的可绘制对象。
Finally, in our MainActivity class, inflate the layout and implement the RotaryKnobListener interface to update the value of the TextField.
最后,在我们的MainActivity类中,为布局充气并实现RotaryKnobListener接口以更新TextField的值。
And we're done! This example project is available on github as well as the original metronome project.
我们完成了! 这个示例项目以及原始的节拍器项目都可以在github上找到。
The Android Metronome app is also available on Google’s play store.
Android Metronome应用程序也可以在Google的Play商店中使用 。
翻译自: https://www.freecodecamp.org/news/how-to-create-an-android-rotary-knob-using-kotlin/