Как расширить внешний край дуги UIBezierPath - PullRequest
0 голосов
/ 27 декабря 2018

Я делаю колесо в стиле Саймона Сайса с 4 UIBezierArcs.Я не могу просто сделать одну дугу с разными цветами и белыми частями, потому что мне нужно уметь определить, какая дуга была нажата.

Однако, когда я размещаю дуги по кругу, пустое пространство между внутренними краями становится меньше, чем белое пространство между внешними краями и делает пространства похожими на клин, а не на равномерный прямоугольник.

Как настроить внешний край дуги так, чтобы его начальный / конечный угол был длиннее внутреннего края дуги?

private struct Constants {
    static let width: CGFloat = 115;
    static let height: CGFloat = 230;
}

override func draw(_ rect: CGRect) {
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

    let radius: CGFloat = bounds.height

    let startAngle: CGFloat = 0 + .pi / 44
    let endAngle: CGFloat = .pi / 2 - .pi / 44

    shapePath = UIBezierPath(arcCenter: center,
                            radius: radius/2 - CGFloat(Constants.width/2),
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)

    shapePath.lineWidth = Constants.width / 2
    color.setStroke()
    shapePath.stroke()
    shapePath.close()
}

Вот как это выглядит в настоящее время:

what it looks like currently

Ответы [ 3 ]

0 голосов
/ 27 декабря 2018

Вы можете определить путь, который описывает четыре формы дуги для вашей диаграммы.И вам просто нужно сместить начальный / конечный угол каждой кривой на арксинус разрыва, деленного на радиус.Например,

let angleAdjustment = asin(gap / 2 / radius) 

Таким образом:

@IBDesignable
class SimonSaysView: UIView {

    @IBInspectable var innerPercentage: CGFloat = 0.5 { didSet { updatePaths() } }
    @IBInspectable var gap: CGFloat = 20              { didSet { updatePaths() } }

    let shapeLayers: [CAShapeLayer] = {
        let colors: [UIColor] = [.red, .blue, .black, .green]
        return colors.map { color -> CAShapeLayer in
            let shapeLayer = CAShapeLayer()
            shapeLayer.strokeColor = UIColor.clear.cgColor
            shapeLayer.fillColor = color.cgColor
            shapeLayer.lineWidth = 0
            return shapeLayer
        }
    }()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        updatePaths()
    }

    func configure() {
        shapeLayers.forEach { layer.addSublayer($0) }
    }

    func updatePaths() {
        let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
        let outerRadius = min(bounds.width, bounds.height) / 2
        let innerRadius = outerRadius * innerPercentage
        let outerAdjustment = asin(gap / 2 / outerRadius)
        let innerAdjustment = asin(gap / 2 / innerRadius)

        for (i, shapeLayer) in shapeLayers.enumerated() {
            let startAngle: CGFloat = -3 * .pi / 4 + CGFloat(i) * .pi / 2
            let endAngle = startAngle + .pi / 2
            let path = UIBezierPath(arcCenter: arcCenter, radius: outerRadius, startAngle: startAngle + outerAdjustment, endAngle: endAngle - outerAdjustment, clockwise: true)
            path.addArc(withCenter: arcCenter, radius: innerRadius, startAngle: endAngle - innerAdjustment, endAngle: startAngle + innerAdjustment, clockwise: false)
            path.close()
            shapeLayer.path = path.cgPath
        }
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        for (index, shapeLayer) in shapeLayers.enumerated() {
            if shapeLayer.path!.contains(touch.location(in: self)) {
                print(index)
                return
            }
        }
    }
}

Выход:

enter image description here


Если вы 'Вам интересно, как я рассчитал, какой угол регулировки θ, должен быть для заданного смещения зазора x и заданного радиуса r, базовая тригонометрия говорит нам, что sin(θ) = x / r , и, таким образом, θ = asin (x / r) , где x - это общий разрыв, деленный на 2.

enter image description here

0 голосов
/ 27 декабря 2018

Итак, вы хотите это:

wedges with gaps

Давайте напишем расширение на UIBezierPath, которое создает путь с выделением одного клина.

Для разогрева сначала мы напишем функцию, которая создает путь клина, не оставляя зазора между клиньями:

import UIKit
import PlaygroundSupport

// This is useful to remind us that we measure angles in radians, not degrees.
typealias Radians = CGFloat

extension UIBezierPath {

    static func simonWedge(innerRadius: CGFloat, outerRadius: CGFloat, centerAngle: Radians) -> UIBezierPath {
        let innerAngle: Radians = CGFloat.pi / 4
        let outerAngle: Radians = CGFloat.pi / 4
        let path = UIBezierPath()
        path.addArc(withCenter: .zero, radius: innerRadius, startAngle: centerAngle - innerAngle, endAngle: centerAngle + innerAngle, clockwise: true)
        path.addArc(withCenter: .zero, radius: outerRadius, startAngle: centerAngle + outerAngle, endAngle: centerAngle - outerAngle, clockwise: false)
        path.close()
        return path
    }

}

С этим расширением мы можем создавать клины, как это:

wedge examples

И мы можем использовать это расширение в подклассе UIView для рисования клина:

class SimonWedgeView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        commonInit()
    }

    var centerAngle: Radians = 0 { didSet { setNeedsDisplay() } }
    var color: UIColor = #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1) { didSet { setNeedsDisplay() } }

    override func draw(_ rect: CGRect) {
        let path = wedgePath()
        color.setFill()
        path.fill()
    }

    private func commonInit() {
        contentMode = .redraw
        backgroundColor = .clear
        isOpaque = false
    }

    private func wedgePath() -> UIBezierPath {
        let bounds = self.bounds
        let outerRadius = min(bounds.size.width, bounds.size.height) / 2
        let innerRadius = outerRadius / 2
        let path = UIBezierPath.simonWedge(innerRadius: innerRadius, outerRadius: outerRadius, centerAngle: centerAngle)
        path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY))
        return path
    }
}

let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
rootView.backgroundColor = .white

func addWedgeView(color: UIColor, angle: Radians) {
    let wedgeView = SimonWedgeView(frame: rootView.bounds)
    wedgeView.color = color
    wedgeView.centerAngle = angle
    rootView.addSubview(wedgeView)
}

addWedgeView(color: #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1), angle: 0)
addWedgeView(color: #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1), angle: 0.5 * .pi)
addWedgeView(color: #colorLiteral(red: 0.2588235438, green: 0.7568627596, blue: 0.9686274529, alpha: 1), angle: .pi)
addWedgeView(color: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1), angle: 1.5 * .pi)

PlaygroundPage.current.liveView = rootView

Результат:

wedge views

Итак, теперь мы хотим добавить промежутки между клиньями.

Рассмотрим следующую диаграмму:

arc diagram

На диаграмме есть круг радиуса r (с центром в начале координат) и дуга этого круга, которая составляет угол θ.Длина дуги составляет θr, когда θ в радианах.(Эта формула, θr, поэтому мы используем радианы для измерения углов!)

В приведенном выше методе без зазоров θ (в качестве переменных innerAngle и outerAngle) было .pi / 4.Но теперь мы хотим, чтобы углы были меньше .pi / 4, чтобы образовался разрыв.Мы хотим, чтобы длина зазора по внутреннему радиусу была равна длине зазора по внешнему радиусу.Таким образом, у нас есть заранее заданная длина зазора, g, и нам нужно вычислить для него правильные θ.

gapless arc length = r π / 4
gapful arc length = θ r = r π / 4 - g / 2

(Мы используем g / 2, потому что каждый клин имеет половину зазора в одинконец и половина пробела на другом конце.)

θ r = r π / 4 - g / 2
// Solve for θ by dividing both sides by r:
θ = π / 4 - g / (2 r)

Теперь мы можем обновить формулы innerAngle и outerAngle в расширении, чтобы создать пути с пробелом:

static func simonWedge(innerRadius: CGFloat, outerRadius: CGFloat, centerAngle: Radians, gap: CGFloat) -> UIBezierPath {
    let innerAngle: Radians = CGFloat.pi / 4 - gap / (2 * innerRadius)
    let outerAngle: Radians = CGFloat.pi / 4 - gap / (2 * outerRadius)
    let path = UIBezierPath()
    path.addArc(withCenter: .zero, radius: innerRadius, startAngle: centerAngle - innerAngle, endAngle: centerAngle + innerAngle, clockwise: true)
    path.addArc(withCenter: .zero, radius: outerRadius, startAngle: centerAngle + outerAngle, endAngle: centerAngle - outerAngle, clockwise: false)
    path.close()
    return path
}

Затем мы обновляем wedgePath метод SimonWedgeView, чтобы вычислить и передать длину разрыва методу simonWidge:

    private func wedgePath() -> UIBezierPath {
        let bounds = self.bounds
        let outerRadius = min(bounds.size.width, bounds.size.height) / 2
        let innerRadius = outerRadius / 2
        let gap = (outerRadius - innerRadius) / 4
        let path = UIBezierPath.simonWedge(innerRadius: innerRadius, outerRadius: outerRadius, centerAngle: centerAngle, gap: gap)
        path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY))
        return path
    }

И мы получаем желаемый результат:

wedges with gaps

Вы можете найти полный исходный код игровой площадки (для версии с пробелами) в этом списке .

ByКстати, после того, как вы получите метод рисования, вы, вероятно, захотите определить, какой клин был повернут.Для этого вам нужно переопределить метод point(inside:with:) в SimonWedgeView.Я объясняю, что делать в этом ответе .

0 голосов
/ 27 декабря 2018

Вы не сможете использовать 4 толстых дуговых сечения.У них будут угловые концы.

Вместо этого вам нужно будет построить 4 отдельных заполненных многоугольника с 2 дугами и 2 отрезками линии каждый.

Чтобы понять это, потребуется некоторая алгебра 2 ималенький триг.

Представьте ограничивающий квадрат для вашего чертежа, у которого есть полосы заданной толщины, идущие от верхнего левого угла к правому нижнему углу и от правого верхнего угла к левому нижнему углу.Давайте назовем их перекладинами.Представьте, что вы рисуете эти перекладины с помощью маркера с прямоугольными краями, поэтому перекладины начинаются с каждого угла квадрата, а углы перекладин немного выступают за пределы ограничивающего квадрата.Нарисуйте его.

Теперь нарисуйте круги с внутренним и внешним диаметром внутри квадрата.

Каждый круг будет пересекать внешние линии каждого стержня.Вам нужно будет найти точки пересечения границ линии стержня и внутренних и внешних окружностей.Если толщина поперечин равна t, координаты углов стержней будут смещены от углов вашего квадрата на t•√2/4.Вам нужно было бы построить диаграммы и наметить координаты внешних линий перекладин.Затем запишите конечные точки каждой перекладины и рассчитайте уравнение внешних линий.(В форме пересечения с уклоном или в стандартной форме. Стандартная форма позволяет вам иметь дело с вертикальными линиями, но в вашем случае не будет вертикальных линий.

Формула для кругов: r² = (x-h)² + (y-k)². Вы должны бытьвозможность использовать одновременные уравнения для каждой окружности и каждой граничной линии стержня, чтобы найти пересечения внешней линии каждого стержня и каждого круга. Для каждой пары должно быть 2 решения.

Как только у вас есть координаты пересечений,вам придется использовать триг для вычисления начального и конечного углов каждой дуги. Затем вы просто будете использовать команды дуги UIBezierPath и линейные команды для сборки ваших фигур.

Это базовый подход. Разработка деталейявляется суетливым и трудоемким, но простым. Я оставлю это как «упражнение для читателя».

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...