IOS 屏幕适配(三)代码实现篇
2. IOS 屏幕适配代码实现
2.1 布局处理
2.1.1 masonary布局适配实例
- 在使用masonary自动布局时,可以根据6s的屏幕设计,设置一个比例系数,比如
//以6/6s为准宽度缩小系数
#define kJLXWidthScale [UIScreen mainScreen].bounds.size.height/375.0
//高度缩小系数
#define kJLXHeightScale [UIScreen mainScreen].bounds.size.height/667.0
- 这样在布局的的时候,可以考虑使用上这个系数设置高度
UIButton *createrButton = [[UIButton alloc] init];
[self.view addSubview:createrButton];
UIEdgeInsets padding = UIEdgeInsetsMake(0, 10, 65, 10);
[createrButton setBackgroundImage:[UIImage imageNamed:@"common_button_pink"] forState:UIControlStateNormal];
[createrButton mas_makeConstraints:^(MASConstraintMaker *make){
make.left.equalTo(self.view.mas_left).with.offset(padding.left);
make.height.equalTo(@(60*kJLXHeightScale));
make.bottom.equalTo(self.view.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(self.view.mas_right).with.offset(-padding.right);
}];
- 这样在5s小屏手机上面,按钮的高度就会根据比例系数来动态调整大小。
2.1.2 Jimu 1.0 用到的布局适配函数
- 之前Jimu 1.0中用到的布局转换主要通过下面这个函数来转换实际的宽度或高度:
横屏下,水平方向适配函数
/// 设备横屏下,水平方向适配·
///
/// - Parameters:
/// - iPhone6Scale: iPhone6 水平方向@2x尺寸
/// - iPadScale: 分辨率比例为768*1024的iPad 水平方向@2x尺寸
/// - Returns: 适配后的尺寸
func layoutHorizontal(iPhone6 iPhone6Scale: Float, iPad iPadScale: Float) -> Float {
let iphoneWidth = iPhone6Scale / 2
let iPadWidth = iPadScale / 2
var newWidth: Float = 0
switch Device.type() {
case .iPhone4:
newWidth = iphoneWidth * (480.0 / 667.0)
case .iPhone5:
newWidth = iphoneWidth * (568.0 / 667.0)
case .iPhone6:
newWidth = iphoneWidth
case .iPhone6p:
newWidth = iphoneWidth * (736.0 / 667.0)
case .iPhoneX:
newWidth = iphoneWidth * ((812.0 - 78) / 667.0)
case .iPhoneXR:
newWidth = iphoneWidth * ((896.0 - 78) / 667.0)
case .iPad_768_1024:
newWidth = iPadWidth
case .iPad_834_1112:
newWidth = iPadWidth * (1112.0 / 1024.0)
case .iPad_1024_1366:
newWidth = iPadWidth * (1366.0 / 1024.0)
}
return newWidth
}
设备横屏下,垂直方向适配函数
/// 设备横屏下,垂直方向适配
///
/// - Parameters:
/// - iPhone6Scale: iPhone6 垂直方向@2x尺寸
/// - iPadScale: 分辨率比例为768*1024的iPad 垂直方向@2x尺寸
/// - Returns: 适配后的尺寸
func layoutVertical(iPhone6 iPhone6Scale: Float, iPad iPadScale: Float) -> Float {
let iphoneHeight = iPhone6Scale / 2
let iPadHeight = iPadScale / 2
var newHeight: Float = 0
switch Device.type() {
case .iPhone4:
newHeight = iphoneHeight * (320.0 / 375.0)
case .iPhone5:
newHeight = iphoneHeight * (320.0 / 375.0)
case .iPhone6:
newHeight = iphoneHeight
case .iPhone6p:
newHeight = iphoneHeight * (414.0 / 375.0)
case .iPhoneX:
newHeight = iphoneHeight * (375.0 / 375.0)
case .iPhoneXR:
newHeight = iphoneHeight * (414.0 / 375.0)
case .iPad_768_1024:
newHeight = iPadHeight
case .iPad_834_1112:
newHeight = iPadHeight * (834.0 / 768.0)
case .iPad_1024_1366:
newHeight = iPadHeight * (1024.0 / 768.0)
}
return newHeight
}
- 这种适配方式,可以满足横屏下适配各种设备,但是所有布局的代码都需要调用这两个函数,浸入性很强。所以需要优化一下。
2.1.3 布局适配优化
2.1.3.1 增加判断设备类型的extension扩展
- 先来看一下之前Jimu 1.0 是通过一个自定义枚举来实现的,这样的不好的地方也是浸入性很强,每个调用的地方都需要用这个枚举值。
/// 获取设备型号
enum Device {
case iPhone4 /// 4/4s 320*480 @2x
case iPhone5 /// 5/5C/5S/SE 320*568 @2x
case iPhone6 /// 6/6S/7/8 375*667 @2x
case iPhone6p /// 6P/6SP/7P/8P 414*736 @3x
case iPhoneX /// X 375*812 @3x
// case iPhoneXS /// XS 375*812 @3x (同X)
case iPhoneXR /// XR 414*896 @2x (放大模式下为 375*812)
// case iPhoneXSMAX /// XSMAX 414*896 @3x (同XR)
case iPad_768_1024 /// iPad(5th generation)/iPad Air/iPad Air2/iPad pro(9.7) 768*1024 @2x
case iPad_834_1112 /// iPad pro(10.5) 834*1112 @2x
case iPad_1024_1366 /// iPad pro(12.9) 1024*1366 @2x
/// 判断具体设备
///
/// - Returns: 具体设备名
static func type() -> Device {
switch screenWidth {
case 480.0:
return .iPhone4
case 568.0:
return .iPhone5
case 667.0:
return .iPhone6
case 736.0:
return .iPhone6p
case 812.0:
return .iPhoneX
case 896.0:
return .iPhoneXR
case 1024.0:
return .iPad_768_1024
case 1112.0:
return .iPad_834_1112
case 1366.0:
return .iPad_1024_1366
default:
return .iPad_768_1024
}
}
/// 判断是否为iPad
///
/// - Returns: true 是, false 否
static func isIPad() -> Bool {
// print("() = \(self.type())")
return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad)
}
static func isIPhone5() -> Bool {
return Device.type() == Device.iPhone5 ? true : false
}
static var safeAreaInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
}
return .zero
}
static var safeScreenWidth: CGFloat {
return UIScreen.main.bounds.width-safeAreaInsets.left-safeAreaInsets.right
}
static var safeScreenHeight: CGFloat {
return UIScreen.main.bounds.height-safeAreaInsets.top-safeAreaInsets.bottom
}
}
- 将上面设备类型判断代码优化为UIDevice的一个类扩展
extension UIDevice {
func Version()->String{
let appVersion: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
return appVersion
}
@objc public class func isiPhoneX() -> Bool {
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 1125, height: 2436)))! {
return true
}
return false
}
public class func isiPhone6PlusBigMode() -> Bool {
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 1125, height: 2001)))! {
return true
}
return false
}
public class func isiPhone6Plus() -> Bool {
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width:1242, height: 2208)))! {
return true
}
return false
}
public class func isiPhone6BigMode() -> Bool{
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 320, height: 568)))! {
return true
}
return false
}
public class func isiPhone6() -> Bool {
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width:750, height: 1334)))! {
return true
}
return false
}
public class func isiPhone5() -> Bool {
if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 640, height: 1136)))! {
return true
}
return false
}
public class func isiOS11() -> Bool {
if #available(iOS 11.0, *) {
return true
} else {
return false
}
}
public class func isiOS10() -> Bool {
if #available(iOS 10.0, *) {
return true
} else {
return false
}
}
public class func isiOS9() -> Bool {
if #available(iOS 9.0, *) {
return true
} else {
return false
}
}
public class func isiOS8() -> Bool {
if #available(iOS 8.0, *) {
return true
} else {
return false
}
}
public class func isAiPad() -> Bool {
if UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad {
return true
}
return false
}
}
- 然后为了简化调用,可以定义一个全局变量
// MARK: - 判断 机型
let isiPhone5 = UIDevice.isiPhone5()
let isiPhone6 = UIDevice.isiPhone6()
let isiPhone6BigModel = UIDevice.isiPhone6BigMode()
let isiPhone6Plus = UIDevice.isiPhone6Plus()
let isiPhone6PlusBigMode = UIDevice.isiPhone6PlusBigMode()
let isiPhoneX = UIDevice.isiPhoneX()
let isIpad = UIDevice.isAiPad()
// MARK: - 系统类型
let kisiOS11 = UIDevice.isiOS11()
let kisiOS10 = UIDevice.isiOS10()
let kisiOS9 = UIDevice.isiOS9()
let kisiOS8 = UIDevice.isiOS8()
- 定义全局变量简化屏幕宽度,高度计算
let screenWidth = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width)
let screenHeight = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width)
let screenBounds = UIScreen.main.bounds
2.1.3.2 增加 NSInteger 类扩展
extension NSInteger {
/// iphone 5 上的大小
/// ? 《*注意运算顺序 -60.i5(-30) 等价于 -(60.i5(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 5 上的大小
/// - Returns: isiPhone5 ? size : CGFloat(self)
func i5(_ size: CGFloat) -> CGFloat {
return isiPhone5 ? size : CGFloat(self)
}
/// iphone 6 放大模式上的大小
/// ? 《*注意运算顺序 -60.i6BigModel(-30) 等价于 -(60.i6BigModel(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 6 放大模式 上的大小
/// - Returns: isiPhone6BigModel ? size : CGFloat(self)
func i6BigModel(_ size: CGFloat) -> CGFloat {
return isiPhone6BigModel ? size : CGFloat(self)
}
/// iphone 6p 放大模式上的大小
/// ? 《*注意运算顺序 -60.i6PBigModel(-30) 等价于 -(60.i6PBigModel(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 6p 放大模式 上的大小
/// - Returns: isiPhone6PlusBigMode ? size : CGFloat(self)
func i6PBigModel(_ size: CGFloat) -> CGFloat {
return isiPhone6PlusBigMode ? size : CGFloat(self)
}
/// iphone x 上的大小
/// ? 《*注意运算顺序 -60.ix(-30) 等价于 -(60.ix(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone x 上的大小
/// - Returns: isiPhoneX ? size / 2.0 : CGFloat(self)
func ix(_ size: CGFloat) -> CGFloat {
return isiPhoneX ? size : CGFloat(self)
}
/// ipad
/// ? 《*注意运算顺序 -60.ipad(-30) 等价于 -(60.ipad(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: ipad 上的大小
/// - Returns: isIpad ? size : CGFloat(self)
func ipad(_ size: CGFloat) -> CGFloat {
return isIpad ? size : CGFloat(self)
}
/// 比例缩放 width
///
/// - Parameter size: origin width
/// - Returns: 比例缩放后的 width 没有除以2.0
func scaleW() -> CGFloat {
return (screenWidth / 375 * CGFloat(self))
}
/// 比例缩放 height result没有除以2.0
///
/// - Parameter size: origin height
/// - Returns: 比例缩放后的 height 没有除以2.0
func scaleH() -> CGFloat {
return (screenHeight / 667 * CGFloat(self))
}
}
2.1.3.3 增加 CGFloat 类扩展
extension CGFloat {
/// iphone 5 上的大小
/// ? 《*注意运算顺序 -60.i5(-30) 等价于 -(60.i5(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 5 上的大小
/// - Returns: isiPhone5 ? size : self
func i5(_ size: CGFloat) -> CGFloat {
return isiPhone5 ? size : self
}
/// iphone 6 放大模式上的大小
/// ? 《*注意运算顺序 -60.i6BigModel(-30) 等价于 -(60.i6BigModel(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 6 放大模式 上的大小
/// - Returns: isiPhone6BigModel ? : self
func i6BigModel(_ size: CGFloat) -> CGFloat {
return isiPhone6BigModel ? size : self
}
/// iphone 6p 放大模式上的大小
/// ? 《*注意运算顺序 -60.i6PBigModel(-30) 等价于 -(60.i6PBigModel(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone 6p 放大模式 上的大小
/// - Returns: isiPhone6PlusBigMode ? size : self
func i6PBigModel(_ size: CGFloat) -> CGFloat {
return isiPhone6PlusBigMode ? size : self
}
/// iphone x上的大小
/// ? 《*注意运算顺序 -60.ix(-30) 等价于 -(60.ix(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: iphone x 上的大小
/// - Returns: isiPhoneX ? size : self
func ix(_ size: CGFloat) -> CGFloat {
return isiPhoneX ? size : self
}
/// ipad 上的大小
/// ? 《*注意运算顺序 -60.ipad(-30) 等价于 -(60.ipad(-30)) 结果为 -(-30) 或者 -60》
///
/// - Parameter size: ipad 上的大小
/// - Returns: isIpad ? size : self
func ipad(_ size: CGFloat) -> CGFloat {
return isIpad ? size : self
}
/// 比例缩放 width
///
/// - Parameter size: origin width
/// - Returns: 比例缩放后的 width 没有除以2.0
func scaleW() -> CGFloat {
return (screenWidth / 375 * self)
}
/// 比例缩放 height
///
/// - Parameter size: origin height
/// - Returns: 比例缩放后的 height 没有除以2.0
func scaleH() -> CGFloat {
return (screenHeight / 667 * self)
}
}
2.1.3.4 增加 Bool 类扩展
extension Bool {
/// iphone 5 上的大小
///
/// - Parameter size: iphone 5 上的大小
/// - Returns: isiPhone5 ? size : self
func i5(_ size: Bool) -> Bool {
return isiPhone5 ? size : self
}
/// iphone 6 放大模式上的大小
///
/// - Parameter size: iphone 6 放大模式 上的大小
/// - Returns: isiPhone6BigModel ? size : self
func i6BigModel(_ size: Bool) -> Bool {
return isiPhone6BigModel ? size : self
}
/// iphone 6p 放大模式上的大小
///
/// - Parameter size: iphone 6p 放大模式 上的大小
/// - Returns: isiPhone6PlusBigMode ? size : self
func i6PBigModel(_ size: Bool) -> Bool {
return isiPhone6PlusBigMode ? size : self
}
/// iphone x 上的大小
///
/// - Parameter size: iphone x 上的大小
/// - Returns: isiPhoneX ? size / 2.0 : self
func ix(_ size: Bool) -> Bool {
return isiPhoneX ? size : self
}
/// ipad
///
/// - Parameter size: ipad 上的大小
/// - Returns: isIpad ? size : self
func ipad(_ size: Bool) -> Bool {
return isIpad ? size : self
}
}
2.2 图片适配处理
- 在项目中经常有这样的需求:如下图,截取一部分拉伸,其他不变
实现代码如下:
UIImage *img = [UIImage imageNamed:@"popup"];
img = [img resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 55) resizingMode:UIImageResizingModeStretch];
self.resizableImgView.image = img;
swift 代码如下
/// 从中间拉伸图片
///
/// - Parameter image: 拉伸之前原始图
/// - Returns: 拉伸后图片
static func stretchFromCenter(image: UIImage?) -> UIImage? {
guard let oriImage = image else {
return nil
}
let result = oriImage.resizableImage(withCapInsets: UIEdgeInsetsMake(oriImage.size.height/2, oriImage.size.width/2, oriImage.size.height/2, oriImage.size.width/2), resizingMode: .stretch)
return result
}
- 平铺图片:即一张小图可以平铺为多张小图
实现代码如下:
UIImage *img = [UIImage imageNamed:@"about"];
img = [img resizableImageWithCapInsets:UIEdgeInsetsMake(0, 11.5, 0, 11) resizingMode:UIImageResizingModeTile];
self.resizableImgView.image = img;
- 通过纯颜色创建图片
/// 通过纯色创建图片
///
/// - Parameter color: 颜色
/// - Returns: 通过纯颜色创建的图片
static func createImage(with color: UIColor) -> UIImage {
let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
UIGraphicsBeginImageContext(rect.size)
let ctx = UIGraphicsGetCurrentContext()
guard let context = ctx else { return UIImage() }
context.setFillColor(color.cgColor)
context.fill(rect)
let theImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return theImage ?? UIImage()
}
2.3 文字适配处理
2.3.1 根据字符串计算宽度,高度
- 自动适配原则上UILabel都是不设置高度的,根据文字内容自动适配高度。这个时候我们经常需要用到根据文字String 字符串来计算整个字符串的宽度和高度。
jimu 1.0 用到的计算方法如下:
extension String {
func calculateSize(_ size: CGSize, font: UIFont) -> CGSize {
let paragraphStyle = NSMutableParagraphStyle()
// paragraphStyle.lineSpacing = 7
paragraphStyle.lineBreakMode = .byCharWrapping
let attributes = [NSAttributedStringKey.font:font, NSAttributedStringKey.paragraphStyle:paragraphStyle.copy()]
let expectedLabelSize = (self as NSString).boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes, context: nil).size
return expectedLabelSize
}
func getWidth(font: UIFont) -> CGFloat {
let attrs = [NSAttributedStringKey.font : font]
return (self as NSString).boundingRect(with: CGSize.zero, options: .usesLineFragmentOrigin, attributes: attrs, context: nil).size.width
}
}
// 计算文字高度或者宽度与weight参数无关
extension String {
func ga_widthForComment(fontSize: CGFloat, height: CGFloat = 15) -> CGFloat {
let font = UIFont.systemFont(ofSize: fontSize)
let rect = NSString(string: self).boundingRect(with: CGSize(width: CGFloat(MAXFLOAT), height: height), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(rect.width)
}
func ga_heightForComment(fontSize: CGFloat, width: CGFloat) -> CGFloat {
let font = UIFont.systemFont(ofSize: fontSize)
let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(rect.height)
}
func ga_heightForComment(fontSize: CGFloat, width: CGFloat, maxHeight: CGFloat) -> CGFloat {
let font = UIFont.systemFont(ofSize: fontSize)
let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(rect.height)>maxHeight ? maxHeight : ceil(rect.height)
}
}
2.3.2 UIColor 转换
extension UIColor {
/// RGB颜色
///
/// - Parameters:
/// - red: R
/// - green: G
/// - blue: B
/// - alpha: A
convenience init(red:Int, green:Int, blue:Int, alpha:CGFloat = 1.0) {
self.init(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: alpha)
}
/// 16进制颜色
///
/// - Parameters:
/// - rgb: RGB Int值
/// - alpha: 透明度
convenience init(hex rgb:Int, alpha:CGFloat = 1.0) {
self.init(red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF, alpha: alpha)
}
/// 随机颜色
///
/// - Parameter randomAlpha: 是否随机透明度,默认false
/// - Returns: 随机颜色
public static func random(randomAlpha: Bool = false) -> UIColor {
let randomRed = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
let randomGreen = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
let randomBlue = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
let alpha = randomAlpha ? CGFloat(Float(arc4random()) / 0xFFFFFFFF) : 1.0
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: alpha)
}
/// Hex String -> UIColor
convenience init(hexString: String, alpha: CGFloat = 1.0) {
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
let scanner = Scanner(string: hexString)
if hexString.hasPrefix("#") {
scanner.scanLocation = 1
}
var color: UInt32 = 0
scanner.scanHexInt32(&color)
let mask = 0x000000FF
let r = Int(color >> 16) & mask
let g = Int(color >> 8) & mask
let b = Int(color) & mask
let red = CGFloat(r) / 255.0
let green = CGFloat(g) / 255.0
let blue = CGFloat(b) / 255.0
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
}