Skip to content

Commit

Permalink
尝试翻页动画,太难了,想实现就必须同时有新旧layer,这就涉及layer的深拷贝了,但是CALayer是不支持深拷贝的,自己手动拷贝属性…
Browse files Browse the repository at this point in the history
…layer又不支持富文本,拷贝出来效果也跟原版有差别,本来快放弃了,又心又不甘于是用一整晚上时间了一个实验品,实测旧layer显示效果跟原版有差异还不是最大的问题,衔接不流畅才是,因为layer运动后最终会停留在父视图下,往上退场动画结束后旧layer会留在panel范围里跟视图layer重叠,还有打断动画也是个问题,因为0.2秒时间内其实很容易形成连按下键,后面再考虑吧,作为实验品勉强成功了,后面效果可能不会太好
  • Loading branch information
吕小布 committed Dec 18, 2024
1 parent b4b5a7b commit 18fa2f9
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 67 deletions.
148 changes: 112 additions & 36 deletions sources/AnimateNSTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AnimateNSTextView: NSTextField {
private var animationType:CAMediaTimingFunctionName = .easeOut //这个类型不能乱设,要通过Str间接设置
var animationDuration: Double = 0.2
var animationInterruptType = "smooth"
private var oldTextLayer = CATextLayer()

//转换传入的字符串为CAMediaTimingFunctionName格式
var animationTypeStr: String = "easeOut" {
Expand All @@ -35,57 +36,132 @@ class AnimateNSTextView: NSTextField {
}
}

var isTurning = 0 // 0:不翻页 1:往下翻页 -1 往上翻页
var page = 0 {
didSet{
if page == oldValue{
isTurning = 0
}else if page > oldValue{
isTurning = 1
}else {
isTurning = -1
}
}
}

override var frame: NSRect {
didSet {
// 仅当位置改变时触发动画
if animationOn{
if oldValue == NSRect(x: 0, y: 0, width: 0, height: 0){//跳过第一次动画防止飞入效果
return
}
if isTurning == 0{
animateInPage(oldValue, frame)
}
}
}

override var attributedStringValue: NSAttributedString {
willSet {

// 复制 CATextLayer 特有的属性
oldTextLayer.frame = self.frame
if let attributedString = self.attributedStringValue as CFAttributedString? {
oldTextLayer.string = attributedString
print("拷贝富文本成功")
}else{
print("拷贝富文本失败")
}

if isTurning == 1{
animateCrossPage()
}else if isTurning == -1{
animateCrossPage()
}
}
}




//翻页动画
func animateCrossPage(){
print("准备执行翻页动画")
oldTextLayer.isHidden = false
// 文本已经改变,现在可以执行动画了
if let newTextLayer = self.layer{
print("开始翻页动画")
oldTextLayer.frame = newTextLayer.frame
// 确保oldTextLayer在正确的层级
// self.layer?.addSublayer(oldTextLayer)
// 将CATextLayer添加到视图层级中,作为NSTextField的兄弟layer
if let textFieldSuperview = self.superview {
textFieldSuperview.layer?.addSublayer(oldTextLayer)
}

print("oldTextLayer.position.y",oldTextLayer.position.y)
print("newTextLayer.position.y",newTextLayer.position.y)

// 旧layer(复制layer)退出动画
let oldLayerAnimation = CABasicAnimation(keyPath: "position.y")
oldLayerAnimation.fromValue = self.frame.midY
oldLayerAnimation.toValue = self.frame.midY + self.frame.height*CGFloat(isTurning) // 向上移动20个单位
oldLayerAnimation.duration = 0.2
oldLayerAnimation.fillMode = .forwards
oldLayerAnimation.isRemovedOnCompletion = true
oldTextLayer.add(oldLayerAnimation, forKey: nil)


// 新layer(自有layer)入场动画
let layerAnimation = CABasicAnimation(keyPath: "position.y")
layerAnimation.fromValue = self.frame.origin.y - self.frame.height*CGFloat(isTurning)
layerAnimation.toValue = self.frame.origin.y // 向上移动20个单位
layerAnimation.duration = 0.2
// layerAnimation.fillMode = .forwards
layerAnimation.isRemovedOnCompletion = true
newTextLayer.add(layerAnimation, forKey: nil)



// 动画完成后,移除oldTextLayer
DispatchQueue.main.asyncAfter(deadline: .now() + 0.17) {
self.oldTextLayer.isHidden = true
}
}
}

//不翻页位移动画
func animateInPage(_ oldFrame:NSRect, _ newFrame:NSRect){
// 仅当位置改变时触发动画
if animationOn{
if oldFrame == NSRect(x: 0, y: 0, width: 0, height: 0){//跳过第一次动画防止飞入效果
return
}
// // 确保在动画之前更新layer的position
// CATransaction.begin()
// CATransaction.setDisableActions(true)
// self.layer?.position = CGPoint(x: frame.origin.x, y: frame.origin.y)
// CATransaction.commit()
// 创建一个动画,将视图从当前位置平滑移动到新位置
let animation = CABasicAnimation(keyPath: "position")
if animationInterruptType == "smooth"{
animation.fromValue = self.layer?.presentation()?.position ?? NSValue(point: CGPoint(x: oldValue.origin.x, y: oldValue.origin.y))
}else if animationInterruptType == "interrupt"{
animation.fromValue = NSValue(point: CGPoint(x: oldValue.origin.x, y: oldValue.origin.y))
}
animation.toValue = NSValue(point: CGPoint(x: frame.origin.x, y: frame.origin.y))
animation.duration = animationDuration // 动画持续时间
animation.timingFunction = CAMediaTimingFunction(name: animationType) // 动画缓动函数

// 创建一个动画,将视图从当前位置平滑移动到新位置
let animation = CABasicAnimation(keyPath: "position")
if animationInterruptType == "smooth"{
animation.fromValue = self.layer?.presentation()?.position ?? NSValue(point: CGPoint(x: oldFrame.origin.x, y: oldFrame.origin.y))
}else if animationInterruptType == "interrupt"{
animation.fromValue = NSValue(point: CGPoint(x: oldFrame.origin.x, y: oldFrame.origin.y))
}
animation.toValue = NSValue(point: CGPoint(x: newFrame.origin.x, y: newFrame.origin.y))
animation.duration = animationDuration // 动画持续时间
animation.timingFunction = CAMediaTimingFunction(name: animationType) // 动画缓动函数

// // 如果当前有动画在运行,从当前动画位置开始新的动画
// if animationInterruptType == "smooth"{
// print("animationInterruptType",animationInterruptType)
// if ((self.layer?.animation(forKey: animationKey)) != nil) {
// animation.fromValue = self.layer?.presentation()?.position
// }
// }

// 移除当前动画,并添加新动画
self.layer?.removeAnimation(forKey: animationKey)
self.layer?.add(animation, forKey: animationKey)
}

// 移除当前动画,并添加新动画
self.layer?.removeAnimation(forKey: animationKey)
self.layer?.add(animation, forKey: animationKey)
}
}
// override func removeFromSuperview() {
// // 开始动画
// NSAnimationContext.beginGrouping()
// NSAnimationContext.current.duration = 1.0 // 动画持续时间为1秒
// NSAnimationContext.current.completionHandler = {
// super.removeFromSuperview() // 动画完成后,调用父类的removeFromSuperview
// }
//
// // 执行淡出动画
// self.animator().alphaValue = 0.0
//
// // 结束动画组
// NSAnimationContext.endGrouping()
// }
}

78 changes: 47 additions & 31 deletions sources/SquirrelPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ final class SquirrelPanel: NSPanel {
candidateRanges.append(NSRange(location: text.length, length: line.length))
text.append(line)
lines.append(line)
print("text.string:{\(text.string)}")

// //添加空格
// var separatedLine = NSMutableAttributedString()
// if i == 0 {
Expand All @@ -365,18 +365,21 @@ final class SquirrelPanel: NSPanel {
for i in 0..<lines.count {
lines[i].insert(NSAttributedString(string: " "), at: 0)
lines[i].append(NSAttributedString(string: " "))
print("****")
print(lines[i])
print("****")
// print("****")
// print(lines[i])
// print("****")
}
// text done!
//以下三行绘制富文本
view.textView.textContentStorage?.attributedString = text
// view.textView.textContentStorage?.attributedString = text
view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal)
view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange, canPageUp: page > 0, canPageDown: !lastPage)

show()
if linear && !vertical {//拆分候选的动画暂时仅支持横版横排的情况
showWithSeparatedCandidates()
}else{
show()
}
///流程解读:
///按下键发送给librime后,librime会经过一段时间的计算,然后发回,
///在收到librime的候选项数组后,SqruirrelPanel用update方法处理成line的集合,拼接成text,然后放进SqruirrelView.textView的textContentStorage里,
Expand All @@ -386,6 +389,11 @@ final class SquirrelPanel: NSPanel {
///思路1:在View里添加一个属性var lines:[NSMutableAttributedString] = [],用来接收lines,然后在draw方法里把lines处理成一个个GCLayer
///难点:如果计算初始化的每个候选项的位置?
///思路2:新建一个NSView类,就叫Line或者其他名字,用来表示每个候选项
///
///2024年12月18日更新:
///现在做出来了,思路是重写NSTextField类,让其frame在didSet的时候自动添加动画,然后把每个NSTextField作为候选项放进
///NSStackView里,这样好处是全自动的,只需要手动改变候选项的文本,改变后候选项长度变了,stack会自动排列,排列的时候
///改变了位置会自动添加动画
}

func updateStatus(long longMessage: String, short shortMessage: String) {
Expand Down Expand Up @@ -519,8 +527,7 @@ private extension SquirrelPanel {
panelRect.origin.y = self.screenRect.minY
}
// panelRect.size.width += 50
/// panelRect为候选视图的坐标、范围,这里赋予本视图类
/// 实测这里display设为false也能显示候选框
/// panelRect为候选视图的坐标、范围,这里赋予panel
self.setFrame(panelRect, display: true)
// print(panelRect)
//这里开始要配置NSView的属性了
Expand Down Expand Up @@ -556,8 +563,34 @@ private extension SquirrelPanel {
/// 只能把一个App的所有窗口带到前台,不能单独),被带到前台的NSView会自动调自己的draw()方法
// orderFront(nil)
// voila!
}

func show(status message: String) {
// print("**** show(status message: String) ****")
let theme = view.currentTheme
let text = NSMutableAttributedString(string: message, attributes: theme.attrs)
text.addAttribute(.paragraphStyle, value: theme.paragraphStyle, range: NSRange(location: 0, length: text.length))
view.textContentStorage.attributedString = text
view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal)
view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1,
preeditRange: .empty, highlightedPreeditRange: .empty, canPageUp: false, canPageDown: false)
show()

statusTimer?.invalidate()
statusTimer = Timer.scheduledTimer(withTimeInterval: SquirrelTheme.showStatusDuration, repeats: false) { _ in
self.hide()
}
}

func showWithSeparatedCandidates(){
self.currentScreen()//获取显示器信息
let theme = self.view.currentTheme
//下面开始计算锚点
var panelRect = NSRect.zero
panelRect.origin = NSPoint(x: self.position.minX - theme.pagingOffset, y: self.position.minY - SquirrelTheme.offsetHeight - panelRect.height)
self.setFrame(panelRect, display: true)

//带动画的候选项
//下面开始添加候选项视图
let oldNum = view.textStack.subviews.count
let newNum = lines.count
// print("oldNum:\(oldNum),newNum:\(newNum)")
Expand All @@ -580,8 +613,7 @@ private extension SquirrelPanel {
}else if oldNum == newNum{

}else if oldNum > newNum{
//为什么这里删除多余的后候选框会瞬间消失并且编辑中的字母会上屏?比如按bq的q的时候,会上屏q,连b都没有了
for i in newNum..<oldNum{
for _ in newNum..<oldNum{
let viewToRemove = view.textStack.arrangedSubviews[newNum]
view.textStack.removeArrangedSubview(viewToRemove)
viewToRemove.removeFromSuperview()
Expand All @@ -596,35 +628,19 @@ private extension SquirrelPanel {
animateNSTextView.animationTypeStr = theme.candidateAnimationType
animateNSTextView.animationDuration = theme.candidateAnimationDuration
animateNSTextView.animationInterruptType = theme.candidateAnimationInterruptType
animateNSTextView.page = page
// animateNSTextView.textContentStorage?.attributedString = lines[i] //如果是NSTextView用这个
animateNSTextView.attributedStringValue = lines[i] //更新视图字符串
print("更新前lines[i]:[\(lines[i].string)]")
print("更新后的a.a.string:[\(animateNSTextView.attributedStringValue.string)]")
print("padding:",self.view.textContainer.lineFragmentPadding)
// print("更新前lines[i]:[\(lines[i].string)]")
// print("更新后的a.a.string:[\(animateNSTextView.attributedStringValue.string)]")
// print("padding:",self.view.textContainer.lineFragmentPadding)
// print("右边距:",animateNSTextView.layer?.layoutManager.rightAnchor)
}
}
//请求前台显示,并且不强求成为活动窗口
orderFrontRegardless()
}

func show(status message: String) {
// print("**** show(status message: String) ****")
let theme = view.currentTheme
let text = NSMutableAttributedString(string: message, attributes: theme.attrs)
text.addAttribute(.paragraphStyle, value: theme.paragraphStyle, range: NSRange(location: 0, length: text.length))
view.textContentStorage.attributedString = text
view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal)
view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1,
preeditRange: .empty, highlightedPreeditRange: .empty, canPageUp: false, canPageDown: false)
show()

statusTimer?.invalidate()
statusTimer = Timer.scheduledTimer(withTimeInterval: SquirrelTheme.showStatusDuration, repeats: false) { _ in
self.hide()
}
}

func convert(range: Range<String.Index>, in string: String) -> NSRange {
let startPos = range.lowerBound.utf16Offset(in: string)
let endPos = range.upperBound.utf16Offset(in: string)
Expand Down

0 comments on commit 18fa2f9

Please sign in to comment.