【问题标题】:Swipe between filtered images在过滤后的图像之间滑动
【发布时间】:2025-12-08 22:55:02
【问题描述】:

我正在尝试允许用户在静态图像上的过滤器之间滑动。这个想法是图像保持在原位,而过滤器在其上方滚动。 Snapchat 最近发布了一个实现此功能的版本。 This video shows exactly what I'm trying to accomplish at 1:05.

到目前为止,我已经尝试将三个 UIImageView 放入一个滚动视图中,在原始图像的左侧和右侧,并使用滚动视图的 contentOffset.x 调整它们的帧 origin.x 和 size.width。我在另一篇帖子here 中发现了这个想法。将左右的内容模式更改为 UIViewContentModeLeft 和 UIViewContentModeRight 没有帮助。

接下来,我尝试将所有三个 UIImageView 堆叠在一起。我制作了两个 CALayer 蒙版,并将它们插入到堆栈左侧和右侧的滚动视图中,这样当您滚动蒙版时,就会显示过滤后的图像。这对我不起作用。任何帮助将不胜感激。

【问题讨论】:

  • 更改位置和大小以及UIViewContentModeLeft/UIViewContentModeRight 似乎应该可以工作。您可以发布您尝试做的代码吗?顺便说一句,您需要图像大小与 UIImageViews 的实际大小相匹配,因为UIViewContentModeLeft 不进行任何缩放。您还需要 UIImageView 拥有clipsToBounds = YES(只是我脑子里可能出错的几件事)。
  • 视频过长。你说的是哪一部分?
  • 1:05。我试图在正确的时间开始播放视频。我猜它没有工作
  • 在我看来这些实际上是图像顶部的叠加层,而不是多个图像。
  • 尽管演示的其他方面似乎来自固定 UIImage 上的透明 UIScrollview。过滤器本身可能是其他东西 - 也许 UIView 从 1 个图像过渡到另一个(UIView 动画) UIViewAnimationOptionShowHideTransitionViews 看起来很有希望。

标签: ios objective-c calayer imagefilter snapchat


【解决方案1】:

您可以创建带有滚动视图的可滑动过滤器,该滚动视图带有 CIImage 的分页滚动。

你可以使用这个:https://github.com/pauljeannot/SnapSliderFilters

【讨论】:

    【解决方案2】:

    部分使用 Aggressor 的解决方案,我想出了我认为最简单的设置方法,并且代码行数最少。

    @IBOutlet weak var topImage: UIImageView!
    @IBOutlet weak var bottomImage: UIImageView!
    @IBOutlet weak var scrollview: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        scrollview.delegate=self
        scrollview.contentSize=CGSizeMake(2*self.view.bounds.width, self.view.bounds.height)
    
        applyMask(CGRectMake(self.view.bounds.width-scrollview.contentOffset.x, scrollview.contentOffset.y, scrollview.contentSize.width, scrollview.contentSize.height))
    
    }
    
    func applyMask(maskRect: CGRect!){
        var maskLayer: CAShapeLayer = CAShapeLayer()
        var path: CGPathRef = CGPathCreateWithRect(maskRect, nil)
        maskLayer.path=path
        topImage.layer.mask = maskLayer
    }
    
    func scrollViewDidScroll(scrollView: UIScrollView) {
        println(scrollView.contentOffset.x)
        applyMask(CGRectMake(self.view.bounds.width-scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.contentSize.width, scrollView.contentSize.height))
    }
    

    然后只需设置图像,并确保在 imageViews 上方有滚动视图。对于请求的行为(如 snapchat),确保滚动视图已启用分页设置为 true,并确保其背景颜色清晰。这种方法的好处是你可以免费获得所有的滚动视图行为......因为你使用了滚动视图

    【讨论】:

    • 我会注意硬编码 320 的使用,因为它对大小的假设不一定成立。否则从概念上讲,它为我的需求指明了一个好的方向。
    • 我看不到这段代码中过滤器的使用位置
    • @cannyboy 在加载图像的某个时刻,您可能会对它们应用过滤器。这个问题指的是如何在图像之间滑动,而不是如何过滤它们。如果您正在寻找一些基本的图像过滤代码,我可以提供它,但在此 developer.apple.com/library/mac/documentation/GraphicsImaging/… 上有很多文档
    • @TimWhiting 谢谢,我会看看 GPUImage。对于多个过滤器,您会推荐多个图像视图,还是在滚动视图位于合适的 contentOffset.x 时交换图像?
    【解决方案3】:

    在我第一次尝试掩盖 UIImage 而不是 UIImage 视图时犯了一个错误,但最终在下面得到了一个相当不错的工作解决方案(它使用 UIImageView 掩码)。如果您有任何问题,请随时提出。

    我基本上是创建当前图像和过滤后的图像。我屏蔽 UIView(使用矩形),然后根据滑动调整屏蔽。

    结果链接: https://www.youtube.com/watch?v=k75nqVsPggY&list=UUIctdpq1Pzujc0u0ixMSeVw

    面具学分:https://*.com/a/11391478/3324388

    @interface FilterTestsViewController ()
    
    @end
    
    @implementation FilterTestsViewController
    
    NSArray *_pictureFilters;
    NSNumber* _pictureFilterIterator;
    UIImage* _originalImage;
    UIImage* _currentImage;
    UIImage* _filterImage;
    UIImageView* _uiImageViewCurrentImage;
    UIImageView* _uiImageViewNewlyFilteredImage;
    CGPoint _startLocation;
    BOOL _directionAssigned = NO;
    enum direction {LEFT,RIGHT};
    enum direction _direction;
    BOOL _reassignIncomingImage = YES;
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        [self initializeFiltering];
    }
    
    //set it up for video feed
    -(void)initializeVideoFeed
    {
    
    }
    
    -(void)initializeFiltering
    {
        //create filters
        _pictureFilters = @[@"CISepiaTone",@"CIColorInvert",@"CIColorCube",@"CIFalseColor",@"CIPhotoEffectNoir"];
        _pictureFilterIterator = 0;
    
        //create initial image and current image
        _originalImage = [UIImage imageNamed:@"ja.jpg"]; //creates image from file, this will result in a nil CIImage but a valid CGImage;
        _currentImage = [UIImage imageNamed:@"ja.jpg"];
    
        //create the UIImageViews for the current and filter object
        _uiImageViewCurrentImage = [[UIImageView alloc] initWithImage:_currentImage]; //creates a UIImageView with the UIImage
        _uiImageViewNewlyFilteredImage = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height)];//need to set its size to full since it doesn't have a filter yet
    
        //add UIImageViews to view
        [self.view addSubview:_uiImageViewCurrentImage]; //adds the UIImageView to view;
        [self.view addSubview:_uiImageViewNewlyFilteredImage];
    
        //add gesture
        UIPanGestureRecognizer* pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRecognized:)];
        [self.view addGestureRecognizer:pan];
    
    }
    
    
    -(void)swipeRecognized:(UIPanGestureRecognizer *)swipe
    {
        CGFloat distance = 0;
        CGPoint stopLocation;
        if (swipe.state == UIGestureRecognizerStateBegan)
        {
            _directionAssigned = NO;
            _startLocation = [swipe locationInView:self.view];
        }else
        {
            stopLocation = [swipe locationInView:self.view];
            CGFloat dx = stopLocation.x - _startLocation.x;
            CGFloat dy = stopLocation.y - _startLocation.y;
            distance = sqrt(dx*dx + dy*dy);
        }
    
        if(swipe.state == UIGestureRecognizerStateEnded)
        {
            if(_direction == LEFT && (([UIScreen mainScreen].bounds.size.width - _startLocation.x) + distance) > [UIScreen mainScreen].bounds.size.width/2)
            {
                [self reassignCurrentImage];
            }else if(_direction == RIGHT && _startLocation.x + distance > [UIScreen mainScreen].bounds.size.width/2)
            {
                [self reassignCurrentImage];
            }else
            {
                //since no filter applied roll it back
                if(_direction == LEFT)
                {
                    _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]-1];
                }else
                {
                    _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]+1];
                }
            }
            [self clearIncomingImage];
            _reassignIncomingImage = YES;
            return;
        }
    
        CGPoint velocity = [swipe velocityInView:self.view];
    
        if(velocity.x > 0)//right
        {
            if(!_directionAssigned)
            {
                _directionAssigned = YES;
                _direction  = RIGHT;
            }
            if(_reassignIncomingImage && !_filterImage)
            {
                _reassignIncomingImage = false;
                [self reassignIncomingImageLeft:NO];
            }
        }
        else//left
        {
            if(!_directionAssigned)
            {
                _directionAssigned = YES;
                _direction  = LEFT;
            }
            if(_reassignIncomingImage && !_filterImage)
            {
                _reassignIncomingImage = false;
                [self reassignIncomingImageLeft:YES];
            }
        }
    
        if(_direction == LEFT)
        {
            if(stopLocation.x > _startLocation.x -5) //adjust to avoid snapping
            {
                distance = -distance;
            }
        }else
        {
            if(stopLocation.x < _startLocation.x +5) //adjust to avoid snapping
            {
                distance = -distance;
            }
        }
    
        [self slideIncomingImageDistance:distance];
    }
    
    -(void)slideIncomingImageDistance:(float)distance
    {
        CGRect incomingImageCrop;
        if(_direction == LEFT) //start on the right side
        {
            incomingImageCrop = CGRectMake(_startLocation.x - distance,0, [UIScreen mainScreen].bounds.size.width - _startLocation.x + distance, [UIScreen mainScreen].bounds.size.height);
        }else//start on the left side
        {
            incomingImageCrop = CGRectMake(0,0, _startLocation.x + distance, [UIScreen mainScreen].bounds.size.height);
        }
    
        [self applyMask:incomingImageCrop];
    }
    
    -(void)reassignCurrentImage
    {
        if(!_filterImage)//if you go fast this is null sometimes
        {
            [self reassignIncomingImageLeft:YES];
        }
        _uiImageViewCurrentImage.image = _filterImage;
        self.view.frame = [[UIScreen mainScreen] bounds];
    }
    
    //left is forward right is back
    -(void)reassignIncomingImageLeft:(BOOL)left
    {
        if(left == YES)
        {
            _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]+1];
        }else
        {
            _pictureFilterIterator = [NSNumber numberWithInt:[_pictureFilterIterator intValue]-1];
        }
    
        NSNumber* arrayCount = [NSNumber numberWithInt:(int)_pictureFilters.count];
    
        if([_pictureFilterIterator integerValue]>=[arrayCount integerValue])
        {
            _pictureFilterIterator = 0;
        }
        if([_pictureFilterIterator integerValue]< 0)
        {
            _pictureFilterIterator = [NSNumber numberWithInt:(int)_pictureFilters.count-1];
        }
    
        CIImage* ciImage = [CIImage imageWithCGImage:_originalImage.CGImage];
        CIFilter* filter = [CIFilter filterWithName:_pictureFilters[[_pictureFilterIterator integerValue]] keysAndValues:kCIInputImageKey,ciImage, nil];
        _filterImage = [UIImage imageWithCIImage:[filter outputImage]];
        _uiImageViewNewlyFilteredImage.image = _filterImage;
        CGRect maskRect = CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height);
        [self applyMask:maskRect];
    }
    
    //apply mask to filter UIImageView
    -(void)applyMask:(CGRect)maskRect
    {
        // Create a mask layer and the frame to determine what will be visible in the view.
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    
        // Create a path with the rectangle in it.
        CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
    
        // Set the path to the mask layer.
        maskLayer.path = path;
    
        // Release the path since it's not covered by ARC.
        CGPathRelease(path);
    
        // Set the mask of the view.
        _uiImageViewNewlyFilteredImage.layer.mask = maskLayer;
    }
    
    
    -(void)clearIncomingImage
    {
        _filterImage = nil;
        _uiImageViewNewlyFilteredImage.image = nil;
        //mask current image view fully again
        [self applyMask:CGRectMake(0,0,[UIScreen mainScreen].bounds.size.width,[UIScreen mainScreen].bounds.size.height)];
    }
    
    - (void)didReceiveMemoryWarning
    {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    @end
    

    【讨论】:

      【解决方案4】:

      您应该只需要 2 个图像视图(当前一个和传入的一个,因为这是一个分页样式滚动),并且它们在每次过滤器更改后切换角色。而且您使用图层蒙版的方法应该有效,但不适用于滚动视图。

      因此,请确保您的视图组织类似于:

      UIView // receives all gestures
          UIScrollView // handles the filter name display, touch disabled
          UIImageView // incoming in front, but masked out
          UIImageView // current behind
      

      每个图像视图都有一个遮罩层,它只是一个简单的层,您可以修改遮罩层的位置以更改图像实际可见的程度。

      现在,主视图处理平移手势,并使用手势的平移来改变传入的图像视图遮罩层位置和滚动视图内容偏移量。

      更改完成后,“当前”图像视图将无法再看到,而“传入”图像视图将占据整个屏幕。 “当前”图像视图现在移动到前面并变成 incoming 视图,它的掩码被更新以使其透明。随着下一个手势开始,它的图像会更新到下一个过滤器,并重新开始更改过程。

      您始终可以在滚动时在后台准备过滤后的图像,以便在您切换时图像准备好推送到视图中(用于快速滚动)。

      【讨论】:

      • 很棒的人很棒。我最初使用滚动视图是因为我想免费获取页面捕捉,但我非常乐意重新创建它。谢谢youtube.com/watch?v=j75d7Khf6iM
      • @Rich86man 你能分享一下gist或pastie中可滑动过滤器的代码吗?我正在尝试做同样的事情并坚持使用滚动视图。谢谢。
      • 有没有办法在可缩放的图像视图上添加滑动过滤器,如 instagram 添加故事页面?