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

iOS刮刮卡开发:如何合并背景色与透明PNG为单个UIImage?

iOS刮刮卡交互实现方案

效果示意图:
效果示意图

问题根因

直接给承载顶层透明PNG的UIImageView设置背景色时,背景色属于视图层属性,和PNG图片本身是两个独立的渲染单元。你当前的擦除逻辑仅修改UIImage的像素内容,擦除PNG透明区域的像素时,对应位置的视图背景色也会一同暴露为空,无法正常显示下层内容。

1. 预合并背景色与透明PNG为单张刮层图片

调用以下方法将指定背景色和透明PNG合并为一张完整的UIImage,作为后续擦除操作的唯一刮层资源:

- (UIImage *)mergeBackgroundColor:(UIColor *)bgColor withPNG:(UIImage *)pngImage {
    CGSize size = pngImage.size;
    CGFloat scale = pngImage.scale;
    // 这里设为NO是为了保留擦除后的透明通道
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    
    // 先填充背景色
    [bgColor setFill];
    UIRectFill(CGRectMake(0, 0, size.width, size.height));
    // 再绘制顶层透明PNG
    [pngImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
    
    UIImage *mergedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return mergedImage;
}

2. 擦除逻辑适配

你现有触摸擦除代码逻辑基本可用,仅需确认两点配置:

  • 初始化存储刮层内容的位图上下文_imageContext时,使用kCGImageAlphaPremultipliedLast格式,支持alpha通道修改
  • 擦除绘制时使用clearColor填充/描边,你当前代码已满足该要求

3. 视图层级配置

  • 最下层放置刮开后要展示的内容视图/图片
  • 上层放置刮层UIImageView,将第一步生成的合并图片赋值给它的image属性
  • 所有触摸擦除操作仅修改上层刮层ImageView的image内容,擦除区域像素变为透明后即可直接露出下层内容

现有可复用擦除代码

- (UIImage *)addTouches:(NSSet *)touches {
    CGSize size = CGSizeMake(self.image.size.width * self.image.scale, self.image.size.height * self.image.scale);
    CGContextRef ctx = _imageContext;
    
    CGContextSetFillColorWithColor(ctx,[UIColor clearColor].CGColor);
    CGContextSetStrokeColorWithColor(ctx,[UIColor colorWithRed:0 green:0 blue:0 alpha:0].CGColor);
    int tempFilled = _tilesFilled;
    
    // process touches
    for (UITouch *touch in touches) {
        CGContextBeginPath(ctx);
        CGPoint touchPoint = [touch locationInView:self];
        touchPoint = fromUItoQuartz(touchPoint, self.bounds.size);
        touchPoint = scalePoint(touchPoint, self.bounds.size, size);
        
        if(UITouchPhaseBegan == touch.phase){
            [self.touchPoints removeAllObjects];
            [self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
            [self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
            // on begin, we just draw ellipse
            CGRect rect = CGRectMake(touchPoint.x - _radius, touchPoint.y - _radius, _radius*2, _radius*2);
            CGContextAddEllipseInRect(ctx, rect);
            CGContextFillPath(ctx);
            static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:@selector(fillTileWithPoint:)];
            (*fillTileFunc)(self,@selector(fillTileWithPoint:),rect.origin);
        } else if (UITouchPhaseMoved == touch.phase) {
            [self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
            // then touch moved, we draw superior-width line
            CGContextSetStrokeColor(ctx, CGColorGetComponents([UIColor clearColor].CGColor));
            CGContextSetLineCap(ctx, kCGLineCapRound);
            CGContextSetLineWidth(ctx, 2 * _radius);
            
            while(self.touchPoints.count > 3){
                CGPoint bezier[4];
                bezier[0] = ((NSValue*)self.touchPoints[1]).CGPointValue;
                bezier[3] = ((NSValue*)self.touchPoints[2]).CGPointValue;
                
                CGFloat k = 0.3;
                CGFloat len = sqrt(pow(bezier[3].x - bezier[0].x, 2) + pow(bezier[3].y - bezier[0].y, 2));
                bezier[1] = ((NSValue*)self.touchPoints[0]).CGPointValue;
                bezier[1] = [self normalizeVector:CGPointMake(bezier[0].x - bezier[1].x - (bezier[0].x - bezier[3].x), bezier[0].y - bezier[1].y - (bezier[0].y - bezier[3].y) )];
                bezier[1].x *= len * k;
                bezier[1].y *= len * k;
                bezier[1].x += bezier[0].x;
                bezier[1].y += bezier[0].y;
                
                bezier[2] = ((NSValue*)self.touchPoints[3]).CGPointValue;
                bezier[2] = [self normalizeVector:CGPointMake( (bezier[3].x - bezier[2].x)  - (bezier[3].x - bezier[0].x), (bezier[3].y - bezier[2].y)  - (bezier[3].y - bezier[0].y) )];
                bezier[2].x *= len * k;
                bezier[2].y *= len * k;
                bezier[2].x += bezier[3].x;
                bezier[2].y += bezier[3].y;
                
                CGContextMoveToPoint(ctx, bezier[0].x, bezier[0].y);
                CGContextAddCurveToPoint(ctx, bezier[1].x, bezier[1].y, bezier[2].x, bezier[2].y, bezier[3].x, bezier[3].y);
                
                [self.touchPoints removeObjectAtIndex:0];
            }
            
            CGContextStrokePath(ctx);
            
            CGPoint prevPoint = [touch previousLocationInView:self];
            prevPoint = fromUItoQuartz(prevPoint, self.bounds.size);
            prevPoint = scalePoint(prevPoint, self.bounds.size, size);
            
            static const FillTileWithTwoPointsFunc fillTileFunc = (FillTileWithTwoPointsFunc) [self methodForSelector:@selector(fillTileWithTwoPoints:end:)];
            (*fillTileFunc)(self,@selector(fillTileWithTwoPoints:end:),touchPoint, prevPoint);
        }
    }
    
    if(tempFilled != _tilesFilled) {
        [_delegate mdScratchImageView:self didChangeMaskingProgress:self.maskingProgress];
    }
    
    CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    return image;
}

-(void)fillTileWithPoint:(CGPoint) point{
    size_t x,y;
    point.x = MAX( MIN(point.x, self.image.size.width - 1) , 0);
    point.y = MAX( MIN(point.y, self.image.size.height - 1), 0);
    x = point.x * self.maskedMatrix.max.x / self.image.size.width;
    y = point.y * self.maskedMatrix.max.y / self.image.size.height;
    char value = [self.maskedMatrix valueForCoordinates:x y:y];
    if (!value){
        [self.maskedMatrix setValue:1 forCoordinates:x y:y];
        _tilesFilled++;
    }
}

-(void)fillTileWithTwoPoints:(CGPoint)begin end:(CGPoint)end{
    CGFloat incrementerForx,incrementerFory;
    static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:@selector(fillTileWithPoint:)];
    
    incrementerForx = (begin.x < end.x ? 1 : -1) * self.image.size.width / _tilesX;
    incrementerFory = (begin.y < end.y ? 1 : -1) * self.image.size.height / _tilesY;
    
    CGPoint i = begin;
    while(i.x <= MAX(begin.x, end.x) && i.y <= MAX(begin.y, end.y) && i.x >= MIN(begin.x, end.x) && i.y >= MIN(begin.y, end.y)){
        (*fillTileFunc)(self,@selector(fillTileWithPoint:),i);
        i.x += incrementerForx;
        i.y += incrementerFory;
    }
    (*fillTileFunc)(self,@selector(fillTileWithPoint:),end);
}

内容的提问来源于stack exchange,提问作者Aashna Narula

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 14:42:04