android 拨盘
The text of the dial and how to draw text on a curve.
表盘上的文字以及如何在曲线上绘制文字。
In part 1 of this series, we designed the basic interface for the dial. In this part we are going to layout the text inside the dial.
在本系列的第1部分中,我们设计了表盘的基本界面。 在这一部分中,我们将在表盘内布置文本。
The text that we need to layout here is the top and bottom labels, “ACCOUNT BALANCE” and “SHOW ME THE MONEY”.
我们需要在此处布置的文本是顶部和底部标签,“帐户余额”和“显示我的钱”。
Apple has a CoreText sample project here. It draw’s text along an arc, I thought that would be enough, unfortunalty it wasn’t a fit for what I needed. The bottom string would’ve been upside down. So I implemented my own text along a path solution.
苹果在这里有一个CoreText示例项目。 它沿弧线绘制文本,我认为就足够了,不幸的是,它不适合我的需要。 底部的字符串本来应该是颠倒的。 因此,我沿着路径解决方案实现了自己的文本。
I chose to use Bézier curves, it would give me the freedom to layout the text however I want.
我选择使用Bézier曲线 ,这将使我可以自由地按需布置文本。
![iOS simulator showing text rendered along a curve](https://miro.medium.com/max/9999/1*zC-UONk7tjWdF1o3WOSrnQ.jpeg)
贝塞尔曲线 (Bézier curves)
To layout the text along a curve, we need to compute the points of the curve as well as it’s tangents. We use the tanget to find the angle at which we need to rotate the text. Think of the tangent line as the line we need to draw the text on.
要沿曲线布置文本,我们需要计算曲线的点及其切线。 我们使用切线找到需要旋转文本的角度。 将切线视为我们需要在其上绘制文本的线。
Bézier curves have the following equation:𝐁❨𝘵❩ = ❨𝟷 − 𝘵❩³𝚸₀ + 𝟹❨𝟷 − 𝘵❩²𝘵 𝚸₁ + 𝟹❨𝟷 − 𝘵❩𝘵²𝚸₂ + 𝘵³𝚸₃, 𝟶 ≤ 𝘵 ≤ 𝟷
贝塞尔曲线具有以下等式:𝐁❨𝘵❩=❨𝟷-𝘵❩³𝚸₀ +𝟹❨𝟷-𝘵❩²𝘵𝚸₁ +𝟹❨𝟷-𝘵❩𝘵²𝚸 2 + 𝘵³𝚸₃,𝟶≤𝘵≤𝟷
And it’s tangent equation is:𝐁´❨𝘵❩ = 𝟹❨𝟷 − 𝘵❩²❨𝚸₁ − 𝚸₀) + 𝟼❨𝟷 − 𝘵❩𝘵 ❨𝚸₂ − 𝚸₁) + 𝟹𝘵²❨𝚸₃ − 𝚸₂❩
它的切线方程为:𝐁=❨𝘵❩-𝘵❩²❨𝚸₁-𝚸₀)+𝟼❨𝟷-𝘵❩𝘵_2-𝚸₁)+𝟹𝘵²𝟹𝘵-𝚸²𝚸
The following function generates the points and tangets for a curve.
以下函数生成曲线的点和切线。
The function above loops through the curve equation from 0 to 1 in steps of 0.001. To draw the curve we draw lines connecting the points together. It’s effectivly an approximation to a curve than an actual true curve.
上面的函数以0.001的步长从0到1循环遍历曲线方程。 为了绘制曲线,我们绘制了将点连接在一起的线。 实际上,它比实际的真实曲线更接近曲线。
The result is stored in a simple structure like this one:
结果存储在这样的简单结构中:
渲染文字 (Rendering The Text)
The text will be rendered onto a graphics context. In Swift I always forgot how to set the last parameter of CGContext
correctly so I've created a simple CGContext wrapper.
文本将被渲染到图形上下文中。 在Swift中,我总是忘记了如何正确设置CGContext
的最后一个参数,因此我创建了一个简单的CGContext包装器。
You can also use
UIGraphicsUIGraphicsImageRenderer
to achieve the same results.您还可以使用
UIGraphicsUIGraphicsImageRenderer
获得相同的结果。
Now we’re ready to render the string, I created a function render(attributedString: curve: bitmap:)
passing in the text as an NSAttributedString
so that we can control parameters such as font, color, alignment and kerning.
现在我们准备渲染字符串,我创建了一个函数render(attributedString: curve: bitmap:)
作为NSAttributedString
传入文本,以便我们可以控制诸如字体,颜色,对齐方式和字距调整等参数。
What we’re doing here is finding a space on the curve that will fit the character’s width, once we find a big enough space, we draw that character’s glyph, and repeat the process until all the characters are drawn or we’ve run out of space on the curve.
我们在这里所做的是在曲线上找到一个适合角色宽度的空间,一旦找到足够大的空间,我们便绘制该字符的字形,然后重复该过程,直到绘制完所有字符或用完为止曲线上的空间。
Lets break it down further. We create a CTLine
with the attributedString
to get the CTRun
's of the line. We’re only interested in the first glyph run. We then check if any kerning is needed.
让我们进一步分解。 我们使用attributedString
创建一个CTLine
以获得该行的CTRun
。 我们只对第一个字形感兴趣。 然后,我们检查是否需要字距调整。
The function indexOfStartPoint()
check's the attributed string's text alignment. If the text is not left aligned, we need to find the point on the curve where we want to start rendering the text. So if the text is right aligned, we measure the length of the curve (remember the curve is just a series of lines) and measure the length of the text, subtracting the length of the text from the length of the curve will tell us where on the curve we need to start.Similarly, if the text is centred, finding the half way point of the curve and subtracting half the length of the text will tell us where we need to start.
函数indexOfStartPoint()
检查属性字符串的文本对齐方式。 如果文本未对齐,则需要在曲线上找到要开始渲染文本的点。 因此,如果文本右对齐,我们将测量曲线的长度(记住曲线只是一系列直线)并测量文本的长度,从曲线的长度中减去文本的长度将告诉我们在哪里类似地,如果文本居中,则找到曲线的中点并减去文本长度的一半将告诉我们需要从哪里开始。
Back to render()
now that we know at which point we want to start on the curve, we loop through the glyph run, measuring each glyph then trying to find the range of points on the curve that will fit the glyph. Once we've found the range, get the tangent of the middle point in that range, from that tangent we can calculate the angle we need to rotate the glyph by finding the arctan of the tangent.
回到render()
现在我们知道要在曲线上的哪一点开始,我们循环遍历该字形,测量每个字形,然后尝试在曲线上找到适合该字形的点范围。 找到范围后,获取该范围内的中点的切线,从该切线中,我们可以通过找到切线的反正切来计算旋转字形所需的角度。
Armed with the point on the curve where we want to draw the glyph and the angle by which to rotate it, it's simply a matter of setting up the bitmap context's transform to match these parameters and draw the glyph.You'll notice that we also get the font and color within the loop, we could do that check outside the loop but then we’ed lose the ability to customise the text per glyph.
有了要绘制字形的曲线上的点以及旋转它的角度,只需设置位图上下文的变换以匹配这些参数并绘制字形即可,您将注意到我们还在循环中获取字体和颜色,我们可以在循环外进行检查,但是我们失去了按字形自定义文本的功能。
返回表盘 (Back To The Dial)
Let’s refactor the above so we can use it in our view.
让我们重构以上内容,以便可以在我们的视图中使用它。
In Xcode’s Project Navigator create a new group, let’s call it Curved Text.
在Xcode的Project Navigator中,创建一个新组,我们将其称为Curved Text 。
If you can’t see the Project Navigator pane, open the View menu then goto Navigators submenu and select Show Project Navigator.
如果看不到Project Navigator窗格,请打开View菜单,然后转到Navigators子菜单,然后选择Show Project Navigator 。
Select that group and create a new swift file called CurvedTextRenderer.
选择该组并创建一个名为CurvedTextRenderer的新快速文件。
![Xcode window showing CurvedTextRenderer class](https://miro.medium.com/max/9999/1*7XiyxLD7-DD-0Sfz9SYlEQ.jpeg)
Add a new file to the group called Bitmap.swift and add the Bitmap
class from above.
将一个新文件添加到名为Bitmap.swift的组中,然后从上方添加Bitmap
类。
Add a bitmap
property to CurvedTextRenderer
.
向CurvedTextRenderer
添加一个bitmap
属性。
Add the Curve
struct from above to CurvedTextRenderer
.
从上方将Curve
结构添加到CurvedTextRenderer
。
To make writing the bézier curve equations easier and to improve readability, I added some operator overloads on CGPoint
in a seperate file called CGPointAdditions.swift as follows:
为了简化编写贝塞尔曲线方程式并提高可读性,我在CGPoint
的单独文件CGPointAdditions.swift中添加了一些运算符重载,如下所示:
Add the functions computeBezierCurve(::::)
, render(:::)
and indexOfStartPoint(:::)
to CurvedTextRenderer
.
将函数computeBezierCurve(::::)
, render(:::)
和indexOfStartPoint(:::)
到CurvedTextRenderer
。
As mentioned above, we need a graphics context to render our content. To present the graphics context in our view we need to add a UIImageView.
如上所述,我们需要一个图形上下文来呈现我们的内容。 为了在我们的视图中显示图形上下文,我们需要添加一个UIImageView。
Open up PaymentDialView.xib, add a UIImageView inside the Text View, make sure it’s behind the labels, you do that by making it the top most subview inside Text View. Pin it to it’s superview and let’s call it Bitmap View.
打开PaymentDialView.xib ,在Text View内添加UIImageView,确保它在标签后面,您可以通过使其成为Text View内最顶部的子视图来实现 。 将其固定到其超级视图,我们称之为Bitmap View 。
![Interface builder showing bitmap view with its consrtaints](https://miro.medium.com/max/9999/1*s17HA3ifMTwmhwWR8BP2qg.jpeg)
Connect our Bitmap View to the PaymentDialView
class by creating an outlet and then connecting it to Bitmap View.
通过创建插座,然后将其连接到Bitmap View,将我们的Bitmap View连接到PaymentDialView
类。
Now let’s create a class method on CurvedTextRenderer
where we pass it two attribtuted string and the frame of our image view. It will return an image of the graphics context that we can set in our image view.
现在,让我们在CurvedTextRenderer
上创建一个类方法,在其中将两个属性字符串和图像视图的框架传递给该方法。 它将返回可以在图像视图中设置的图形上下文的图像。
类方法 (The Class Method)
Our class method will have the following declaration:
我们的类方法将具有以下声明:
To draw the the two strings, we need two curves, one for the top string and one for the bottom string. In the top screenshot of the curved text, the text is drawn above the line, we’ll need to factor that in when laying the top curve.
要绘制两条弦,我们需要两条曲线,一条曲线用于顶部的弦,一条曲线用于底部的弦。 在弯曲文本的顶部屏幕截图中,文本绘制在直线上方,我们在放置顶部曲线时需要将其考虑在内。
We first need create an instance of CurvedTextRenderer
.
我们首先需要创建一个CurvedTextRenderer
实例。
Then setup our graphics context, we create one using the frame’s size that was passed in:
然后设置我们的图形上下文,我们使用传入的帧大小创建一个上下文:
I mentioned above breifly that the text is drawn above the curve line, that means if we use frame
's current size for our curve's points, the top string will be drawn outside our graphics context. Therefore we need to create a new inset frame, a frame that is smaller than the context's size but share's the same centre. we'll also add some extra padding at the top so that the text looks nicer.The value of the inset will be the top string's font size multiplied by a line spacing factor, I've set line spacing at 1.7. The font size is retrieved from top attributed string.
我在上面简短地提到,文本是绘制在曲线上方的,这意味着如果我们将frame
的当前大小用于曲线的点,则顶部字符串将绘制在图形上下文之外。 因此,我们需要创建一个新的插入框架,该框架小于上下文的大小,但共享相同的中心。 我们还将在顶部添加一些额外的填充以使文本看起来更好。插入值将是顶部字符串的字体大小乘以行距因子,我将行距设置为1.7。 字体大小是从顶级属性字符串中检索的。
According to this post to approximate a circle’s arc with bézier curves, the control points need to be
根据这篇文章 ,用贝塞尔曲线近似圆弧,控制点需要是
xValueInset = Diameter * 0.05 yValueOffset = radius * 4.0 / 3.0
xValueInset = Diameter * 0.05 yValueOffset = radius * 4.0 / 3.0
P0 = (0,0) P1 = (xValueInset, yValueOffset) P2 = (Diameter - xValueInset, yValueOffset) P3 = (Diameter, 0)
P0 = (0,0) P1 = (xValueInset, yValueOffset) P2 = (Diameter - xValueInset, yValueOffset) P3 = (Diameter, 0)
As as result of the above our top curve’s points will be as follows:
由于以上原因,我们的顶部曲线的点如下:
scaledFrame
is the frame
multiplied by UIScreen.main.scale
, scaling the frame to the device's native resolution.
scaledFrame
是frame
乘以UIScreen.main.scale
,将帧缩放到设备的原始分辨率。
For the bottom string we follow a similar approach as above, however we don’t add a line spacing factor, since the text is drawn above the curve.
对于底部字符串,我们采用与上述类似的方法,但是由于文本是在曲线上方绘制的,因此我们不添加行距因子。
Finally we return a UIImage of the context to display in our view.
最后,我们返回上下文的UIImage以显示在我们的视图中。
In the PaymentDialView
’s commonInit()
we'll need to call this class method to set the image. We'll also call it again in updateConstraints()
so that the image view updates its content whenever the image view changes it's frame. We will be putting all this in a seperate function, that way we don't have to repeat ourselves, preventing code duplicaiton.
在PaymentDialView
的commonInit()
我们需要调用此类方法来设置图像。 我们还将在updateConstraints()
再次调用它,以便每当图像视图更改其框架时,该图像视图都会更新其内容。 我们将把所有这些都放在一个单独的函数中,这样我们就不必重复自己了,从而避免了代码重复。
Our updateRoundedTextView()
function inside PaymentDialView
will look like this:
PaymentDialView
内部的updateRoundedTextView()
函数如下所示:
And our view should now look like this:
现在,我们的视图应如下所示:
![The dial showing the curved text](https://miro.medium.com/max/9999/1*2-Yj03ZXPKpyAnN9Zy6BDA.jpeg)
I must admit I found this part to be the hardest and it took the longest to develop!I hope you enjoyed this part and it was clear enough. Join me in part 3 to setup the interaction on the dial.
我必须承认,我发现这部分是最难的,而且开发时间也最长。我希望您喜欢这一部分,并且足够清楚。 和我一起参加第3部分,在表盘上设置互动。
奖金材料 (Bonus Material)
In part 1 we added updateConstriants()
into prepareForInterfaceBuilder()
so when we open the Main.storyboard this is what should appear:
在第1部分中,我们将updateConstriants()
添加到prepareForInterfaceBuilder()
因此当我们打开Main.storyboard时 ,应显示以下内容:
![Interface Builder showing the curved text in live render](https://miro.medium.com/max/9999/1*0tLbzWaUYpCWMPgFkWg6dw.jpeg)
However we want to able to change the text in Interface Builder as well, so that we don’t have to keep running the project to see our changes. We do that by adding these two properties:
但是,我们也希望能够在Interface Builder中更改文本,这样我们就不必继续运行项目来查看所做的更改。 为此,我们添加了以下两个属性:
We also need to update the updateRoundedTextView()
function to use these values, instead of the hardcoded ones.
我们还需要更新updateRoundedTextView()
函数以使用这些值,而不是硬编码的值。
With the above, we should be able to do the following in Interface Builder.
通过以上操作,我们应该能够在Interface Builder中执行以下操作。
![Interface builder showing the IBInspectable fields to edit the curved text](https://miro.medium.com/max/9999/1*pfmk4fkEGbpaFfT-3pfhyg.jpeg)
We can also change the font, font size, kerning and text color. I’ll leave that as an exercise for you dear reader.
我们还可以更改字体,字体大小,字距和文本颜色。 亲爱的读者,我将其保留为练习。
Join me in part 3 to make the dial respond to user input.
加入我的第3部分,使表盘响应用户输入。
翻译自: https://medium.com/dev-genius/how-to-create-an-apple-card-payment-dial-part-2-ad056b8cf572
android 拨盘