You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS中如何计算UIView的可见百分比?

计算UIView的屏幕可见百分比(解决部分/完全遮挡问题)

现有方案的核心问题

你当前的实现存在几个关键缺陷:

  1. 视图遍历逻辑错误:仅在topWindow的子视图中寻找目标视图之后的视图,完全忽略了目标视图所在的window可能不是顶层window,以及嵌套层级中父视图的同级上层视图、更高层级window的视图等遮挡源。
  2. 重复扣除遮挡区域:多个遮挡视图重叠的区域会被多次减去,直接导致可见百分比出现负数。
  3. 未过滤无效视图:没有处理遮挡视图的hidden、alpha、clipsToBounds等影响可见性的属性,导致计算偏差。

正确实现思路

核心逻辑是:获取目标视图在屏幕坐标系下的可见区域,计算该区域面积占目标视图自身面积的比例。步骤如下:

  1. 快速排除完全不可见的情况:视图隐藏、透明度极低、无window关联、自身面积为0时,直接返回0%。
  2. 统一坐标系:将目标视图和所有遮挡视图的frame转换到屏幕坐标系(即window坐标系,用convertRect:toView:nil实现)。
  3. 收集所有遮挡源:包括所有层级高于目标视图所在window的window的所有可见子视图,以及同window中视觉层级在目标视图之上的所有可见视图。
  4. 合并遮挡区域:对所有遮挡视图与目标视图的交集区域进行合并,避免重复计算重叠部分。
  5. 计算可见百分比:用目标视图的屏幕面积减去合并后的遮挡总面积,再除以自身面积得到最终百分比。

完整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 02:15:40