Я пытаюсь реализовать виджет экрана блокировки самостоятельно
Виджет, который я сейчас реализую
Я хочу реализовать этот виджет блокировки экрана ios16
Я сделал почти все, но не смог реализовать прозрачную рамку маленького круга. Я не мог найти способ сделать прозрачным даже фон кольца за ним.
Мой код
struct RingTipShape: Shape { // small circle
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
let angle = CGFloat((240 * currentPercentage) * .pi / 180)
let controlRadius: CGFloat = rect.width / 2 - thickness / 2
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let x = center.x + controlRadius * cos(angle)
let y = center.y + controlRadius * sin(angle)
let pointCenter = CGPoint(x: x, y: y)
path.addEllipse(in:
CGRect(
x: pointCenter.x - thickness / 2,
y: pointCenter.y - thickness / 2,
width: thickness,
height: thickness
)
)
return path
}
var animatableData: Double {
get { return currentPercentage }
set { currentPercentage = newValue }
}
}
struct RingShape: Shape {
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.width / 2, y: rect.height / 2), radius: rect.width / 2 - (thickness / 2), startAngle: Angle(degrees: 0), endAngle: Angle(degrees: currentPercentage * 240), clockwise: false)
return path.strokedPath(.init(lineWidth: thickness, lineCap: .round, lineJoin: .round))
}
var animatableData: Double {
get { return currentPercentage}
set { currentPercentage = newValue}
}
}
struct CircularWidgetView: View { // My customizing widget view
@State var percentage: Double = 1.0
var body: some View {
GeometryReader { geo in
ZStack {
RingBackgroundShape(thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.21))
RingShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.385))
RingTipShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white)
/*
I want to make RingTipShape completely
transparent. Ignoring even the RingShape behind it
*/
VStack(spacing: 4) {
Image(systemName: "scooter")
.resizable()
.frame(width: 24, height: 24)
Text("hello")
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
}
}
}
Как я могу сделать прозрачную рамку, которая также игнорирует фон представления за ней?
Проблем с использованием манометра нет. Это потому, что я думаю, что было бы неплохо реализовать виджеты, которые я хочу сам. Кроме того, потому что у него нет калибровочного пользовательского интерфейса, который я хочу.
Я полностью уважаю причину (и согласен с тем, что этот опыт бесценен), поэтому попытался опубликовать ответ, имея это в виду. Надеюсь, это помогло!
Это отличное упражнение. Недостающая часть - маска.
Примечание. Несмотря на то, что существует множество способов улучшить существующий код, я постараюсь придерживаться исходного решения, так как смысл в том, чтобы набираться опыта на практике (на основе комментариев). Однако в конце я поделюсь некоторыми советами.
Таким образом, мы можем думать об этом в два этапа:
RingTipShape
в том же (по центру) положении, что и наш существующий, но немного больше.Первый пункт прост, нам просто нужно определить внешнюю толщину, чтобы разместить эллипс поверх дорожки в правильном месте:
struct RingTipShape: Shape { // small circle
//...
let outerThickness: CGFloat
//...
let controlRadius: CGFloat = rect.width / 2 - outerThickness / 2
//...
}
тогда наш существующий код изменится на:
RingTipShape(currentPercentage: percentage, thickness: 5.5, outerThickness: 5.5)
теперь для второй части нам нужно что-то, чтобы создать больший круг, что легко:
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
Итак, теперь в заключительной части мы собираемся использовать эту (более крупную) форму, чтобы создать своего рода перевернутую маску:
private var thumbMask: some View {
ZStack {
Color.white // This part will be transparent
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
.fill(Color.black) // This will be masked out
.rotationEffect(Angle(degrees: 150))
}
.compositingGroup() // Rasterize the shape
.luminanceToAlpha() // Map luminance to alpha values
}
и мы применяем маску следующим образом:
RingShape(currentPercentage: percentage, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.foregroundColor(.white.opacity(0.385))
.mask(thumbMask)
что приводит к этому:
Некоторые наблюдения/советы:
GeometryReader
(и все модификаторы кадров) в вашем CircularWidgetView
, ZStack
предложит все доступное пространство для просмотра..aspectRatio(contentMode: .fit)
к своему изображению, чтобы избежать растяжения.Например:
struct MyGauge: View {
let value: Double = 0.5
let range = 0.1...0.9
var body: some View {
ZStack {
// Backing track
track().opacity(0.2)
// Value track
track(showsProgress: true)
}
}
private var mappedValue: Double {
(range.upperBound + range.lowerBound) * value
}
private func track(showsProgress: Bool = false) -> some View {
Circle()
.trim(from: range.lowerBound, to: showsProgress ? mappedValue : range.upperBound)
.stroke(.white, style: .init(lineWidth: 5.5, lineCap: .round))
.rotationEffect(.radians(Double.pi / 2))
}
}
приведет к:
что немного упрощает работу, используя модификатор trim
.
Я надеюсь, что это имеет смысл.
Я решил это отлично. Я многому научился благодаря вам. Есть части, которые я не понимаю. Я не уверен, зачем вам нужно .luminanceToAlpha()
в thumMask()
. И очень сложно понять поведение, когда .luminanceToAlpha()
и .mask()
используются вместе.
Я не знал, что маска также применяет прозрачность. Спасибо, все мои сомнения развеялись. Большое спасибо!
Просто чтобы убедиться... есть ли причина, по которой вы не хотите использовать Датчик?