需要帮助将 iOS 中用 Swift 编写的仪 TableView 转换为 Android 中用 Kotlin 编写的自定义 View 。 Video & assets
import UIKit
class GaugeView: UIView {
var outerBezelColor = UIColor.gray20!
var outerBezelWidth: CGFloat = 2
var innerBezelColor = UIColor.baseWhite
var innerBezelWidth: CGFloat = 5
var insideColor = UIColor.baseWhite
var segmentWidth: CGFloat = 0
var segmentColors = [UIColor.gray20!]
var totalAngle: CGFloat = 270
var rotation: CGFloat = -135
let mainBg = UIImageView()
var needleColor = UIColor.clear
var needleWidth: CGFloat = 23
let needle = UIView()
let polygon = UIImageView()
let valueLabel = UILabel()
var valueFont = UIFont(name: "PlusJakartaSans-ExtraBold", size: 32)
var valueColor = UIColor.gray80
let statusLabel = UILabel()
var statusFont = UIFont(name: "PlusJakartaSans-Regular", size: 16)
var statusColor = UIColor.gray70
var value: Int = 0 {
didSet {
// update the value label to show the exact number
valueLabel.text = String(value)
// figure out where the needle is, between 0 and 1
let needlePosition = CGFloat(value) / 100
// create a lerp from the start angle (rotation) through to the end angle (rotation + totalAngle)
let lerpFrom = rotation
let lerpTo = rotation + totalAngle
// lerp from the start to the end position, based on the needle's position
let needleRotation = lerpFrom + (lerpTo - lerpFrom) * needlePosition
needle.transform = CGAffineTransform(rotationAngle: deg2rad(needleRotation))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
func setUp() {
needle.backgroundColor = needleColor
needle.translatesAutoresizingMaskIntoConstraints = false
// make the needle a third of our height
needle.bounds = CGRect(x: 0, y: 0, width: needleWidth, height: bounds.height / 3)
mainBg.bounds = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width/1.2, height: UIScreen.main.bounds.size.width/1.2)
mainBg.image = UIImage(named: "credit-score-meter")
mainBg.contentMode = .scaleAspectFit
mainBg.center = CGPoint(x: bounds.midX, y: bounds.midY)
polygon.bounds = CGRect(x: 0, y: 0, width: 23, height: 23)
polygon.image = UIImage(named: "polygon")
polygon.center = CGPoint(x: needle.bounds.midX, y: 0)
// align it so that it is positioned and rotated from the bottom center
needle.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
// now center the needle over our center point
needle.center = CGPoint(x: bounds.midX, y: bounds.midY)
addSubview(mainBg)
addSubview(needle)
needle.addSubview(polygon)
valueLabel.font = valueFont
valueLabel.text = "0"
valueLabel.textColor = valueColor
valueLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(valueLabel)
statusLabel.font = statusFont
statusLabel.text = "VERY GOOD"
statusLabel.textColor = statusColor
statusLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(statusLabel)
NSLayoutConstraint.activate([
valueLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20)
])
NSLayoutConstraint.activate([
statusLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
statusLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 20)
])
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
drawSegments(in: rect, context: ctx)
}
func deg2rad(_ number: CGFloat) -> CGFloat {
return number * .pi / 180
}
func drawSegments(in rect: CGRect, context ctx: CGContext) {
// 1: Save the current drawing configuration
ctx.saveGState()
// 2: Move to the center of our drawing rectangle and rotate so that we're pointing at the start of the first segment
ctx.translateBy(x: rect.midX, y: rect.midY)
ctx.rotate(by: deg2rad(rotation) - (.pi / 2))
// 3: Set up the user's line width
ctx.setLineWidth(segmentWidth)
// 4: Calculate the size of each segment in the total gauge
let segmentAngle = deg2rad(totalAngle / CGFloat(segmentColors.count))
// 5: Calculate how wide the segment arcs should be
let segmentRadius = (((rect.width - segmentWidth) / 2) - outerBezelWidth) - innerBezelWidth
// 6: Draw each segment
for (index, segment) in segmentColors.enumerated() {
// figure out where the segment starts in our arc
let start = CGFloat(index) * segmentAngle
// activate its color
segment.set()
// add a path for the segment
ctx.addArc(center: .zero, radius: segmentRadius, startAngle: start, endAngle: start + segmentAngle, clockwise: false)
// and stroke it using the activated color
ctx.drawPath(using: .stroke)
}
// 7: Reset the graphics state
ctx.restoreGState()
}
}
我想做的是 使用 FrameLayout 在其上绘制小灰线段。我将尝试添加的下一件事是 ImageView 和针 View ,但我不确定如何适本地旋转它。
class CreditScore(context: Context, attrs: AttributeSet): FrameLayout(context, attrs) {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
}
最佳答案
要在 Android 中实现具有类似行为的上述 Needle GaugeView,您需要一个自定义 ViewGroup
才能为背景图像和针添加 subview ,如 RelativeLayout
> 或您在问题中建议的 FrameLayout
。基于您的快速代码,我在 kotlin 中实现了一个类似的 GaugeView,它从 RelativeLayout
扩展而来。
1.首先创建自定义的 GaugeView,它从 RelativeLayout
扩展而来,如下所示:
class GaugeView : RelativeLayout {
private lateinit var mainBg: ImageView
private lateinit var needle: RelativeLayout
private lateinit var polygon: ImageView
private lateinit var labelsLL: LinearLayout
private lateinit var valueLabel: TextView
private lateinit var statusLabel: TextView
var outerBezelColor: Int = Color.GRAY
var innerBezelColor: Int = Color.WHITE
var insideColor: Int = Color.WHITE
var needleColor: Int = Color.TRANSPARENT
var valueColor: Int = Color.DKGRAY
var statusColor: Int = Color.DKGRAY
var outerBezelWidth = 2f
var innerBezelWidth = 5f
var segmentWidth = 0f
var needleWidth = 23f
var segmentColors = intArrayOf(Color.GRAY)
var totalAngle = 270f
var rotationAngle = -135f
var path: Path = Path()
var paint: Paint = Paint()
var radiusPathRectF = RectF()
var w = 0
var h = 0
constructor(context: Context?) : super(context) {
setup()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
setup()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setup()
}
private fun setup() {
//create the needle RelativeLayout ViewGroup
needle = RelativeLayout(context)
needle.setBackgroundColor(needleColor)
//create the mainBg ImageView
mainBg = ImageView(context)
mainBg.setImageResource(R.drawable.credit_score_meter)
mainBg.setAdjustViewBounds(true)
mainBg.setScaleType(ImageView.ScaleType.CENTER_INSIDE)
//create the polygon ImageView
polygon = ImageView(context)
polygon.setImageResource(R.drawable.ic_needle)
polygon.setAdjustViewBounds(true)
polygon.setScaleType(ImageView.ScaleType.CENTER_INSIDE)
//add the mainBg and needle as subviews and polygon as a subview of needle
addView(mainBg)
addView(needle)
needle.addView(polygon)
//create a Vertical LinearLayout ViewGroup to add the valueLabel and statusLabel as subviews
labelsLL = LinearLayout(context)
labelsLL.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
labelsLL.orientation = LinearLayout.VERTICAL
addView(labelsLL)
//create the valueLabel TextView and add it as a subview of labelsLL
valueLabel = TextView(context)
valueLabel.text = "0"
valueLabel.setTextColor(valueColor)
valueLabel.gravity = Gravity.CENTER
valueLabel.setTypeface(valueLabel.typeface, Typeface.BOLD)
valueLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 25f)
labelsLL.addView(valueLabel)
//create the statusLabel TextView and add it as a subview of labelsLL
statusLabel = TextView(context)
statusLabel.text = "VERY GOOD"
statusLabel.setTextColor(statusColor)
statusLabel.gravity = Gravity.CENTER
statusLabel.setTypeface(statusLabel.typeface, Typeface.NORMAL)
statusLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
labelsLL.addView(statusLabel)
//initialize a path, a paint and a RectF which are needed during the drawing phase
path = Path()
paint = Paint()
radiusPathRectF = RectF()
//center the mainBg ImageView
val mainBgParams = mainBg.layoutParams as LayoutParams
mainBgParams.addRule(CENTER_IN_PARENT, TRUE)
mainBg.layoutParams = mainBgParams
//center the needle RelativeLayout
val needleParams = needle.layoutParams as LayoutParams
needleParams.addRule(CENTER_IN_PARENT, TRUE)
needle.layoutParams = needleParams
//center the labels LinearLayout
val labelsLLParams = labelsLL.layoutParams as LayoutParams
labelsLLParams.addRule(CENTER_IN_PARENT, TRUE)
labelsLL.layoutParams = labelsLLParams
//set valueLabel margins
val valueParams = valueLabel.layoutParams as LinearLayout.LayoutParams
valueParams.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, context.resources.displayMetrics).toInt())
valueLabel.layoutParams = valueParams
//set statusLabel margins
val statusParams = statusLabel.layoutParams as LinearLayout.LayoutParams
statusParams.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, context.resources.displayMetrics).toInt())
statusLabel.layoutParams = statusParams
//set WillNotDraw to false to allow onDraw(Canvas canvas) to be called (This is needed when you have ViewGroups as subviews)
setWillNotDraw(false)
//set the value initially to 0
setValue(0)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
val w = r - l
val h = b - t
//set the mainBg ImageView width and height
val mainBgParams = mainBg.layoutParams as LayoutParams
mainBgParams.width = (w / 1.2).toInt()
mainBgParams.height = (w / 1.2).toInt()
mainBg.layoutParams = mainBgParams
//set the needle width
val needleW = mainBgParams.height / 11
//set the needle RelativeLayout width and height
val needleParams = needle.layoutParams as LayoutParams
needleParams.width = needleW
needleParams.height = 2 * mainBgParams.height / 3
needle.layoutParams = needleParams
//set the polygon ImageView width and height to the same width of needle. Also add some top margin eg: 2 dps.
val polygonParams = polygon.layoutParams as LayoutParams
polygonParams.width = needleW
polygonParams.height = needleW
polygonParams.setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, context.resources.displayMetrics).toInt(), 0, 0)
polygon.layoutParams = polygonParams
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
this.w = w
this.h = h
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 1: Save the current drawing configuration
canvas.save()
// 2: Move to the center of our drawing rectangle and rotate so that we're pointing at the start of the first segment
canvas.translate(w.toFloat() / 2, h.toFloat() / 2)
canvas.rotate((rotationAngle - Math.PI / 2).toFloat())
// 3: Set up the user's line width
paint.setStrokeWidth(segmentWidth)
// 4: Calculate the size of each segment in the total gauge in degrees
val segmentAngle = totalAngle / segmentColors.size.toFloat()
// 5: Calculate how wide the segment arcs should be
val segmentRadius = (w - segmentWidth) / 2 - outerBezelWidth - innerBezelWidth
// 6: Draw each segment
for (index in segmentColors.indices) {
val segment = segmentColors[index]
// figure out where the segment starts in our arc in degrees
val start = index.toFloat() * segmentAngle
//activate its color
paint.setColor(segment)
// add a path for the segment
radiusPathRectF.left = -segmentRadius/2
radiusPathRectF.top = -segmentRadius/2
radiusPathRectF.right = segmentRadius/2
radiusPathRectF.bottom = segmentRadius/2
path.addArc(radiusPathRectF, -90F, start + segmentAngle)
// and stroke it using the activated color
paint.setStyle(Paint.Style.STROKE)
canvas.drawPath(path, paint)
}
// 7: Reset the graphics state
canvas.restore()
}
/**
* Call this helper method to set a new value
* @param value must be a number between 0-100
*/
fun setValue(value: Int) {
// update the value label to show the exact number
valueLabel.text = value.toString()
// update the status label based on the value eg: VERY GOOD or GOOD
statusLabel.text = if (value > 50) "VERY GOOD" else "GOOD"
// figure out where the needle is, between 0 and 1 (This will set the min value to 0 and max value to 100)
// in case you want to have a range between 0-1000 divide below with 1000
val needlePosition = value.toFloat() / 100
// create a lerp from the start angle (rotationAngle) through to the end angle (rotationAngle + totalAngle)
val lerpFrom = rotationAngle
val lerpTo = rotationAngle + totalAngle
// lerp from the start to the end position, based on the needle's position
val needleRotation = lerpFrom + (lerpTo - lerpFrom) * needlePosition
//in android rotation is in degrees instead of radians
needle.rotation = needleRotation
}
}
其中R.drawable.credit_score_meter
是您的主要背景图片:
R.drawable.ic_needle
是针矢量图标:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="23dp"
android:height="23dp"
android:viewportWidth="23"
android:viewportHeight="23">
<path
android:pathData="M11.8054,0.8052L22.8054,22.8052C22.8054,22.8052 16.0848,21.4364 11.7257,21.4441C7.4285,21.4517 0.8054,22.8052 0.8054,22.8052L11.8054,0.8052Z"
android:fillColor="#2BE252"/>
</vector>
2.在 xml 布局中定义自定义 GaugeView,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<com.my.packagename.GaugeView
android:id="@+id/gaugeView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true"/>
</RelativeLayout>
3.最后使用上面的 GaugeView 以编程方式设置新值,如下所示:
val gaugeView = findViewById<GaugeView>(R.id.gaugeView)
gaugeView.setValue(75) //values range 0-100
从上面的代码来看,与 swift 版本的主要区别是旋转,在 Android 中使用度数而不是 iOS 中使用的弧度。
另请注意,上面的示例具有 0 到 100 之间的数字范围。如果您想要 0 到 1000 之间的范围,您可以更改此行 val NeedlePosition = value.toFloat()/100
并除以为1000。当然您可以根据您的需要进一步修改代码。这是您起点的示例版本。希望对您有所帮助。
结果:
动画针
要将针动画到特定的角度值,您可以简单地使用 needle.animate().rotationBy(angle)
中的构建,如下所示:
needle.animate()
.rotationBy(rotationAngle)
.setDuration(500)
.setInterpolator(LinearInterpolator())
.start()
如果旋转角度为正值,则顺时针旋转,如果为负值,则逆时针旋转。
我已经用上面的动画实现了一个示例,您可以在其中使用并进一步修改它以适合您的情况。只需将 fun setValue(value: Int)
替换为新的,如下所示:
private var prevValue = -1;
/**
* Call this helper method to set a new value
* @param value must be a number between 0-100
*/
fun setValue(value: Int) {
if(prevValue == value)
return
// update the value label to show the exact number
valueLabel.text = value.toString()
// update the status label based on the value eg: VERY GOOD or GOOD
statusLabel.text = if (value > 50) "VERY GOOD" else "GOOD"
// figure out where the needle is, between 0 and 1 (This will set the min value to 0 and max value to 100)
// in case you want to have a range between 0-1000 divide below with 1000
val needlePosition = value.toFloat() / 100
// create a lerp from the start angle (rotationAngle) through to the end angle (rotationAngle + totalAngle)
val lerpFrom = rotationAngle
val lerpTo = rotationAngle + totalAngle
// lerp from the start to the end position, based on the needle's position
val needleRotation = lerpFrom + (lerpTo - lerpFrom) * needlePosition
//calculate the rotationBy angle (rotation delta angle)
var rot = 0f
val diff = Math.abs(Math.abs(needle.rotation) - Math.abs(needleRotation))
if(needle.rotation == 0f && needleRotation == rotationAngle)
{
rot = rotationAngle
}
else if(needle.rotation == rotationAngle && needleRotation == 135f){
rot = 135f*2;
}
else if(needleRotation < 0)
{
if(needleRotation < needle.rotation){
if(needle.rotation > 0) {
if (needle.rotation == 135f){
rot = -(135f*2 - diff)
}
else if(needleRotation == rotationAngle){
rot = -(135f + Math.abs(needle.rotation))
}
else {
rot = -(Math.abs(needle.rotation) + Math.abs(needleRotation))
}
}
else {
rot = -diff
}
}
else if(needleRotation > needle.rotation){
rot = +diff
}
else{
rot = rotationAngle
}
}
else if(needleRotation > 0)
{
if(needleRotation < needle.rotation){
rot = -diff
}
else if(needleRotation > needle.rotation){
if(needle.rotation < 0) {
if (needle.rotation == rotationAngle){
rot = 135f + Math.abs(needleRotation)
}
else{
rot = Math.abs(needle.rotation) + Math.abs(needleRotation)
}
}
else {
rot = +diff
}
}
else{
rot = rotationAngle
}
}
else if (needleRotation == 0f)
{
if(needle.rotation == 135f)
rot = -diff
else
rot = +diff
}
//and animate the needle using the rotationBy()
needle.animate()
.rotationBy(rot) //if this value is negative it goes anticlockwise and if its positive is goes clockwise
.setDuration(500)
.setInterpolator(LinearInterpolator())
.start()
prevValue = value
}
您可以像下面的示例一样测试它:
var i = 1
object : CountDownTimer(202000, 2000) {
override fun onTick(millisUntilFinished: Long) {
changeValue(i++)
}
override fun onFinish() {}
}.start()
其中 fun changeValue(number: Int)
是以下助手:
fun changeValue(number: Int){
gaugeView.setValue(0)
Thread(Runnable {
Handler(Looper.getMainLooper()).postDelayed(Runnable {
gaugeView.setValue(number)
}, 1000)
}).start()
}
动画结果:
关于android - 创建从 iOS (Swift) 到 Android(Kotlin) 的 Needle GaugeView,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74010297/