iOS中如何计算UIView的可见百分比?
计算UIView的屏幕可见百分比(解决部分/完全遮挡问题)
现有方案的核心问题
你当前的实现存在几个关键缺陷:
- 视图遍历逻辑错误:仅在
topWindow的子视图中寻找目标视图之后的视图,完全忽略了目标视图所在的window可能不是顶层window,以及嵌套层级中父视图的同级上层视图、更高层级window的视图等遮挡源。 - 重复扣除遮挡区域:多个遮挡视图重叠的区域会被多次减去,直接导致可见百分比出现负数。
- 未过滤无效视图:没有处理遮挡视图的
hidden、alpha、clipsToBounds等影响可见性的属性,导致计算偏差。
正确实现思路
核心逻辑是:获取目标视图在屏幕坐标系下的可见区域,计算该区域面积占目标视图自身面积的比例。步骤如下:
- 快速排除完全不可见的情况:视图隐藏、透明度极低、无window关联、自身面积为0时,直接返回0%。
- 统一坐标系:将目标视图和所有遮挡视图的frame转换到屏幕坐标系(即window坐标系,用
convertRect:toView:nil实现)。 - 收集所有遮挡源:包括所有层级高于目标视图所在window的window的所有可见子视图,以及同window中视觉层级在目标视图之上的所有可见视图。
- 合并遮挡区域:对所有遮挡视图与目标视图的交集区域进行合并,避免重复计算重叠部分。
- 计算可见百分比:用目标视图的屏幕面积减去合并后的遮挡总面积,再除以自身面积得到最终百分比。
完整OC实现代码
#import <UIKit/UIKit.h> @interface UIView (VisiblePercentage) /// 计算视图在屏幕上的可见百分比(0~100) - (CGFloat)visiblePercentage; @end @implementation UIView (VisiblePercentage) - (CGFloat)visiblePercentage { // 基础判断:视图本身不可见 if (self.isHidden || self.alpha <= 0.01 || self.window == nil || CGRectGetWidth(self.bounds) == 0 || CGRectGetHeight(self.bounds) == 0) { return 0.0; } // 获取目标视图在屏幕坐标系下的frame CGRect targetScreenFrame = [self convertRect:self.bounds toView:nil]; CGFloat targetArea = CGRectGetWidth(targetScreenFrame) * CGRectGetHeight(targetScreenFrame); if (targetArea <= 0) { return 0.0; } // 收集所有可能的遮挡视图 NSMutableArray<UIView *> *obstructingViews = [NSMutableArray array]; // 按window层级排序,从低到高 NSArray<UIWindow *> *sortedWindows = [[UIApplication sharedApplication].windows sortedArrayUsingComparator:^NSComparisonResult(UIWindow *win1, UIWindow *win2) { if (win1.windowLevel < win2.windowLevel) { return NSOrderedAscending; } else if (win1.windowLevel > win2.windowLevel) { return NSOrderedDescending; } return NSOrderedSame; }]; BOOL foundTargetWindow = NO; for (UIWindow *window in sortedWindows) { if (window == self.window) { foundTargetWindow = YES; // 收集当前window中所有在目标视图上层的视图 [self collectObstructingViewsInWindow:window targetView:self intoArray:obstructingViews]; } else if (foundTargetWindow) { // 层级更高的window,所有可见子视图都可能遮挡 [self collectAllVisibleSubviewsOfView:window intoArray:obstructingViews]; } } // 计算合并后的遮挡总面积 CGRect totalObstructedRect = CGRectZero; for (UIView *obstructingView in obstructingViews) { if (obstructingView.isHidden || obstructingView.alpha <= 0.01 || obstructingView.window == nil) { continue; } CGRect obstructingScreenFrame = [obstructingView convertRect:obstructingView.bounds toView:nil]; CGRect intersection = CGRectIntersection(targetScreenFrame, obstructingScreenFrame); if (!CGRectIsNull(intersection)) { totalObstructedRect = CGRectUnion(totalObstructedRect, intersection); } } CGFloat obstructedArea = CGRectGetWidth(totalObstructedRect) * CGRectGetHeight(totalObstructedRect); CGFloat visibleArea = targetArea - obstructedArea; // 确保百分比不会为负数 return MAX(0.0, visibleArea / targetArea * 100); } // 收集指定window中所有在目标视图视觉上层的视图 - (void)collectObstructingViewsInWindow:(UIWindow *)window targetView:(UIView *)target intoArray:(NSMutableArray<UIView *> *)array { // 构建目标视图到window的层级链 NSMutableArray<UIView *> *targetHierarchy = [NSMutableArray array]; UIView *current = target; while (current != window && current != nil) { [targetHierarchy insertObject:current atIndex:0]; current = current.superview; } if (current != window) return; // 遍历视图层级,收集目标视图上层的所有视图 [self traverseView:window targetHierarchy:targetHierarchy currentLevel:0 collectIntoArray:array]; } - (void)traverseView:(UIView *)parent targetHierarchy:(NSArray<UIView *> *)hierarchy currentLevel:(NSInteger)level collectIntoArray:(NSMutableArray<UIView *> *)array { if (level >= hierarchy.count) return; UIView *targetInCurrentLevel = hierarchy[level]; BOOL foundTarget = NO; for (UIView *subview in parent.subviews) { if (subview == targetInCurrentLevel) { foundTarget = YES; continue; } if (foundTarget) { [array addObject:subview]; [self collectAllVisibleSubviewsOfView:subview intoArray:array]; } else { [self traverseView:subview targetHierarchy:hierarchy currentLevel:level + 1 collectIntoArray:array]; } } } // 收集视图的所有可见子视图 - (void)collectAllVisibleSubviewsOfView:(UIView *)view intoArray:(NSMutableArray<UIView *> *)array { if (view.isHidden || view.alpha <= 0.01) return; [array addObject:view]; for (UIView *subview in view.subviews) { [self collectAllVisibleSubviewsOfView:subview intoArray:array]; } } @end
使用方式
直接给目标视图调用分类方法即可:
CGFloat visiblePercent = [yourTargetView visiblePercentage]; NSLog(@"可见百分比:%.2f%%", visiblePercent);
关键细节说明
- 用
convertRect:toView:nil直接获取屏幕坐标系的frame,避免手动转换到topWindow的繁琐和误差。 - 通过合并遮挡区域解决了重复扣除的问题,彻底避免百分比负数。
- 覆盖了所有可能的遮挡场景:跨window遮挡、嵌套层级遮挡、视图自身可见性属性过滤。
内容的提问来源于stack exchange,提问作者TonyW
相关产品推荐
相关产品推荐

