iOS 动态对UIImageView加载到的图片进行偏移裁剪处理

关键点:图片偏移重绘、图片重绘时机处理

  1. 图片偏移重绘问题
  • 原因:UIImageView的现有填充模式已经不能满足应用需求,需自定义偏移量结合填充模式实现效果
  • 方法:按需求样式重绘,设置偏移量比例,偏移阈值,对图片进行裁剪。
  1. 重绘时机问题
  • 选择:UIImageView自带的setImage方法,这是获取图片和进行重绘渲染的关口,从这里处理最恰当
  • 方法:利用iOS底层方法,在运行时(runtime)进行方法交换,属性绑定等。
  1. 补充
  • 因为运行时编程代码复杂冗余多,可以考虑运用第三方库方法解决这个问题,此处选择德国软件工程师Philipp Rentzsch开发的一个开源库[JRSwizzle](https://github.com/rentzsch/jrswizzle)辅助完成。
  • 使用cocoapod集成JRSwizzle库

核心代码:

  • 宏定义再次简化运行时代码

    #import <JRSwizzle/JRSwizzle.h>
    /* 方法交换宏*/
    #define MYSwizzle(prefix, SEL)
    do {
    NSError *error = nil;
    [[self class] jr_swizzleMethod: @selector(SEL)
    withMethod: @selector(prefix####SEL)error:&error];
    if (error) {NSLogError(@ "%@ swizzle %@ with %@ failed.",
    NSStringFromClass(self),
    NSStringFromSelector(@selector(SEL)),
    NSStringFromSelector(@selector(prefix##
    ##SEL)));}
    } while (0)

    /添加属性宏/
    #define MY_ASSOCIATE_OBJECT(getter, setter, association, type)
    - (void)setter: (type)object {
    [self willChangeValueForKey: @#getter];
    objc_setAssociatedObject(self, cmd, object, OBJC_ASSOCIATION##association);
    [self didChangeValueForKey: @#getter];
    }
    -(type)getter {
    return objc_getAssociatedObject(self, @selector(setter:));
    }

  • 定义好红代码之后,写一个UIImageView的分类方法,记录加载原始图片imgOriginal,以及便宜设置类MYImaeViewOffset;

  • 首先定义偏移重绘配置类MYImaeViewOffset:

    /**

    • 用于判断和控制UIImageView设置图片时候的偏移量的数据类。
    • ratioOffsetX x轴方向的偏移量比例.
    • ratioOffsetY y轴方向的偏移量比例.
    • ratioThreshold 进行偏移重绘的阈值. 实值为 imgv_w / imgv_h.
    • 如果设定ratioThreshold = 1.8, 而当前UIImageView.size = {100, 50}
    • 则实值为 2。此时超过偏移阈值,需对图片进行偏移裁剪重绘。
    • @note ignoreThreshold 注意如果此值为YES,则忽略阈值,需要重绘。
      */

    typedef NS_OPTIONS(NSUInteger, MYImageViewAlignmentMask) {
    MYImageViewAlignmentMaskCenter = 1 << 0,

      MYImageViewAlignmentMaskLeft       = 1 << 1,
      MYImageViewAlignmentMaskRight      = 1 << 2,
      MYImageViewAlignmentMaskTop        = 1 << 3,
      MYImageViewAlignmentMaskBottom     = 1 << 4,
    

    };

    @interface MYImageViewOffset : NSObject
    @property (nonatomic,assign) CGFloat ratioOffsetX;
    @property (nonatomic,assign) CGFloat ratioOffsetY;
    @property (nonatomic,assign) CGFloat ratioThreshold;//阈值
    @property (nonatomic,assign) BOOL ignoreThreshold;//忽略阈值(默认阈值1.3)长图w/h >= 1.3
    @property (nonatomic,assign) CGFloat cornerRadius;//圆角半径
    @property (nonatomic,assign) MYImageViewAlignmentMask alignment;//对齐方式。

    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY;
    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY cornerRadius:(CGFloat)cornerRadius;
    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY cornerRadius:(CGFloat)cornerRadius align:(MYImageViewAlignmentMask)aligment;
      @end

    @implementation MYImageViewOffset

    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY {
      return [MYImageViewOffset offsetRatio:ratioX ratioY:ratioY cornerRadius:0];
      }
    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY cornerRadius:(CGFloat)cornerRadius {
      MYImageViewOffset *offset = [MYImageViewOffset offsetRatio:ratioX ratioY:ratioY cornerRadius:cornerRadius align:MYImageViewAlignmentMaskCenter];
      return offset;
      }
    • (instancetype)offsetRatio:(CGFloat)ratioX ratioY:(CGFloat)ratioY cornerRadius:(CGFloat)cornerRadius align:(MYImageViewAlignmentMask)aligment {
      MYImageViewOffset *offset = [MYImageViewOffset new];
      offset.ratioOffsetX = ratioX;
      offset.ratioOffsetY = ratioY;
      offset.ratioThreshold = 1.3;//默认阈值。
      offset.cornerRadius = cornerRadius;
      offset.alignment = aligment;
      return offset;
      }
      @end
  • 然后实现UIImageVIew分类方法:

    @interface UIImageView (ImageOffset)
    @property (nonatomic, strong) UIImage *imgOriginal;
    @property (nonatomic, strong) MYImageViewOffset *offset;

    • (void)reloadOffsetJudge;
      @end

    @implementation UIImageView (ImageOffset)
    MY_ASSOCIATE_OBJECT(offset, setOffset, RETAIN, MYImageViewOffset *);
    MY_ASSOCIATE_OBJECT(imgOriginal, setImgOriginal, RETAIN, UIImage *);

    • (void)load {
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      MYSwizzle(MY, setImage:);
      MYSwizzle(MY, layoutSubviews);
      });
      }
    • (void)MY_layoutSubviews {
      if ([self needImageRedrawJudge]) {
      UIImage *imgOffset = [self offsetImage:self.imgOriginal];
      if (imgOffset) {
      [self MY_setImage:imgOffset];
      }
      }
      [self MY_layoutSubviews];
      }

    • (void)MY_setImage:(UIImage *)image {
      [self setImgOriginal:image];
      UIImage *destImg = nil;
      if ([self needImageRedrawJudge] && image) {
      destImg = [self offsetImage:image];
      }else{
      destImg = image;
      }
      [self MY_setImage:destImg];
      }

    • (void)reloadOffsetJudge {
      if ([self needImageRedrawJudge]) {
      NSLogInfo(@"-- layout re");
      UIImage *imgOffset = [self offsetImage:self.imgOriginal];
      if (imgOffset) {
      [self MY_setImage:imgOffset];
      }
      }
      }

    • (UIImage *)offsetImage:(UIImage *)imgOri {
      if (!imgOri) return nil;
      CGFloat offX = self.offset.ratioOffsetX * imgOri.size.width;
      CGFloat offY = self.offset.ratioOffsetY * imgOri.size.height;
      CGRect cutRect = CGRectMake(offX, offY, imgOri.size.width, imgOri.size.height);
      UIImage *newImage = [self image:imgOri cropToRect:cutRect cornerRadius:self.offset.cornerRadius];
      return newImage;
      }

    • (BOOL)needImageRedrawJudge {
      if(self.offset.cornerRadius){
      return YES;
      }
      CGSize imgSize = self.imgOriginal.size;
      if (self.width <= 0 || self.height <= 0) return NO;
      if (imgSize.width / imgSize.height >= 1){
      //如果图片本身是一张横图(w>=h)则不需进行重绘
      return NO;
      }
      if (self.offset) {
      CGFloat ratioThreshold = self.width / self.height;
      if (ratioThreshold > self.offset.ratioThreshold) {
      //图片视图控件的阈值判断(视图控件UIImageView是一个通过了阈值的横框,方可进行重绘。)
      return YES;
      }
      }
      return NO;
      }
      /*
      图片剪裁接口

    • 1.rect超过image.size则返回原图
    • 2.rect可以是size的任何一个子集,比如取中间半区域 CG
      */
    • (UIImage *)image:(UIImage *)imgOri cropToRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius{
      rect.origin.x *= imgOri.scale;
      rect.origin.y *= imgOri.scale;
      rect.size.width *= imgOri.scale;
      rect.size.height *= imgOri.scale;
      UIImage *outputImage = imgOri;
      if(rect.size.width > 0 && rect.size.height > 0){
      CGImageRef imageRef = CGImageCreateWithImageInRect(imgOri.CGImage, rect);
      UIImage *image = [UIImage imageWithCGImage:imageRef scale:imgOri.scale orientation:imgOri.imageOrientation];
      outputImage = image;
      CGImageRelease(imageRef);
      }
      if(self.width && self.height){
      if(cornerRadius > 0){
      CGFloat ratioCR = cornerRadius * (outputImage.size.width / self.width);
      UIGraphicsBeginImageContextWithOptions(outputImage.size, NO, outputImage.scale);
      CGContextRef context = UIGraphicsGetCurrentContext();
      CGRect imgRect = CGRectMake(0, 0, outputImage.size.width, outputImage.size.height);

            CGContextSaveGState(context);
            [[UIBezierPath bezierPathWithRoundedRect:imgRect cornerRadius:ratioCR] addClip];
            [outputImage drawInRect:imgRect];
            CGContextRestoreGState(context);
            UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            outputImage =  roundedImage;
        }
        
        if(self.offset.alignment != MYImageViewAlignmentMaskCenter){
            CGFloat logicH = (outputImage.size.height * self.width)/outputImage.size.width;
            CGFloat logicW = (outputImage.size.width * self.height)/outputImage.size.height;
            CGFloat offsetH = (self.height - logicH);
            CGFloat offsetV = (self.width - logicW);
            BOOL needRedraw = NO;
            if(offsetH > 0){ //横图(只有上下偏移)
                offsetV = 0;
                logicW = self.width;
                if(self.offset.alignment == MYImageViewAlignmentMaskTop){
                    offsetH = 0;
                    needRedraw = YES;
                }else if (self.offset.alignment == MYImageViewAlignmentMaskBottom){
                    needRedraw = YES;
                }
            }
            if (offsetV > 0){ //竖图(只有左右偏移)
                logicH = self.height;
                offsetH = 0;
                if(self.offset.alignment == MYImageViewAlignmentMaskLeft){
                    offsetV = 0;
                    needRedraw = YES;
                }else if (self.offset.alignment == MYImageViewAlignmentMaskRight){
                    needRedraw = YES;
                }
            }
            if(needRedraw){
                UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
                [outputImage drawInRect:CGRectMake(offsetV, offsetH, logicW, logicH)];
                UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();
                outputImage =  resizedImage;
            }
        }
      

      }
      return outputImage;
      }
      @end

补充说明:

说明

  1. 利用swizzle方法交换setImage 和layoutSubviews方法实现,它们将风别走向MY_setImage:和方法My_layoutSubviews从中获取到imgOriginal之后,再判断是否要进行重绘(needImageRedrawJudge),如果要进行重绘,重绘之后得到图片对象 destImg|imgOffset,这时候调用MY_setImage:方法,根据方法交换的逻辑,实际上就是调用了UIImageView的setImage:方法,从而实现拦截图片,并判断和重回图片在设置最终图片的效果。
  2. 至于图片重绘的具体逻辑,可根据具体需求进行灵活变换,此处重在说明如何判断需求和在拦截,以及拦截使用的方法,核心设计知识领域有:宏代码的编程简化、运行时方法、图片重绘
相关推荐
/**书香门第*/8 小时前
Laya ios接入goole广告,搭建环境 1
ios
wakangda15 小时前
React Native 集成 iOS 原生功能
react native·ios·cocoa
crasowas1 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
ii_best2 天前
ios按键精灵脚本开发:ios悬浮窗命令
ios
Code&Ocean2 天前
iOS从Matter的设备认证证书中获取VID和PID
ios·matter·chip
/**书香门第*/2 天前
Laya ios接入goole广告,开始接入 2
ios
恋猫de小郭2 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨3 天前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题3 天前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
二流小码农3 天前
鸿蒙开发:简单了解属性动画
android·ios·harmonyos