iOS實現(xiàn)動態(tài)區(qū)域裁剪圖片

裁剪圖片功能在很多上傳圖片的場景里都需要用到柳击,一方面應用服務器可能對圖片的尺寸大小有限制淘衙,因而希望上傳的圖片都是符合規(guī)定的,另一方面腻暮,用戶可能希望只上傳圖片中的部分內(nèi)容彤守,突出圖片中關(guān)鍵的信息。而為了滿足用戶多種多樣的裁剪需求哭靖,就需要裁剪圖片時能支持由用戶動態(tài)地改變裁剪范圍具垫、裁剪尺寸等。

源代碼倉庫

動態(tài)裁剪圖片的基本過程大致可以分為以下幾步

  • 顯示圖片與裁剪區(qū)域
  • 支持移動和縮放圖片
  • 支持手勢改變裁剪區(qū)域
  • 進行圖片裁剪并獲得裁剪后的圖片

顯示圖片與裁剪區(qū)域

顯示圖片

在裁剪圖片之前试幽,首先我們要在頁面上顯示待裁剪的圖片筝蚕,如下圖所示

image.png

這一步比較簡單,配置一個 UIImageView 來放置圖片即可铺坞。但是要注意一點起宽,UIImageView 有多種 contentMode,最常見有三種

  • UIViewContentModeScaleToFill
  • UIViewContentModeScaleAspectFit
  • UIViewContentModeScaleAspectFill

三者區(qū)別可以看下面的比較

  • UIViewContentModeScaleToFill
image.png
  • UIViewContentModeScaleAspectFit
image.png
  • UIViewContentModeScaleAspectFill
image.png

可以看出济榨,ScaleToFill 會改變圖片的長寬比例來鋪滿整個 UIImageView坯沪,ScaleAspectFill 則會保持圖片比例來鋪滿,從而會有部分圖片內(nèi)容超出 UIImageView 區(qū)域的情況擒滑,而 ScaleAspectFit 則會保證圖片比例不變腐晾,同時圖片內(nèi)容都顯示在 UIImageView 中,即使無法鋪滿 UIImageView丐一。

因此不同顯示模式會影響到我們最終顯示到屏幕上的圖片的樣子藻糖,而在裁剪過程中最理想的放置圖片的模式則是,圖片的短邊剛好鋪滿裁剪區(qū)域的短邊库车,而長邊至少不會小于裁剪區(qū)域的長邊巨柒,這就要求我們要考慮裁剪區(qū)域的長寬來放置我們的圖片。

裁剪區(qū)域

接下來我們要放置我們的裁剪區(qū)域柠衍,它的樣子如下所示

image.png

裁剪區(qū)域本身就是在 UIImageView 上放上一層 UIView洋满,再在 UIView 上繪制出一個白邊框的方格 Layer。

首先自定義一個 CAShapeLayer

#import <QuartzCore/QuartzCore.h>

@interface YasicClipAreaLayer : CAShapeLayer

@property(assign, nonatomic) NSInteger cropAreaLeft;
@property(assign, nonatomic) NSInteger cropAreaTop;
@property(assign, nonatomic) NSInteger cropAreaRight;
@property(assign, nonatomic) NSInteger cropAreaBottom;

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft CropAreaTop:(NSInteger)cropAreaTop CropAreaRight:(NSInteger)cropAreaRight CropAreaBottom:(NSInteger)cropAreaBottom;


@end

@implementation YasicClipAreaLayer

- (instancetype)init
{
    self = [super init];
    if (self) {
        _cropAreaLeft = 50;
        _cropAreaTop = 50;
        _cropAreaRight = SCREEN_WIDTH - self.cropAreaLeft;
        _cropAreaBottom = 400;
    }
    return self;
}

- (void)drawInContext:(CGContextRef)ctx
{
    UIGraphicsPushContext(ctx);
    
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetLineWidth(ctx, lineWidth);
    CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaTop);
    CGContextAddLineToPoint(ctx, self.cropAreaLeft, self.cropAreaBottom);
    CGContextSetShadow(ctx, CGSizeMake(2, 0), 2.0);
    CGContextStrokePath(ctx);
    
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetLineWidth(ctx, lineWidth);
    CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaTop);
    CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaTop);
    CGContextSetShadow(ctx, CGSizeMake(0, 2), 2.0);
    CGContextStrokePath(ctx);
    
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetLineWidth(ctx, lineWidth);
    CGContextMoveToPoint(ctx, self.cropAreaRight, self.cropAreaTop);
    CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaBottom);
    CGContextSetShadow(ctx, CGSizeMake(-2, 0), 2.0);
    CGContextStrokePath(ctx);
    
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetLineWidth(ctx, lineWidth);
    CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaBottom);
    CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaBottom);
    CGContextSetShadow(ctx, CGSizeMake(0, -2), 2.0);
    CGContextStrokePath(ctx);
    
    UIGraphicsPopContext();
}

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft
{
    _cropAreaLeft = cropAreaLeft;
    [self setNeedsDisplay];
}

- (void)setCropAreaTop:(NSInteger)cropAreaTop
{
    _cropAreaTop = cropAreaTop;
    [self setNeedsDisplay];
}

- (void)setCropAreaRight:(NSInteger)cropAreaRight
{
    _cropAreaRight = cropAreaRight;
    [self setNeedsDisplay];
}

- (void)setCropAreaBottom:(NSInteger)cropAreaBottom
{
    _cropAreaBottom = cropAreaBottom;
    [self setNeedsDisplay];
}

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft CropAreaTop:(NSInteger)cropAreaTop CropAreaRight:(NSInteger)cropAreaRight CropAreaBottom:(NSInteger)cropAreaBottom
{
    _cropAreaLeft = cropAreaLeft;
    _cropAreaRight = cropAreaRight;
    _cropAreaTop = cropAreaTop;
    _cropAreaBottom = cropAreaBottom;
    
    [self setNeedsDisplay];
}

@end

這里 layer 有幾個屬性 cropAreaLeft拧略、cropAreaRight芦岂、cropAreaTop、cropAreaBottom垫蛆,從命名上可以看出這幾個屬性定義了這個 layer 上繪制的白邊框裁剪區(qū)域的坐標信息禽最。還暴露了一個方法用于配置這四個屬性腺怯。

然后在 CAShapeLayer 內(nèi)部,重點在于復寫 drawInContext 方法川无,這個方法負責直接在圖層上繪圖呛占,復寫的方法主要做的事情是根據(jù)上面四個屬性 cropAreaLeft、cropAreaRight懦趋、cropAreaTop晾虑、cropAreaBottom 繪制出封閉的四條線,這樣就能表示裁剪區(qū)域的邊界了仅叫。

要注意的是 drawInContext 方法不能手動顯示調(diào)用帜篇,必須通過調(diào)用 setNeedsDisplay 或者 setNeedsDisplayInRect 讓系統(tǒng)自動調(diào)該方法。

在裁剪頁面里诫咱,我們放置了一個 cropView笙隙,然后將自定義的 CAShaplayer 加入到這個 view 上

    self.cropView.layer.sublayers = nil;
    YasicClipAreaLayer * layer = [[YasicClipAreaLayer alloc] init];
    
    CGRect cropframe = CGRectMake(self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight);
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.cropView.frame cornerRadius:0];
    UIBezierPath * cropPath = [UIBezierPath bezierPathWithRect:cropframe];
    [path appendPath:cropPath];
    layer.path = path.CGPath;
    
    layer.fillRule = kCAFillRuleEvenOdd;
    layer.fillColor = [[UIColor blackColor] CGColor];
    layer.opacity = 0.5;
    
    layer.frame = self.cropView.bounds;
    [layer setCropAreaLeft:self.cropAreaX CropAreaTop:self.cropAreaY CropAreaRight:self.cropAreaX + self.cropAreaWidth CropAreaBottom:self.cropAreaY + self.cropAreaHeight];
    [self.cropView.layer addSublayer:layer];
    [self.view bringSubviewToFront:self.cropView];

這里主要是為了用自定義的 CAShapelayer 產(chǎn)生出空心遮罩的效果,從而出現(xiàn)中心的裁剪區(qū)域高亮而四周非裁剪區(qū)域有蒙層的效果坎缭,示意圖如下

image.png

所以首先確定了 cashapelayer 的大小為 cropview 的大小竟痰,生成一個對應的 UIBezierPath,然后根據(jù)裁剪區(qū)域的大刑秃簟(由 self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight 確定)生成空心遮罩的內(nèi)圈 UIBezierPath坏快,

CGRect cropframe = CGRectMake(self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight);
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.cropView.frame cornerRadius:0];
    UIBezierPath * cropPath = [UIBezierPath bezierPathWithRect:cropframe];
    [path appendPath:cropPath];
    layer.path = path.CGPath;

然后將這個 path 配置給 CAShapeLayer,并將 CAShapeLayer 的 fillRule 配置為 kCAFillRuleEvenOdd

    layer.fillRule = kCAFillRuleEvenOdd;
    layer.fillColor = [[UIColor blackColor] CGColor];
    layer.opacity = 0.5;
    layer.frame = self.cropView.bounds;

其中 fillRule 屬性表示使用哪一種算法去判斷畫布上的某區(qū)域是否屬于該圖形“內(nèi)部”憎夷,內(nèi)部區(qū)域?qū)⒈惶畛漕伾Ш瑁饕袃煞N方式

  • kCAFillRuleNonZero,這種算法判斷規(guī)則是岭接,如果從某一點射出任意方向射線富拗,與對應 Layer 交點為 0 則不在 Layer 內(nèi)臼予,大于 0 則在 畫布內(nèi)
  • kCAFillRuleEvenOdd 如果從某一點射出任意射線鸣戴,與對應 Layer 交點為偶數(shù)則在畫布內(nèi),否則不在畫布內(nèi)

再給 CAShapeLayer 設(shè)置蒙層顏色為透明度 0.5 的黑色粘拾,就可以實現(xiàn)空心蒙層效果了窄锅。

最后就是設(shè)置 layer 的四個屬性并繪制內(nèi)邊框的白色邊線。

    [layer setCropAreaLeft:self.cropAreaX CropAreaTop:self.cropAreaY CropAreaRight:self.cropAreaX + self.cropAreaWidth CropAreaBottom:self.cropAreaY + self.cropAreaHeight];
    [self.cropView.layer addSublayer:layer];
    [self.view bringSubviewToFront:self.cropView];

合理放置圖片

到這一步我們正確顯示了圖片缰雇,也正確顯示出了裁剪區(qū)域入偷,但是我們沒有將二者的約束關(guān)系建立起來,因此可能會出現(xiàn)下面這樣的情況

image.png

可以看到這里由于這張圖片的 width 遠大于 height械哟,因此會在裁剪區(qū)域內(nèi)出現(xiàn)黑色區(qū)域疏之,這對用戶來說是一種不好的體驗,同時也會影響到我們后面的裁剪步驟暇咆,究其原因是因為我們沒有針對裁剪區(qū)域的寬高來放置 UIImageView锋爪,我們希望最理想的效果是丙曙,能在裁剪區(qū)域內(nèi)實現(xiàn)類似 UIViewContentModeScaleAspectFill 的效果,也就是圖片保持比例地鋪滿裁剪區(qū)域强饮,并允許部分內(nèi)容超出裁剪區(qū)域添诉,這就要求

  • 當圖片寬與裁剪區(qū)域?qū)捴却笥趫D片高與裁剪區(qū)域高之比時钢拧,將圖片高鋪滿裁剪區(qū)域高,圖片寬成比例放大
  • 當圖片高與裁剪區(qū)域高之比大于圖片寬與裁剪區(qū)域?qū)捴葧r索抓,將圖片寬鋪滿裁剪區(qū)域?qū)挘瑘D片高成比例方法

這里我們用到 Masonry 來做這些布局操作

    CGFloat tempWidth = 0.0;
    CGFloat tempHeight = 0.0;
    
    if (self.targetImage.size.width/self.cropAreaWidth <= self.targetImage.size.height/self.cropAreaHeight) {
        tempWidth = self.cropAreaWidth;
        tempHeight = (tempWidth/self.targetImage.size.width) * self.targetImage.size.height;
    } else if (self.targetImage.size.width/self.cropAreaWidth > self.targetImage.size.height/self.cropAreaHeight) {
        tempHeight = self.cropAreaHeight;
        tempWidth = (tempHeight/self.targetImage.size.height) * self.targetImage.size.width;
    }
    
    [self.bigImageView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.cropAreaX - (tempWidth - self.cropAreaWidth)/2);
        make.top.mas_equalTo(self.cropAreaY - (tempHeight - self.cropAreaHeight)/2);
        make.width.mas_equalTo(tempWidth);
        make.height.mas_equalTo(tempHeight);
    }];

可以看到毯炮,我們進行了兩步判斷逼肯,從而獲得合適的寬高值,然后將圖片進行布局桃煎,在自動布局時將圖片中心與裁剪區(qū)域中心重合汉矿,最后我們會得到下面的效果圖

image.png

支持移動和縮放圖片

正如上面所講,由于圖片在裁剪區(qū)域內(nèi)是以類似 UIViewContentModeScaleAspectFill 的方式放置的备禀,很可能出現(xiàn)部分內(nèi)容溢出裁剪區(qū)域洲拇,因此我們要讓圖片能支持動態(tài)移動和縮放,從而使用戶能靈活地裁剪圖片的內(nèi)容曲尸。

具體實現(xiàn)上赋续,我們其實是在 cropview 上加上手勢,間接操作圖片的尺寸和位置另患,這樣有助于后面我們實現(xiàn)動態(tài)改變裁剪區(qū)域的實現(xiàn)纽乱。

縮放功能

這里實現(xiàn)縮放的原理實際是對放置圖片的 UIImageView 的 frame 進行修改,首先我們要記錄下最初的 UIImageView 的 frame

self.originalFrame = CGRectMake(self.cropAreaX - (tempWidth - self.cropAreaWidth)/2, self.cropAreaY - (tempHeight - self.cropAreaHeight)/2, tempWidth, tempHeight);

然后為 cropView 添加手勢

    // 捏合手勢
    UIPinchGestureRecognizer *pinGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleCenterPinGesture:)];
    [self.view addGestureRecognizer:pinGesture];

然后是手勢處理函數(shù)

-(void)handleCenterPinGesture:(UIPinchGestureRecognizer *)pinGesture
{
    CGFloat scaleRation = 3;
    UIView * view = self.bigImageView;
    
    // 縮放開始與縮放中
    if (pinGesture.state == UIGestureRecognizerStateBegan || pinGesture.state == UIGestureRecognizerStateChanged) {
        // 移動縮放中心到手指中心
        CGPoint pinchCenter = [pinGesture locationInView:view.superview];
        CGFloat distanceX = view.frame.origin.x - pinchCenter.x;
        CGFloat distanceY = view.frame.origin.y - pinchCenter.y;
        CGFloat scaledDistanceX = distanceX * pinGesture.scale;
        CGFloat scaledDistanceY = distanceY * pinGesture.scale;
        CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);
        view.frame = newFrame;
        pinGesture.scale = 1;
    }
    
    // 縮放結(jié)束
    if (pinGesture.state == UIGestureRecognizerStateEnded) {
        CGFloat ration =  view.frame.size.width / self.originalFrame.size.width;
        
        // 縮放過大
        if (ration > 5) {
            CGRect newFrame = CGRectMake(0, 0, self.originalFrame.size.width * scaleRation, self.originalFrame.size.height * scaleRation);
            view.frame = newFrame;
        }
        
        // 縮放過小
        if (ration < 0.25) {
            view.frame = self.originalFrame;
        }
        // 對圖片進行位置修正
        CGRect resetPosition = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
        
        if (resetPosition.origin.x >= self.cropAreaX) {
            resetPosition.origin.x = self.cropAreaX;
        }
        if (resetPosition.origin.y >= self.cropAreaY) {
            resetPosition.origin.y = self.cropAreaY;
        }
        if (resetPosition.size.width + resetPosition.origin.x < self.cropAreaX + self.cropAreaWidth) {
            CGFloat movedLeftX = fabs(resetPosition.size.width + resetPosition.origin.x - (self.cropAreaX + self.cropAreaWidth));
            resetPosition.origin.x += movedLeftX;
        }
        if (resetPosition.size.height + resetPosition.origin.y < self.cropAreaY + self.cropAreaHeight) {
            CGFloat moveUpY = fabs(resetPosition.size.height + resetPosition.origin.y - (self.cropAreaY + self.cropAreaHeight));
            resetPosition.origin.y += moveUpY;
        }
        view.frame = resetPosition;
        
        // 對圖片縮放進行比例修正昆箕,防止過小
        if (self.cropAreaX < self.bigImageView.frame.origin.x
            || ((self.cropAreaX + self.cropAreaWidth) > self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width)
            || self.cropAreaY < self.bigImageView.frame.origin.y
            || ((self.cropAreaY + self.cropAreaHeight) > self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height)) {
            view.frame = self.originalFrame;
        }
    }
}

在手勢處理時鸦列,要注意,為了能跟隨用戶捏合手勢的中心進行縮放鹏倘,我們要在手勢過程中移動縮放中心到手指中心薯嗤,這里我們判斷了 pinGesture 的 state 來確定手勢開始、進行中和結(jié)束階段纤泵。

    if (pinGesture.state == UIGestureRecognizerStateBegan || pinGesture.state == UIGestureRecognizerStateChanged) {
        // 移動縮放中心到手指中心
        CGPoint pinchCenter = [pinGesture locationInView:view.superview];
        CGFloat distanceX = view.frame.origin.x - pinchCenter.x;
        CGFloat distanceY = view.frame.origin.y - pinchCenter.y;
        CGFloat scaledDistanceX = distanceX * pinGesture.scale;
        CGFloat scaledDistanceY = distanceY * pinGesture.scale;
        CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);
        view.frame = newFrame;
        pinGesture.scale = 1;
    }

pinchCenter 就是捏合手勢的中心骆姐,我們獲取到當前圖片 view 的 frame,然后計算當前 view 與手勢中心的 x捏题、y 坐標差玻褪,再根據(jù)手勢縮放值 scale,創(chuàng)建出新的 frame

        CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);

這個 frame 的中心坐標就在縮放手勢的中心公荧,將新的 frame 賦值給圖片 view带射,從而實現(xiàn)依據(jù)手勢中心進行縮放的效果。

而在手勢結(jié)束階段循狰,我們要對圖片縮放進行邊界保護窟社,既不能放大過大捻浦,也不能縮小過小。

CGFloat ration =  view.frame.size.width / self.originalFrame.size.width;
        
        // 縮放過大
        if (ration > 5) {
            CGRect newFrame = CGRectMake(0, 0, self.originalFrame.size.width * scaleRation, self.originalFrame.size.height * scaleRation);
            view.frame = newFrame;
        }
        
        // 縮放過小
        if (ration < 0.25) {
            view.frame = self.originalFrame;
        }

同時縮放后如果圖片與裁剪區(qū)域出現(xiàn)了空白區(qū)域桥爽,還要對圖片的位置進行修正以保證圖片始終是覆蓋全裁剪區(qū)域的朱灿。

// 對圖片進行位置修正
        CGRect resetPosition = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
        
        if (resetPosition.origin.x >= self.cropAreaX) {
            resetPosition.origin.x = self.cropAreaX;
        }
        if (resetPosition.origin.y >= self.cropAreaY) {
            resetPosition.origin.y = self.cropAreaY;
        }
        if (resetPosition.size.width + resetPosition.origin.x < self.cropAreaX + self.cropAreaWidth) {
            CGFloat movedLeftX = fabs(resetPosition.size.width + resetPosition.origin.x - (self.cropAreaX + self.cropAreaWidth));
            resetPosition.origin.x += movedLeftX;
        }
        if (resetPosition.size.height + resetPosition.origin.y < self.cropAreaY + self.cropAreaHeight) {
            CGFloat moveUpY = fabs(resetPosition.size.height + resetPosition.origin.y - (self.cropAreaY + self.cropAreaHeight));
            resetPosition.origin.y += moveUpY;
        }
        view.frame = resetPosition;

這里我們通過生成當前圖片的 CGRect,與裁剪區(qū)域的邊界進行如下比較

  • 圖片左邊線大于裁剪區(qū)域左邊線時圖片移動到裁剪區(qū)域 x 值
  • 圖片上邊線大于裁剪區(qū)域上邊線時圖片移動到裁剪區(qū)域 y 值
  • 圖片右邊線小于裁剪區(qū)域右邊線時圖片右貼裁剪區(qū)域右邊線
  • 圖片下邊線小于裁剪區(qū)域右邊線時圖片下貼裁剪區(qū)域下邊線

進行這番操作后钠四,可能會出現(xiàn)由于圖片過小無法鋪滿裁剪區(qū)域的情況盗扒,如下圖所示

image.png

因此還需要再次對圖片尺寸進行修正

        // 對圖片縮放進行比例修正,防止過小
        if (self.cropAreaX < self.bigImageView.frame.origin.x
            || ((self.cropAreaX + self.cropAreaWidth) > self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width)
            || self.cropAreaY < self.bigImageView.frame.origin.y
            || ((self.cropAreaY + self.cropAreaHeight) > self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height)) {
            view.frame = self.originalFrame;
        }

這樣就實現(xiàn)了縮放功能缀去。

移動功能

相比于縮放侣灶,移動功能實現(xiàn)就簡單了,只需要在 cropview 上添加 UIPanGestureRecognizer缕碎,然后在回調(diào)方法里拿到需要移動的距離褥影,修改 UIImageView 的 center 就可以了。

    CGPoint translation = [panGesture translationInView:view.superview];
    [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
                [panGesture setTranslation:CGPointZero inView:view.superview];

但是同樣為了保證移動后的圖片不會與裁剪區(qū)域出現(xiàn)空白甚至是超出裁剪區(qū)域咏雌,這里更新了圖片位置后凡怎,在手勢結(jié)束時還要對圖片進行位置修正

                CGRect currentFrame = view.frame;
                
                if (currentFrame.origin.x >= self.cropAreaX) {
                    currentFrame.origin.x = self.cropAreaX;
                    
                }
                if (currentFrame.origin.y >= self.cropAreaY) {
                    currentFrame.origin.y = self.cropAreaY;
                }
                if (currentFrame.size.width + currentFrame.origin.x < self.cropAreaX + self.cropAreaWidth) {
                    CGFloat movedLeftX = fabs(currentFrame.size.width + currentFrame.origin.x - (self.cropAreaX + self.cropAreaWidth));
                    currentFrame.origin.x += movedLeftX;
                }
                if (currentFrame.size.height + currentFrame.origin.y < self.cropAreaY + self.cropAreaHeight) {
                    CGFloat moveUpY = fabs(currentFrame.size.height + currentFrame.origin.y - (self.cropAreaY + self.cropAreaHeight));
                    currentFrame.origin.y += moveUpY;
                }
                [UIView animateWithDuration:0.3 animations:^{
                    
                    [view setFrame:currentFrame];
                }];

可以看到,這里做的位置檢查與縮放時做的檢查是一樣的赊抖,只是由于不會改變圖片尺寸所以這里不需要進行尺寸修正统倒。

支持手勢改變裁剪區(qū)域

接下來就是動態(tài)裁剪圖片的核心內(nèi)容了,其實原理也很簡單氛雪,只要在上面的移動手勢處理函數(shù)中房匆,進行一些判斷,決定是移動圖片位置還是改變裁剪區(qū)域报亩,也就是自定義的 CAShapeLayer 的繪制方框的尺寸就可以了浴鸿。

首先我們定義一個枚舉,用來表示當前應當操作的是圖片還是裁剪區(qū)域的邊線

typedef NS_ENUM(NSInteger, ACTIVEGESTUREVIEW) {
    CROPVIEWLEFT,
    CROPVIEWRIGHT,
    CROPVIEWTOP,
    CROPVIEWBOTTOM,
    BIGIMAGEVIEW
};

它們分別表示觸發(fā)對象為裁剪區(qū)域左邊線弦追、右邊線岳链、上邊線、下邊線以及 UIImageView

然后我們定義一個枚舉屬性

@property(assign, nonatomic) ACTIVEGESTUREVIEW activeGestureView;

判斷操作對象的標準是當前的手勢所觸發(fā)的位置是在邊線上還是在非邊線上骗卜,因此我們需要知道手勢觸發(fā)時的坐標宠页,要知道這一點就需要我們繼承一個 UIPanGestureRecognizer 并覆寫一些方法

@interface YasicPanGestureRecognizer : UIPanGestureRecognizer

@property(assign, nonatomic) CGPoint beginPoint;
@property(assign, nonatomic) CGPoint movePoint;

-(instancetype)initWithTarget:(id)target action:(SEL)action inview:(UIView*)view;

@end

@interface YasicPanGestureRecognizer()

@property(strong, nonatomic) UIView *targetView;

@end

@implementation YasicPanGestureRecognizer

-(instancetype)initWithTarget:(id)target action:(SEL)action inview:(UIView*)view{
    
    self = [super initWithTarget:target action:action];
    if(self) {
        self.targetView = view;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{
    
    [super touchesBegan:touches withEvent:event];
    UITouch *touch = [touches anyObject];
    self.beginPoint = [touch locationInView:self.targetView];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    UITouch *touch = [touches anyObject];
    self.movePoint = [touch locationInView:self.targetView];
}

@end

可以看到,我們首先傳入了一個 view寇仓,用于將手勢觸發(fā)的位置轉(zhuǎn)換為 view 中的坐標值。在 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{ 方法中我們得到了手勢開始時的觸發(fā)點 beginPoint烤宙,在 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 方法中我們獲得了手勢進行時的觸發(fā)點 movePoint遍烦。

自定義完 UIPanGestureRecognizer 后我們將其加到 cropview 上并把 cropview 作為參數(shù)傳給 UIPanGestureRecognizer

    // 拖動手勢
    YasicPanGestureRecognizer *panGesture = [[YasicPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDynamicPanGesture:) inview:self.cropView];
    [self.cropView addGestureRecognizer:panGesture];

接下來就是處理手勢的函數(shù),這里我們可以將整個過程分為三個步驟躺枕,開始時 -> 進行時 -> 結(jié)束時服猪。

  • 手勢開始時

在這里我們要根據(jù)手勢的 beginPoint 判斷觸發(fā)對象是邊線還是 UIImageView

// 開始滑動時判斷滑動對象是 ImageView 還是 Layer 上的 Line
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        if (beginPoint.x >= self.cropAreaX - judgeWidth && beginPoint.x <= self.cropAreaX + judgeWidth && beginPoint.y >= self.cropAreaY && beginPoint.y <= self.cropAreaY + self.cropAreaHeight && self.cropAreaWidth >= 50) {
            self.activeGestureView = CROPVIEWLEFT;
        } else if (beginPoint.x >= self.cropAreaX + self.cropAreaWidth - judgeWidth && beginPoint.x <= self.cropAreaX + self.cropAreaWidth + judgeWidth && beginPoint.y >= self.cropAreaY && beginPoint.y <= self.cropAreaY + self.cropAreaHeight &&  self.cropAreaWidth >= 50) {
            self.activeGestureView = CROPVIEWRIGHT;
        } else if (beginPoint.y >= self.cropAreaY - judgeWidth && beginPoint.y <= self.cropAreaY + judgeWidth && beginPoint.x >= self.cropAreaX && beginPoint.x <= self.cropAreaX + self.cropAreaWidth && self.cropAreaHeight >= 50) {
            self.activeGestureView = CROPVIEWTOP;
        } else if (beginPoint.y >= self.cropAreaY + self.cropAreaHeight - judgeWidth && beginPoint.y <= self.cropAreaY + self.cropAreaHeight + judgeWidth && beginPoint.x >= self.cropAreaX && beginPoint.x <= self.cropAreaX + self.cropAreaWidth && self.cropAreaHeight >= 50) {
            self.activeGestureView = CROPVIEWBOTTOM;
        } else {
            self.activeGestureView = BIGIMAGEVIEW;
            [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
            [panGesture setTranslation:CGPointZero inView:view.superview];
        }
    }
  • 手勢進行時

在這里供填,如果觸發(fā)對象是邊線,則計算邊線需要移動的距離和方向罢猪,以及對于邊界條件的限制以防止邊線之間交叉錯位的情況近她,具體來說就是獲得坐標差值,更新 cropAreaX膳帕、cropAreaWidth 等值粘捎,然后更新 CAShapeLayer 上的空心蒙層。

如果觸發(fā)對象是 UIImageView 則只需要將其位置進行改變即可危彩。

// 滑動過程中進行位置改變
    if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGFloat diff = 0;
        switch (self.activeGestureView) {
            case CROPVIEWLEFT: {
                diff = movePoint.x - self.cropAreaX;
                if (diff >= 0 && self.cropAreaWidth > 50) {
                    self.cropAreaWidth -= diff;
                    self.cropAreaX += diff;
                } else if (diff < 0 && self.cropAreaX > self.bigImageView.frame.origin.x && self.cropAreaX >= 15) {
                    self.cropAreaWidth -= diff;
                    self.cropAreaX += diff;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWRIGHT: {
                diff = movePoint.x - self.cropAreaX - self.cropAreaWidth;
                if (diff >= 0 && (self.cropAreaX + self.cropAreaWidth) < MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15)){
                    self.cropAreaWidth += diff;
                } else if (diff < 0 && self.cropAreaWidth >= 50) {
                    self.cropAreaWidth += diff;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWTOP: {
                diff = movePoint.y - self.cropAreaY;
                if (diff >= 0 && self.cropAreaHeight > 50) {
                    self.cropAreaHeight -= diff;
                    self.cropAreaY += diff;
                } else if (diff < 0 && self.cropAreaY > self.bigImageView.frame.origin.y && self.cropAreaY >= 15) {
                    self.cropAreaHeight -= diff;
                    self.cropAreaY += diff;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWBOTTOM: {
                diff = movePoint.y - self.cropAreaY - self.cropAreaHeight;
                if (diff >= 0 && (self.cropAreaY + self.cropAreaHeight) < MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15)){
                    self.cropAreaHeight += diff;
                } else if (diff < 0 && self.cropAreaHeight >= 50) {
                    self.cropAreaHeight += diff;
                }
                [self setUpCropLayer];
                break;
            }
            case BIGIMAGEVIEW: {
                [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
                [panGesture setTranslation:CGPointZero inView:view.superview];
                break;
            }
            default:
                break;
        }
    }
  • 手勢結(jié)束時

手勢結(jié)束時攒磨,我們需要對位置進行修正。如果是裁剪區(qū)域邊線汤徽,則要判斷左右娩缰、上下邊線之間的距離是否過短,邊線是否超出 UIImageView 的范圍等谒府。如果左右邊線距離過短則設(shè)置最小裁剪寬度拼坎,如果上線邊線距離過短則設(shè)置最小裁剪高度,如果左邊線超出了 UIImageView 左邊線則需要緊貼 UIImageView 的左邊線完疫,并更新裁剪區(qū)域?qū)挾妊菅鳎源祟愅啤H缓蟾?CAShapeLayer 上的空心蒙層即可趋惨。

如果是 UIImageView 則跟上一節(jié)一樣要保證圖片不會與裁剪區(qū)域出現(xiàn)空白鸟顺。

    // 滑動結(jié)束后進行位置修正
    if (panGesture.state == UIGestureRecognizerStateEnded) {
        switch (self.activeGestureView) {
            case CROPVIEWLEFT: {
                if (self.cropAreaWidth < 50) {
                    self.cropAreaX -= 50 - self.cropAreaWidth;
                    self.cropAreaWidth = 50;
                }
                if (self.cropAreaX < MAX(self.bigImageView.frame.origin.x, 15)) {
                    CGFloat temp = self.cropAreaX + self.cropAreaWidth;
                    self.cropAreaX = MAX(self.bigImageView.frame.origin.x, 15);
                    self.cropAreaWidth = temp - self.cropAreaX;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWRIGHT: {
                if (self.cropAreaWidth < 50) {
                    self.cropAreaWidth = 50;
                }
                if (self.cropAreaX + self.cropAreaWidth > MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15)) {
                    self.cropAreaWidth = MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15) - self.cropAreaX;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWTOP: {
                if (self.cropAreaHeight < 50) {
                    self.cropAreaY -= 50 - self.cropAreaHeight;
                    self.cropAreaHeight = 50;
                }
                if (self.cropAreaY < MAX(self.bigImageView.frame.origin.y, 15)) {
                    CGFloat temp = self.cropAreaY + self.cropAreaHeight;
                    self.cropAreaY = MAX(self.bigImageView.frame.origin.y, 15);
                    self.cropAreaHeight = temp - self.cropAreaY;
                }
                [self setUpCropLayer];
                break;
            }
            case CROPVIEWBOTTOM: {
                if (self.cropAreaHeight < 50) {
                    self.cropAreaHeight = 50;
                }
                if (self.cropAreaY + self.cropAreaHeight > MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15)) {
                    self.cropAreaHeight = MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15) - self.cropAreaY;
                }
                [self setUpCropLayer];
                break;
            }
            case BIGIMAGEVIEW: {
                CGRect currentFrame = view.frame;
                
                if (currentFrame.origin.x >= self.cropAreaX) {
                    currentFrame.origin.x = self.cropAreaX;
                    
                }
                if (currentFrame.origin.y >= self.cropAreaY) {
                    currentFrame.origin.y = self.cropAreaY;
                }
                if (currentFrame.size.width + currentFrame.origin.x < self.cropAreaX + self.cropAreaWidth) {
                    CGFloat movedLeftX = fabs(currentFrame.size.width + currentFrame.origin.x - (self.cropAreaX + self.cropAreaWidth));
                    currentFrame.origin.x += movedLeftX;
                }
                if (currentFrame.size.height + currentFrame.origin.y < self.cropAreaY + self.cropAreaHeight) {
                    CGFloat moveUpY = fabs(currentFrame.size.height + currentFrame.origin.y - (self.cropAreaY + self.cropAreaHeight));
                    currentFrame.origin.y += moveUpY;
                }
                [UIView animateWithDuration:0.3 animations:^{
                    
                    [view setFrame:currentFrame];
                }];
                break;
            }
            default:
                break;
        }
    }

進行圖片裁剪并獲得裁剪后的圖片

最后一步就是對圖片進行裁剪了。首先確定對圖片的縮放尺寸 imageScale

    CGFloat imageScale = MIN(self.bigImageView.frame.size.width/self.targetImage.size.width, self.bigImageView.frame.size.height/self.targetImage.size.height);

然后將 cropView 的裁剪區(qū)域?qū)?UIImageView 上器虾,再除以縮放值讯嫂,即可得到對應 UIImage 上需要裁剪的區(qū)域

    CGFloat cropX = (self.cropAreaX - self.bigImageView.frame.origin.x)/imageScale;
    CGFloat cropY = (self.cropAreaY - self.bigImageView.frame.origin.y)/imageScale;
    CGFloat cropWidth = self.cropAreaWidth/imageScale;
    CGFloat cropHeight = self.cropAreaHeight/imageScale;
    CGRect cropRect = CGRectMake(cropX, cropY, cropWidth, cropHeight);

最后調(diào)用 CoreGraphic 的方法,將圖片對應區(qū)域的數(shù)據(jù)取出來生成新的圖片兆沙,就是我們需要的裁剪后的圖片了欧芽。

    CGImageRef sourceImageRef = [self.targetImage CGImage];
    CGImageRef newImageRef = CGImageCreateWithImageInRect(sourceImageRef, cropRect);
    UIImage *newImage = [UIImage imageWithCGImage:newImageRef];

GitHub 代碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葛圃,隨后出現(xiàn)的幾起案子千扔,更是在濱河造成了極大的恐慌,老刑警劉巖库正,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件曲楚,死亡現(xiàn)場離奇詭異,居然都是意外死亡褥符,警方通過查閱死者的電腦和手機龙誊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喷楣,“玉大人趟大,你說我怎么就攤上這事鹤树。” “怎么了逊朽?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵罕伯,是天一觀的道長。 經(jīng)常有香客問我叽讳,道長追他,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任绽榛,我火速辦了婚禮湿酸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灭美。我一直安慰自己推溃,他們只是感情好,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布届腐。 她就那樣靜靜地躺著铁坎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪犁苏。 梳的紋絲不亂的頭發(fā)上硬萍,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機與錄音围详,去河邊找鬼朴乖。 笑死,一個胖子當著我的面吹牛助赞,可吹牛的內(nèi)容都是我干的买羞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼雹食,長吁一口氣:“原來是場噩夢啊……” “哼畜普!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起群叶,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吃挑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后街立,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舶衬,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年几晤,在試婚紗的時候發(fā)現(xiàn)自己被綠了约炎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蟹瘾,死狀恐怖圾浅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情憾朴,我是刑警寧澤狸捕,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站众雷,受9級特大地震影響灸拍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砾省,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一鸡岗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧编兄,春花似錦轩性、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至件舵,卻和暖如春卸察,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铅祸。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工坑质, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人临梗。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓涡扼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親夜焦。 傳聞我的和親對象是個殘疾皇子壳澳,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容