Skip to content

set dar、scale、rotate preference,由外部控制动画,onRecord可测试#76

Closed
CloudlessMoon wants to merge 8 commits intodebugly:mainfrom
CloudlessMoon:main
Closed

set dar、scale、rotate preference,由外部控制动画,onRecord可测试#76
CloudlessMoon wants to merge 8 commits intodebugly:mainfrom
CloudlessMoon:main

Conversation

@CloudlessMoon
Copy link
Contributor

@debugly
Copy link
Owner

debugly commented Jan 28, 2026

为了修复macOS上手动旋转时画面显示感觉闪烁的bug,故意做了这个动画来避免。为了不突兀改成所有手动调节的设置项都有动画。

你这么改回去的话那macOS上这个bug怎么搞?实际上也只有iOS上才会有你之前优化的那个问题,因为mac不支持旋转屏幕。

@CloudlessMoon
Copy link
Contributor Author

CloudlessMoon commented Jan 28, 2026

为了修复macOS上手动旋转时画面显示感觉闪烁的bug,故意做了这个动画来避免。为了不突兀改成所有手动调节的设置项都有动画。

macOS上也由业务实现动画即可:
图片
这样做也会有动画,和你之前的提交一样,但存在一个Bug,当执行runAnimationGroup时动画可能不会生效,比如说播放视频后,先点击适应模式,设置为拉伸填充,再设置为等比适应,此后再旋转画面,上述的动画就可以生效,反之不生效,感觉这应该是AppKit的bug,当renderedView.frame.origin没有进行像素对齐时,runAnimationGroup就不生效,当进行像素对齐后就没问题了,UIKit倒没这种奇怪的问题。。。最新的commit也解决了此问题,可以拉下来试试

若按照之前self.animating = YES来作为标记,会有一些副作用,比如说678b498#commitcomment-175793545 提到的。还有当动画执行完设置self.animating = NO,在此之前若改变FSMetalView的frame,触发layoutSubviews,renderedView用的还是之前的size导致显示错误。

实际上也只有iOS上才会有你之前优化的那个问题,因为mac不支持旋转屏幕

macOS也有问题,旋转屏幕只是其中的一种场景,实际上当以动画的形式去设置FSMetalView的frame都会有之前的拉伸变形问题.

@debugly
Copy link
Owner

debugly commented Jan 28, 2026

动画放在外部的话,意味着:“我们知道有bug,需要使用者自己去解决”。使用者如果不知道设定个动画,那么就会出现主动设置旋转时画面闪烁问题,这种情况我不想看到,因为属于功能回退了。反倒是现在打的补丁能解决这个问题,并且违和下用户主动这个场景,给个动画是可以接受的。

“实际上当以动画的形式去设置FSMetalView的frame都会有之前的拉伸变形问题”,是的,现在的动画在设定旋转时是拉伸的,但设置dar和scalingMode没问题。

我没有更好的办法了,我试了编写动画组模拟 iOS 系统旋转屏的效果做旋转,但是那样会导致和播放器底层渲染的旋转叠加,比如设定旋转90度,实际旋转180的奇怪效果,试着顺着这个路子往下解决需要处理的事情更加复杂了。

                // 1. 确保开启 Layer 支持
                self.renderedView.wantsLayer = YES;
                CALayer *realLayer = self.renderedView.layer;

                // 2. 记录当前状态作为动画起点
                // macOS 下使用 presentationLayer 获取当前视觉上的真实位置
                CATransform3D startTransform = realLayer.presentationLayer.transform;
                CGPoint startPosition = realLayer.presentationLayer.position;
                CGRect startBounds = realLayer.presentationLayer.bounds;

                // 3. 计算目标值
                CGFloat rotateRadian = (zDegrees / 180.0) * M_PI;
                CGRect targetBounds = CGRectMake(0, 0, size.width, size.height);
                CGPoint targetPosition = CGPointMake(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
                CATransform3D targetTransform = CATransform3DMakeRotation(rotateRadian, 0, 0, 1);

                // 4. 【关键】同步 Model 层并禁用隐式动画
                [CATransaction begin];
                [CATransaction setDisableActions:YES];

                realLayer.anchorPoint = CGPointMake(0.5, 0.5);
                realLayer.position = targetPosition;
                realLayer.bounds = targetBounds;
                realLayer.transform = targetTransform;

                [CATransaction commit];

                // 5. 创建显式动画
                // 使用 transform 属性整体动画,比单独动 rotation.z 更稳定
                CABasicAnimation *transformAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
                transformAnim.fromValue = [NSValue valueWithCATransform3D:startTransform];
                transformAnim.toValue = [NSValue valueWithCATransform3D:targetTransform];

                CABasicAnimation *positionAnim = [CABasicAnimation animationWithKeyPath:@"position"];
                positionAnim.fromValue = [NSValue valueWithPoint:startPosition]; // macOS 使用 valueWithPoint
                positionAnim.toValue = [NSValue valueWithPoint:targetPosition];

                CABasicAnimation *boundsAnim = [CABasicAnimation animationWithKeyPath:@"bounds"];
                boundsAnim.fromValue = [NSValue valueWithRect:startBounds];     // macOS 使用 valueWithRect
                boundsAnim.toValue = [NSValue valueWithRect:targetBounds];

                // 6. 组合动画
                CAAnimationGroup *animGroup = [CAAnimationGroup animation];
                animGroup.animations = @[transformAnim, positionAnim, boundsAnim];
                animGroup.duration = 0.35;
                animGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
                // 既然 Model 层已经改了,这里千万不要用 FillModeForwards
                animGroup.removedOnCompletion = YES;

                [CATransaction begin];
                [CATransaction setCompletionBlock:^{
                    // 动画结束后的业务逻辑
                    self.animating = NO;
                    self.renderedView.frame = CGRectMake(origin.x, origin.y, size.width, size.height);
                    [self setNeedsRefreshCurrentPic];
                }];
                [realLayer addAnimation:animGroup forKey:@"rotateFrameAnim"];
                [CATransaction commit];

@CloudlessMoon
Copy link
Contributor Author

CloudlessMoon commented Jan 29, 2026

反倒是现在打的补丁能解决这个问题

现在的补丁也没有解决问题,就算了加了动画也还有闪烁的问题,只是没有之前明显了,反而是增加了几个副作用。

分析下这个“闪烁”问题产生的原因,分为优化前和优化后,优化前是指没有使用frame,完全使用Metal来控制scalingMode等参数,优化后是指使用frame来控制scalingMode等参数。

以下描述的「横竖屏」旋转特指业务中改变FSMetalView的frame,「横竖屏」只是其中的一种场景,能够直观的体现这一行为。

设置scalingMode、rotatePreference、darPreference会出现「拉伸」或者「闪烁」的问题吗

① 优化前:「横竖屏」时会出现拉伸和闪烁的问题;非「横竖屏」则不会出现
② 优化后:「横竖屏」时设置scalingMode、darPreference不会出现拉伸和闪烁问题,rotatePreference会出现;非「横竖屏」设置scalingMode、darPreference也不会出现拉伸和闪烁问题,rotatePreference会出现

可以看到优化后「不管是否在横竖屏」中都解决了scalingMode、darPreference拉伸和闪烁问题,但rotatePreference没有解决,结合上面的讨论,可以分析得出:
scalingMode、darPreference均是通过设置frame来实现的,与metal的渲染无关,也就是说layoutSubviewsmetal drawRect是完全隔离的,不管有没有「横竖屏」,两者都不会出现问题。

但rotatePreference就不一样了,layoutSubviews里根据rotatePreference改变了frame.size,同时metal drawRectpicturePipeline设置了rotatePreference改变了画面旋转,layoutSubviews在主线程,metal drawRect在子线程,当设置完frame.size,画面还没有更改,或者画面更改了,frame.size还没有更改,此时就会导致拉伸和闪烁。这在任何程序里都是无解的,你加的补丁只是“延缓”了闪烁效果,实际上还是存在的,仔细观察还是能看出来。

解决rotatePreference拉伸和闪烁问题,只有一个办法,就是通过设置renderedView的transform或者transform3D来实现rotatePreference,去掉metal drawRect里的rotatePreference,例如如下代码:

            if (rotatePreference.type == FSRotateZ) {
                self.renderedView.transform = CGAffineTransformMakeRotation(M_PI / 2);
            } else {
                self.renderedView.transform = CGAffineTransformIdentity;
            }
            self.renderedView.frame = __FSMetalCGRectApplyAffineTransformWithAnchorPoint(CGRectMake(origin.x, origin.y, size.width, size.height),
                                                                                         self.renderedView.transform,
                                                                                         self.renderedView.layer.anchorPoint);
CG_INLINE CGPoint
__FSMetalCGPointApplyAffineTransformWithCoordinatePoint(CGPoint coordinatePoint, CGPoint targetPoint, CGAffineTransform t) {
    CGPoint p;
    p.x = (targetPoint.x - coordinatePoint.x) * t.a + (targetPoint.y - coordinatePoint.y) * t.c + coordinatePoint.x;
    p.y = (targetPoint.x - coordinatePoint.x) * t.b + (targetPoint.y - coordinatePoint.y) * t.d + coordinatePoint.y;
    p.x += t.tx;
    p.y += t.ty;
    return p;
}

CG_INLINE CGRect
__FSMetalCGRectApplyAffineTransformWithAnchorPoint(CGRect rect, CGAffineTransform t, CGPoint anchorPoint) {
    CGFloat width = CGRectGetWidth(rect);
    CGFloat height = CGRectGetHeight(rect);
    CGPoint oPoint = CGPointMake(rect.origin.x + width * anchorPoint.x, rect.origin.y + height * anchorPoint.y);
    CGPoint top_left = __FSMetalCGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x, rect.origin.y), t);
    CGPoint bottom_left = __FSMetalCGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x, rect.origin.y + height), t);
    CGPoint top_right = __FSMetalCGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x + width, rect.origin.y), t);
    CGPoint bottom_right = __FSMetalCGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x + width, rect.origin.y + height), t);
    CGFloat minX = MIN(MIN(MIN(top_left.x, bottom_left.x), top_right.x), bottom_right.x);
    CGFloat maxX = MAX(MAX(MAX(top_left.x, bottom_left.x), top_right.x), bottom_right.x);
    CGFloat minY = MIN(MIN(MIN(top_left.y, bottom_left.y), top_right.y), bottom_right.y);
    CGFloat maxY = MAX(MAX(MAX(top_left.y, bottom_left.y), top_right.y), bottom_right.y);
    CGFloat newWidth = maxX - minX;
    CGFloat newHeight = maxY - minY;
    CGRect result = CGRectMake(minX, minY, newWidth, newHeight);
    return result;
}

macOS上则是通过rotateByAngle等属性实现。

@debugly
Copy link
Owner

debugly commented Feb 4, 2026

既然问题出自这个frame的设定,那么何不在即将旋转时去修改frame,修正你提出的画面变形问题,在旋转结束后再把frame修改回来,解决设置旋转角度问题,这样是不是就能完美解决了?

@CloudlessMoon
Copy link
Contributor Author

CloudlessMoon commented Feb 4, 2026

既然问题出自这个frame的设定,那么何不在即将旋转时去修改frame,修正你提出的画面变形问题,在旋转结束后再把frame修改回来,解决设置旋转角度问题,这样是不是就能完美解决了?

问题原因是设置frame必须在主线程,set rotatePreference是metal线程,就算在主线程调用draw也不行,因为总有一帧对应新的frame。

实际上这些属性要么都用frame去实现,要么都用metal去draw,结合使用就会有上述问题。

目前开源的播放器例如https://github.com/libobjc/SGPlayer https://github.com/kingslay/KSPlayer ,或多或少都有我在上面提过的问题,闭源的例如阿里云播放器解决的比较粗暴,直接将CATransaction关闭了,也就是说视图层没办法实现动画了,腾讯云播放器处理的比较好,腾讯是完全用metalLayer去draw,但是它解决了横竖屏旋转画面会被拉伸变形的问题

@debugly
Copy link
Owner

debugly commented Feb 4, 2026

我尝试过关闭 CATransaction,我也尝试了我说的旋转结束后再把frame修改回来,改回来的时候发现只要改frame就会出现画面闪下,感觉又回到了我加动画缓解变形的现场了。
这都是 metal 的异步绘制和Core Animation无法同步导致的,闪下是隐式动画在作怪。

我又找到了一个新思路,改动简单,你可以看下有啥问题没。我看效果挺好的,画面没有拉伸变形,围绕中心点旋转。

debugly added a commit that referenced this pull request Feb 4, 2026
@CloudlessMoon
Copy link
Contributor Author

CloudlessMoon commented Feb 4, 2026

我又找到了一个新思路,改动简单,你可以看下有啥问题没。我看效果挺好的,画面没有拉伸变形,围绕中心点旋转。

这个思路好,不会拉伸变形了,不过也无法实现Core Animation了,但目前应该是最好的方案了。

reset FSMetalView to 4a668d55,这个提交得把之前的一些commit带上:
图片
UIView初始化时不要返回nil

图片

后台不渲染

@debugly
Copy link
Owner

debugly commented Feb 4, 2026

只是改了内容显示模式,不会影响 Core Animation 的工作啊。

@CloudlessMoon
Copy link
Contributor Author

只是改了内容显示模式,不会影响 Core Animation 的工作啊。

设置内容显示模式不会影响,但是之前通过设置frame会自动应用Core Animation,也就是说横竖屏旋转场景会有个默认的动画,可以看看#66 这个PR中的视频,与现在的有些差别。

@debugly
Copy link
Owner

debugly commented Feb 4, 2026

是的,设置frame会触发隐式动画。现在有一点瑕疵,但比最初好太多了。
感谢有你。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants