Swiftgram/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift
2020-12-15 02:12:49 +04:00

790 lines
30 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import LegacyComponents
import AnimatedCountLabelNode
private let blue = UIColor(rgb: 0x0078ff)
private let lightBlue = UIColor(rgb: 0x59c7f8)
private let green = UIColor(rgb: 0x33c659)
private let activeBlue = UIColor(rgb: 0x00a0b9)
private class CallStatusBarBackgroundNode: ASDisplayNode {
private let foregroundView: UIView
private let foregroundGradientLayer: CAGradientLayer
private let maskCurveView: VoiceCurveView
var audioLevel: Float = 0.0 {
didSet {
self.maskCurveView.updateLevel(CGFloat(audioLevel))
}
}
var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) {
didSet {
if self.connectingColor.rgb != oldValue.rgb {
self.updateGradientColors()
}
}
}
var speaking: Bool? = nil {
didSet {
if self.speaking != oldValue {
self.updateGradientColors()
}
}
}
private func updateGradientColors() {
let initialColors = self.foregroundGradientLayer.colors
let targetColors: [CGColor]
if let speaking = self.speaking {
targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor]
} else {
targetColors = [connectingColor.cgColor, connectingColor.cgColor]
}
self.foregroundGradientLayer.colors = targetColors
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
}
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
override init() {
self.foregroundView = UIView()
self.foregroundGradientLayer = CAGradientLayer()
self.maskCurveView = VoiceCurveView(frame: CGRect(), maxLevel: 1.5, smallCurveRange: (0.0, 0.0), mediumCurveRange: (0.1, 0.55), bigCurveRange: (0.1, 1.0))
self.maskCurveView.setColor(UIColor(rgb: 0xffffff))
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
self.addSubnode(self.hierarchyTrackingNode)
self.foregroundGradientLayer.colors = [blue.cgColor, lightBlue.cgColor]
self.foregroundGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
self.foregroundGradientLayer.endPoint = CGPoint(x: 2.0, y: 0.5)
self.foregroundView.mask = self.maskCurveView
self.isOpaque = false
self.updateAnimations()
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
strongSelf.updateAnimations()
}
}
}
override func didLoad() {
super.didLoad()
self.view.addSubview(self.foregroundView)
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
}
override func layout() {
super.layout()
CATransaction.begin()
CATransaction.setDisableActions(true)
if self.maskCurveView.frame != self.bounds {
self.foregroundView.frame = self.bounds
self.foregroundGradientLayer.frame = self.bounds
self.maskCurveView.frame = self.bounds
}
CATransaction.commit()
}
private func setupGradientAnimations() {
return
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.foregroundGradientLayer.startPoint
let newValue: CGPoint
if self.maskCurveView.presentationAudioLevel > 0.1 {
newValue = CGPoint(x: CGFloat.random(in: 1.0 ..< 1.3), y: 0.5)
} else {
newValue = CGPoint(x: CGFloat.random(in: 0.85 ..< 1.2), y: 0.5)
}
self.foregroundGradientLayer.startPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "endPoint")
animation.duration = Double.random(in: 0.8 ..< 1.4)
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.foregroundGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
}
func updateAnimations() {
if !isCurrentlyInHierarchy {
self.foregroundGradientLayer.removeAllAnimations()
self.maskCurveView.stopAnimating()
return
}
self.setupGradientAnimations()
self.maskCurveView.startAnimating()
}
}
public class CallStatusBarNodeImpl: CallStatusBarNode {
public enum Content {
case call(SharedAccountContext, Account, PresentationCall)
case groupCall(SharedAccountContext, Account, PresentationGroupCall)
}
private let backgroundNode: CallStatusBarBackgroundNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateAnimatedCountLabelNode
private let speakerNode: ImmediateTextNode
private let audioLevelDisposable = MetaDisposable()
private let stateDisposable = MetaDisposable()
private var didSetupData = false
private var currentSize: CGSize?
private var currentContent: Content?
private var presentationData: PresentationData?
private let presentationDataDisposable = MetaDisposable()
private var currentPeer: Peer?
private var currentCallTimer: SwiftSignalKit.Timer?
private var currentCallState: PresentationCallState?
private var currentGroupCallState: PresentationGroupCallSummaryState?
private var currentIsMuted = true
private var currentMembers: PresentationGroupCallMembers?
private var currentIsConnected = true
public override init() {
self.backgroundNode = CallStatusBarBackgroundNode()
self.titleNode = ImmediateTextNode()
self.subtitleNode = ImmediateAnimatedCountLabelNode()
self.subtitleNode.reverseAnimationDirection = true
self.speakerNode = ImmediateTextNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.speakerNode)
}
deinit {
self.presentationDataDisposable.dispose()
self.audioLevelDisposable.dispose()
self.stateDisposable.dispose()
self.currentCallTimer?.invalidate()
}
public func update(content: Content) {
self.currentContent = content
self.update()
}
public override func update(size: CGSize) {
self.currentSize = size
self.update()
}
private func update() {
guard let size = self.currentSize, let content = self.currentContent else {
return
}
let wasEmpty = (self.titleNode.attributedText?.string ?? "").isEmpty
if !self.didSetupData {
self.didSetupData = true
switch content {
case let .call(sharedContext, account, call):
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.stateDisposable.set(
(combineLatest(
account.postbox.loadedPeerWithId(call.peerId),
call.state,
call.isMuted
)
|> deliverOnMainQueue).start(next: { [weak self] peer, state, isMuted in
if let strongSelf = self {
strongSelf.currentPeer = peer
strongSelf.currentCallState = state
strongSelf.currentIsMuted = isMuted
let currentIsConnected: Bool
switch state.state {
case .active, .terminating, .terminated:
currentIsConnected = true
default:
currentIsConnected = false
}
strongSelf.currentIsConnected = currentIsConnected
strongSelf.update()
}
}))
self.audioLevelDisposable.set((call.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] audioLevel in
guard let strongSelf = self else {
return
}
strongSelf.backgroundNode.audioLevel = audioLevel
}))
case let .groupCall(sharedContext, account, call):
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.presentationDataDisposable.set((sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.update()
}
}))
self.stateDisposable.set(
(combineLatest(
account.postbox.peerView(id: call.peerId),
call.summaryState,
call.isMuted,
call.members
)
|> deliverOnMainQueue).start(next: { [weak self] view, state, isMuted, members in
if let strongSelf = self {
strongSelf.currentPeer = view.peers[view.peerId]
strongSelf.currentGroupCallState = state
strongSelf.currentMembers = members
var isMuted = isMuted
if let state = state, let muteState = state.callState.muteState {
if !muteState.canUnmute {
isMuted = true
}
}
strongSelf.currentIsMuted = isMuted
let currentIsConnected: Bool
if let state = state, case .connected = state.callState.networkState {
currentIsConnected = true
} else {
currentIsConnected = false
}
strongSelf.currentIsConnected = currentIsConnected
strongSelf.update()
}
}))
self.audioLevelDisposable.set((combineLatest(call.myAudioLevel, .single([]) |> then(call.audioLevels))
|> deliverOnMainQueue).start(next: { [weak self] myAudioLevel, audioLevels in
guard let strongSelf = self else {
return
}
var effectiveLevel: Float = 0.0
var audioLevels = audioLevels
if !strongSelf.currentIsMuted {
audioLevels.append((PeerId(0), myAudioLevel, true))
}
effectiveLevel = audioLevels.map { $0.1 }.max() ?? 0.0
strongSelf.backgroundNode.audioLevel = effectiveLevel
}))
}
}
var title: String = ""
var speakerSubtitle: String = ""
let textFont = Font.regular(13.0)
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
if let presentationData = self.presentationData {
if let currentPeer = self.currentPeer {
title = currentPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
var membersCount: Int32?
if let groupCallState = self.currentGroupCallState {
membersCount = Int32(max(1, groupCallState.participantCount))
} else if let content = self.currentContent, case .groupCall = content {
membersCount = 1
}
var speakingPeer: Peer?
if let members = currentMembers {
var speakingPeers: [Peer] = []
for member in members.participants {
if members.speakingParticipants.contains(member.peer.id) {
speakingPeers.append(member.peer)
}
}
speakingPeer = speakingPeers.first
}
if let speakingPeer = speakingPeer {
speakerSubtitle = speakingPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
if let membersCount = membersCount {
var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount)
if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") {
membersPart.removeSubrange(startIndex ... endIndex)
}
let rawTextAndRanges = presentationData.strings.VoiceChat_Status_MembersFormat("\(membersCount)", membersPart)
let (rawText, ranges) = rawTextAndRanges
var textIndex = 0
var latestIndex = 0
for (index, range) in ranges {
var lowerSegmentIndex = range.lowerBound
if index != 0 {
lowerSegmentIndex = min(lowerSegmentIndex, latestIndex)
} else {
if latestIndex < range.lowerBound {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.lowerBound)])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
}
latestIndex = range.upperBound
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: range.upperBound)])
if index == 0 {
segments.append(.number(Int(membersCount), NSAttributedString(string: part, font: textFont, textColor: textColor)))
} else {
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
}
if latestIndex < rawText.count {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: textColor)))
textIndex += 1
}
}
let sourceColor = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor
let color: UIColor
if sourceColor.alpha < 1.0 {
color = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor.mixedWith(sourceColor.withAlphaComponent(1.0), alpha: sourceColor.alpha)
} else {
color = sourceColor
}
self.backgroundNode.connectingColor = color
}
if self.subtitleNode.segments != segments && speakerSubtitle.isEmpty {
self.subtitleNode.segments = segments
}
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.subtitleNode, alpha: !speakerSubtitle.isEmpty ? 0.0 : 1.0)
alphaTransition.updateAlpha(node: self.speakerNode, alpha: !speakerSubtitle.isEmpty ? 1.0 : 0.0)
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white)
if !speakerSubtitle.isEmpty {
self.speakerNode.attributedText = NSAttributedString(string: speakerSubtitle, font: Font.regular(13.0), textColor: .white)
}
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.updateLayout(CGSize(width: 150.0, height: size.height))
let subtitleSize = self.subtitleNode.updateLayout(size: CGSize(width: 150.0, height: size.height), animated: true)
let speakerSize = self.speakerNode.updateLayout(CGSize(width: 150.0, height: size.height))
let totalWidth = titleSize.width + spacing + subtitleSize.width
let horizontalOrigin: CGFloat = floor((size.width - totalWidth) / 2.0)
let contentHeight: CGFloat = 24.0
let verticalOrigin: CGFloat = size.height - contentHeight
let transition: ContainedViewLayoutTransition = wasEmpty ? .immediate : .animated(duration: 0.2, curve: .easeInOut)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin + floor((contentHeight - titleSize.height) / 2.0)), size: titleSize))
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - subtitleSize.height) / 2.0)), size: subtitleSize))
if !speakerSubtitle.isEmpty {
self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize)
}
self.backgroundNode.speaking = self.currentIsConnected ? !self.currentIsMuted : nil
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
}
}
private final class VoiceCurveView: UIView {
private let smallCurve: CurveView
private let mediumCurve: CurveView
private let bigCurve: CurveView
private let maxLevel: CGFloat
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
private var audioLevel: CGFloat = 0.0
var presentationAudioLevel: CGFloat = 0.0
private(set) var isAnimating = false
public typealias CurveRange = (min: CGFloat, max: CGFloat)
public init(
frame: CGRect,
maxLevel: CGFloat,
smallCurveRange: CurveRange,
mediumCurveRange: CurveRange,
bigCurveRange: CurveRange
) {
self.maxLevel = maxLevel
self.smallCurve = CurveView(
pointsCount: 8,
minRandomness: 1,
maxRandomness: 1.3,
minSpeed: 0.9,
maxSpeed: 3.2,
minOffset: smallCurveRange.min,
maxOffset: smallCurveRange.max
)
self.mediumCurve = CurveView(
pointsCount: 8,
minRandomness: 1.2,
maxRandomness: 1.5,
minSpeed: 1.0,
maxSpeed: 4.4,
minOffset: mediumCurveRange.min,
maxOffset: mediumCurveRange.max
)
self.bigCurve = CurveView(
pointsCount: 8,
minRandomness: 1.2,
maxRandomness: 1.7,
minSpeed: 1.0,
maxSpeed: 5.8,
minOffset: bigCurveRange.min,
maxOffset: bigCurveRange.max
)
super.init(frame: frame)
addSubview(bigCurve)
addSubview(mediumCurve)
addSubview(smallCurve)
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
guard let strongSelf = self else { return }
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
strongSelf.smallCurve.level = strongSelf.presentationAudioLevel
strongSelf.mediumCurve.level = strongSelf.presentationAudioLevel
strongSelf.bigCurve.level = strongSelf.presentationAudioLevel
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setColor(_ color: UIColor) {
smallCurve.setColor(color.withAlphaComponent(1.0))
mediumCurve.setColor(color.withAlphaComponent(0.55))
bigCurve.setColor(color.withAlphaComponent(0.35))
}
public func updateLevel(_ level: CGFloat) {
let normalizedLevel = min(1, max(level / maxLevel, 0))
smallCurve.updateSpeedLevel(to: normalizedLevel)
mediumCurve.updateSpeedLevel(to: normalizedLevel)
bigCurve.updateSpeedLevel(to: normalizedLevel)
audioLevel = normalizedLevel
}
public func startAnimating() {
guard !isAnimating else { return }
isAnimating = true
updateCurvesState()
displayLinkAnimator?.isPaused = false
}
public func stopAnimating() {
self.stopAnimating(duration: 0.15)
}
public func stopAnimating(duration: Double) {
guard isAnimating else { return }
isAnimating = false
updateCurvesState()
displayLinkAnimator?.isPaused = true
}
private func updateCurvesState() {
if isAnimating {
if smallCurve.frame.size != .zero {
smallCurve.startAnimating()
mediumCurve.startAnimating()
bigCurve.startAnimating()
}
} else {
smallCurve.stopAnimating()
mediumCurve.stopAnimating()
bigCurve.stopAnimating()
}
}
override public func layoutSubviews() {
super.layoutSubviews()
smallCurve.frame = bounds
mediumCurve.frame = bounds
bigCurve.frame = bounds
updateCurvesState()
}
}
final class CurveView: UIView {
let pointsCount: Int
let smoothness: CGFloat
let minRandomness: CGFloat
let maxRandomness: CGFloat
let minSpeed: CGFloat
let maxSpeed: CGFloat
let minOffset: CGFloat
let maxOffset: CGFloat
var level: CGFloat = 0 {
didSet {
guard self.minOffset > 0.0 else {
return
}
CATransaction.begin()
CATransaction.setDisableActions(true)
let lv = minOffset + (maxOffset - minOffset) * level
shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0)
CATransaction.commit()
}
}
private var speedLevel: CGFloat = 0
private var lastSpeedLevel: CGFloat = 0
private let shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.strokeColor = nil
return layer
}()
private var transition: CGFloat = 0 {
didSet {
guard let currentPoints = currentPoints else { return }
shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness, curve: true).cgPath
}
}
override var frame: CGRect {
didSet {
if self.frame.size != oldValue.size {
self.fromPoints = nil
self.toPoints = nil
self.animateToNewShape()
}
}
}
private var fromPoints: [CGPoint]?
private var toPoints: [CGPoint]?
private var currentPoints: [CGPoint]? {
guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil }
return fromPoints.enumerated().map { offset, fromPoint in
let toPoint = toPoints[offset]
return CGPoint(
x: fromPoint.x + (toPoint.x - fromPoint.x) * transition,
y: fromPoint.y + (toPoint.y - fromPoint.y) * transition
)
}
}
init(
pointsCount: Int,
minRandomness: CGFloat,
maxRandomness: CGFloat,
minSpeed: CGFloat,
maxSpeed: CGFloat,
minOffset: CGFloat,
maxOffset: CGFloat
) {
self.pointsCount = pointsCount
self.minRandomness = minRandomness
self.maxRandomness = maxRandomness
self.minSpeed = minSpeed
self.maxSpeed = maxSpeed
self.minOffset = minOffset
self.maxOffset = maxOffset
self.smoothness = 0.35
super.init(frame: .zero)
layer.addSublayer(shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setColor(_ color: UIColor) {
shapeLayer.fillColor = color.cgColor
}
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
speedLevel = max(speedLevel, newSpeedLevel)
// if abs(lastSpeedLevel - newSpeedLevel) > 0.45 {
// animateToNewShape()
// }
}
func startAnimating() {
animateToNewShape()
}
func stopAnimating() {
fromPoints = currentPoints
toPoints = nil
pop_removeAnimation(forKey: "curve")
}
private func animateToNewShape() {
if pop_animation(forKey: "curve") != nil {
fromPoints = currentPoints
toPoints = nil
pop_removeAnimation(forKey: "curve")
}
if fromPoints == nil {
fromPoints = generateNextCurve(for: bounds.size)
}
if toPoints == nil {
toPoints = generateNextCurve(for: bounds.size)
}
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "curve.transition", initializer: { property in
property?.readBlock = { curveView, values in
guard let curveView = curveView as? CurveView, let values = values else { return }
values.pointee = curveView.transition
}
property?.writeBlock = { curveView, values in
guard let curveView = curveView as? CurveView, let values = values else { return }
curveView.transition = values.pointee
}
}) as? POPAnimatableProperty
animation.completionBlock = { [weak self] animation, finished in
if finished {
self?.fromPoints = self?.currentPoints
self?.toPoints = nil
self?.animateToNewShape()
}
}
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.fromValue = 0
animation.toValue = 1
pop_add(animation, forKey: "curve")
lastSpeedLevel = speedLevel
speedLevel = 0
}
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
return curve(pointsCount: pointsCount, randomness: randomness).map {
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 18.0 + $0.y * 12.0)
}
}
private func curve(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
let segment = 1.0 / CGFloat(pointsCount - 1)
let rgen = { () -> CGFloat in
let accuracy: UInt32 = 1000
let random = arc4random_uniform(accuracy)
return CGFloat(random) / CGFloat(accuracy)
}
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
let points = (0 ..< pointsCount).map { i -> CGPoint in
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
let segmentRandomness: CGFloat = randomness
let pointX: CGFloat
let pointY: CGFloat
let randomXDelta: CGFloat
if i == 0 {
pointX = 0.0
pointY = 0.0
randomXDelta = 0.0
} else if i == pointsCount - 1 {
pointX = 1.0
pointY = 0.0
randomXDelta = 0.0
} else {
pointX = segment * CGFloat(i)
pointY = ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset
randomXDelta = segment - segment * randPointOffset
}
return CGPoint(x: pointX + randomXDelta, y: pointY)
}
return points
}
override func layoutSubviews() {
super.layoutSubviews()
CATransaction.begin()
CATransaction.setDisableActions(true)
shapeLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
shapeLayer.bounds = self.bounds
CATransaction.commit()
}
}