web-dev-qa-db-ja.com

UIBezierPath:角が丸いビューの周りに境界線を追加する方法は?

UIBezierPathを使用してimageviewの角を丸くしていますが、imageviewに境界線も追加したいと思います。上部がuiimageで下部がラベルであることを覚えておいてください。

現在このコードを使用すると、以下が生成されます。

let rectShape = CAShapeLayer()
rectShape.bounds = myCell2.NewFeedImageView.frame
rectShape.position = myCell2.NewFeedImageView.center
rectShape.path = UIBezierPath(roundedRect: myCell2.NewFeedImageView.bounds,
    byRoundingCorners: .TopRight | .TopLeft,
    cornerRadii: CGSize(width: 25, height: 25)).CGPath
myCell2.NewFeedImageView.layer.mask = rectShape

current

緑色の枠を追加したいのですが、使用できません

myCell2.NewFeedImageView.layer.borderWidth = 8
myCell2.NewFeedImageView.layer.borderColor = UIColor.greenColor().CGColor

この画像のように、境界線の左上隅と右上隅が切り取られているためです。

issue

私の現在のコードと一緒にUIBezierPathを使用して境界線を追加する方法もありますか?

18
gooberboobbutt

UIBezierPathパスを再利用して、シェイプレイヤーをビューに追加できます。以下は、ビューコントローラ内の例です。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create a view with red background for demonstration
        let v = UIView(frame: CGRectMake(0, 0, 100, 100))
        v.center = view.center
        v.backgroundColor = UIColor.redColor()
        view.addSubview(v)

        // Add rounded corners
        let maskLayer = CAShapeLayer()
        maskLayer.frame = v.bounds
        maskLayer.path = UIBezierPath(roundedRect: v.bounds, byRoundingCorners: .TopRight | .TopLeft, cornerRadii: CGSize(width: 25, height: 25)).CGPath
        v.layer.mask = maskLayer

        // Add border
        let borderLayer = CAShapeLayer()
        borderLayer.path = maskLayer.path // Reuse the Bezier path
        borderLayer.fillColor = UIColor.clearColor().CGColor
        borderLayer.strokeColor = UIColor.greenColor().CGColor
        borderLayer.lineWidth = 5
        borderLayer.frame = v.bounds
        v.layer.addSublayer(borderLayer)   
    }

}

最終結果は次のようになります。

Simulator screenshot

これは、ビューのサイズが固定されている場合にのみ期待どおりに機能することに注意してください。ビューのサイズを変更できる場合は、カスタムビュークラスを作成し、layoutSubviewsのレイヤーのサイズを変更する必要があります。

48
hennes

上記のように:

これを完璧に行うのは簡単ではありません。

これがドロップインソリューションです。


この

  • 描画している問題に正しく対処します[〜#〜] half [〜#〜]OFボーダーライン

  • 自動レイアウトで完全に使用可能です

  • ビューのサイズが変更された場合またはアニメーション化された場合に、完全に再調整されます。

  • 完全にIBDesignableです-ストーリーボードでリアルタイムに確認できます

2019のために...

@IBDesignable
class RoundedCornersAndTrueBorder: UIView {
    @IBInspectable var cornerRadius: CGFloat = 10 {
        didSet { setup() }
    }
    @IBInspectable var borderColor: UIColor = UIColor.black {
        didSet { setup() }
    }
    @IBInspectable var trueBorderWidth: CGFloat = 2.0 {
        didSet { setup() }
    }

    override func layoutSubviews() {
        setup()
    }

    var border:CAShapeLayer? = nil

    func setup() {
        // make a path with round corners
        let path = UIBezierPath(
          roundedRect: self.bounds, cornerRadius:cornerRadius)

        // note that it is >exactly< the size of the whole view

        // mask the whole view to that shape
        // note that you will ALSO be masking the border we'll draw below
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        self.layer.mask = mask

        // add another layer, which will be the border as such

        if (border == nil) {
            border = CAShapeLayer()
            self.layer.addSublayer(border!)
        }
        // IN SOME APPROACHES YOU would INSET THE FRAME
        // of the border-drawing layer by the width of the border
        // border.frame = bounds.insetBy(dx: borderWidth, dy: borderWidth)
        // so that when you draw the line, ALL of the WIDTH of the line
        // DOES fall within the actual mask.

        // here, we will draw the border-line LITERALLY ON THE Edge
        // of the path. that means >HALF< THE LINE will be INSIDE
        // the path and HALF THE LINE WILL BE OUTSIDE the path
        border!.frame = bounds
        let pathUsingCorrectInsetIfAny =
          UIBezierPath(roundedRect: border!.bounds, cornerRadius:cornerRadius)

        border!.path = pathUsingCorrectInsetIfAny.cgPath
        border!.fillColor = UIColor.clear.cgColor

        // the following is not what you want:
        // it results in "half-missing corners"
        // (note however, sometimes you do use this approach):
        //border.borderColor = borderColor.cgColor
        //border.borderWidth = borderWidth

        // this approach will indeed be "inside" the path:
        border!.strokeColor = borderColor.cgColor
        border!.lineWidth = trueBorderWidth * 2.0
        // HALF THE LINE will be INSIDE the path and HALF THE LINE
        // WILL BE OUTSIDE the path. so MAKE IT >>TWICE AS THICK<<
        // as requested by the consumer class.

    }
}

以上です。


コメント内の質問に対する初心者ヘルプ...

  1. 「Fattie.Swift」と呼ばれる「新しいSwiftファイル)を作成します。(おかしなことに、実際にはそれが何と呼んでも違いはありません。「わからない」の段階にいる場合新しいファイルを作成する方法」で基本的なXcodeチュートリアルを探してください。)

  2. 上記のコードをすべてファイルに入れます

  3. プロジェクトにクラス「RoundedCornersAndTrueBorder」を追加しました。

  4. あなたのストーリーボード。 通常のUIViewをシーンに追加します。実際、実際のサイズや形は何でも好きなようにしてください。

  5. Identity Inspectorを見てください。 (それが何であるかわからない場合は、基本的なチュートリアルを探してください。)クラスを「RoundedCornersAndTrueBorder」に変更するだけです。 (「Roun ...」と入力し始めると、どのクラスを意味するか推測します。

  6. これで完了です-プロジェクトを実行してください。

もちろん、Xcodeで行うこととまったく同じように、UIViewに完全で正しい制約を追加する必要があることに注意してください。楽しい!

同様のソリューション:

https://stackoverflow.com/a/57465440/294884 -画像+丸め+影
https://stackoverflow.com/a/41553784/294884 -2つのコーナーの問題
https://stackoverflow.com/a/59092828/294884 -「シャドウ+ホール」または「グローボックス」の問題
https://stackoverflow.com/a/57400842/294884 -「境界線とギャップ」の問題
https://stackoverflow.com/a/57514286/294884 -基本的な「追加」ベジエ

6
Fattie

絶対的に完璧な2019ソリューション

enter image description here

さらに面倒なことはありませんが、これはまさにこれを行う方法です。

  1. ビューに付属している「基本」レイヤーを実際に使用しないでください
  2. 新しいレイヤーを画像のみに作成します。これで、次のレイヤーに影響を与えずにこれを(循環的に)マスクできます
  3. そのようなボーダーのための新しいレイヤーを作成します。それは画像レイヤーによって安全にマスクされません。

重要な事実は

  1. CALayerを使用すると、確かに.maskを適用でき、そのレイヤーにのみ影響します
  2. 円(または実際に境界線)を描くときは、「幅の半分」しか得られないという事実に非常に注意深く注意する必要があります。つまり、決してsameパスを使用して切り取りますdrawで。
  3. 元の猫の画像の幅は、黄色の水平矢印とまったく同じです。 whole画像がラウンデルに表示されるように、画像をペイントするように注意する必要がありますこれは、全体のカスタムコントロールよりも小さい

だから、通常の方法でセットアップ

import UIKit

@IBDesignable class GreenCirclePerson: UIView {

    @IBInspectable var borderColor: UIColor = UIColor.black { didSet { setup() } }
    @IBInspectable var trueBorderThickness: CGFloat = 2.0 { didSet { setup() } }
    @IBInspectable var trueGapThickness: CGFloat = 2.0 { didSet { setup() } }

    @IBInspectable var picture: UIImage? = nil { didSet { setup() } }

    override func layoutSubviews() { setup() }

    var imageLayer: CALayer? = nil
    var border: CAShapeLayer? = nil

    func setup() {

        if (imageLayer == nil) {
            imageLayer = CALayer()
            self.layer.addSublayer(imageLayer!)
        }
        if (border == nil) {
            border = CAShapeLayer()
            self.layer.addSublayer(border!)
        }

次に、円形に切り抜いた画像のレイヤーを注意深く作成します。

        // the ultimate size of our custom control:
        let box = self.bounds.aspectFit()

        let totalInsetOnAnyOneSide = trueBorderThickness + trueGapThickness

        let boxInWhichImageSits = box.inset(by:
           UIEdgeInsets(top: totalInsetOnAnyOneSide, left: totalInsetOnAnyOneSide,
           bottom: totalInsetOnAnyOneSide, right: totalInsetOnAnyOneSide))

        // just a note. that version of inset#by is much clearer than the
        // confusing dx/dy variant, so best to use that one

        imageLayer!.frame = boxInWhichImageSits
        imageLayer!.contents = picture?.cgImage
        imageLayer?.contentsGravity = .resizeAspectFill

        let halfImageSize = boxInWhichImageSits.width / 2.0

        let maskPath = UIBezierPath(roundedRect: imageLayer!.bounds,
           cornerRadius:halfImageSize)
        let maskLayer = CAShapeLayer()
        maskLayer.path = maskPath.cgPath
        imageLayer!.mask = maskLayer

次に、完全に別のレイヤーとして、必要に応じて境界線を描画します。

        // now create the border

        border!.frame = bounds

        // To draw the border, you must inset it by half the width of the border,
        // otherwise you'll be drawing only half the border. (Indeed, as an additional
        // subtle problem you are clipping rather than rendering the outside Edge.)

        let halfWidth = trueBorderThickness / 2.0
        let borderCenterlineBox = box.inset(by:
            UIEdgeInsets(top: halfWidth, left: halfWidth,
            bottom: halfWidth, right: halfWidth))

        let halfBorderBoxSize = borderCenterlineBox.width / 2.0

        let borderPath = UIBezierPath(roundedRect: borderCenterlineBox,
          cornerRadius:halfBorderBoxSize)

        border!.path = borderPath.cgPath
        border!.fillColor = UIColor.clear.cgColor

        border!.strokeColor = borderColor.cgColor
        border!.lineWidth = trueBorderThickness
    }
}

すべてがiOS標準コントロールのように完全に機能します。

enter image description here

見えないものはすべて見えない。カスタムコントロール全体を背後のマテリアルまで透視できます。「半分の厚さ」の問題や画像マテリアルの欠落はなく、カスタムコントロールの背景色を通常の方法で設定できます。インスペクターコントロールはすべて正しく機能します。 (ふhe!)

同様のソリューション:

https://stackoverflow.com/a/57465440/294884 -画像+丸め+影
https://stackoverflow.com/a/41553784/294884 -2つのコーナーの問題
https://stackoverflow.com/a/59092828/294884 -「シャドウ+ホール」または「グローボックス」の問題
https://stackoverflow.com/a/57400842/294884 -「境界線とギャップ」の問題
https://stackoverflow.com/a/57514286/294884 -基本的な「追加」ベジエ

2
Fattie

確かにあります!すべてのビューにはlayerプロパティがあります(レイヤーに角を丸くするとわかる)。 layerのもう2つのプロパティは、borderColorborderWidthです。それらを設定するだけで、ビューに境界線を追加できます! (境界は丸みを帯びた角に続きます。)UIColor.CGColor for borderColor as a plain UIColor will not match the type。

0
Clay Ellis