项目要适配阿拉伯地区,而阿拉伯的言语是从右往左显示的,恰好与咱们的习惯相反,适配起来很别扭。
RTL布局(Right To Left)
咱们这边的习惯是从左到右,设计图也是如此:
而阿拉伯地区的习惯是从右到左的:
- 除了字符和UI布局,还有侧返手势也要做相同的处理。
针对这两种布局方法,假如运用主动布局AutoLayout
的话就很轻松,只要把left
换成leading
,把right
换成trailing
就能够了。
但绝对布局frame
就不可,究竟有名字给你叫的:绝对不妥协,坐标点在哪就在哪。关于喜爱用绝对布局的开发者(例如我)就很不友好了。
为了frame
布局也能适配RTL布局,专门写了这几个Extension用来平常开发运用:
首要设置一个全局变量,用于判断当时是否RTL(从右到左)布局
let isRTL: Bool = {
guard let window = UIApplication.shared.delegate?.window ?? nil else { return false }
let layoutDirection = UIView.userInterfaceLayoutDirection(for: window.semanticContentAttribute)
return layoutDirection == .rightToLeft
}()
UIView+RTL
import UIKit
private var refWidthKey: UInt8 = 0
extension UIView {
/// 参照宽度,也便是父视图的宽度。
/// - 假如是`UIScrollView`最好将其设置为它的`contentSize.width`。
var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superview?.bounds.width ?? 0 }
}
var rtl_frame: CGRect {
set {
guard isRTL else {
frame = newValue
return
}
let x = rtl_refWidth - newValue.maxX
frame = CGRect(origin: CGPoint(x: x, y: newValue.origin.y), size: newValue.size)
}
get {
guard isRTL else { return frame }
let x = rtl_refWidth - frame.maxX
return CGRect(origin: CGPoint(x: x, y: frame.origin.y), size: frame.size)
}
}
var rtl_center: CGPoint {
set {
guard isRTL else {
center = newValue
return
}
let centerX = rtl_refWidth - newValue.x
center = CGPoint(x: centerX, y: newValue.y)
}
get {
guard isRTL else { return center }
let centerX = rtl_refWidth - center.x
return CGPoint(x: centerX, y: center.y)
}
}
var rtl_x: CGFloat {
set {
guard isRTL else {
frame.origin.x = newValue
return
}
let x = rtl_refWidth - frame.width - newValue
frame.origin.x = x
}
get {
guard isRTL else { return frame.origin.x }
let x = rtl_refWidth - frame.maxX
return x
}
}
var rtl_midX: CGFloat {
guard isRTL else { return frame.midX }
let midX = rtl_refWidth - frame.midX
return midX
}
var rtl_maxX: CGFloat {
guard isRTL else { return frame.maxX }
return rtl_refWidth - frame.origin.x
}
/// 相对本身的转化值
@objc func rtl_value(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
}
CALayer+RTL
import UIKit
private var refWidthKey: UInt8 = 0
extension CALayer {
/// 参照宽度,也便是父视图的宽度。
/// - 假如是`CAScrollLayer`最好将其设置为它的`内容宽度`。
var rtl_refWidth: CGFloat {
set { objc_setAssociatedObject(self, &refWidthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
get { objc_getAssociatedObject(self, &refWidthKey) as? CGFloat ?? superlayer?.bounds.width ?? 0 }
}
var rtl_frame: CGRect {
set {
guard isRTL else {
frame = newValue
return
}
let x = rtl_refWidth - newValue.maxX
frame = CGRect(origin: CGPoint(x: x, y: newValue.origin.y), size: newValue.size)
}
get {
guard isRTL else { return frame }
let x = rtl_refWidth - frame.maxX
return CGRect(origin: CGPoint(x: x, y: frame.origin.y), size: frame.size)
}
}
var rtl_position: CGPoint {
set {
guard isRTL else {
position = newValue
return
}
let positionX = rtl_refWidth - newValue.x
position = CGPoint(x: positionX, y: newValue.y)
}
get {
guard isRTL else { return position }
let positionX = rtl_refWidth - position.x
return CGPoint(x: positionX, y: position.y)
}
}
var rtl_x: CGFloat {
set {
guard isRTL else {
frame.origin.x = newValue
return
}
let x = rtl_refWidth - frame.width - newValue
frame.origin.x = x
}
get {
guard isRTL else { return frame.origin.x }
let x = rtl_refWidth - frame.maxX
return x
}
}
var rtl_midX: CGFloat {
guard isRTL else { return frame.midX }
let midX = rtl_refWidth - frame.midX
return midX
}
var rtl_maxX: CGFloat {
guard isRTL else { return frame.maxX }
return rtl_refWidth - frame.origin.x
}
/// 相对本身的转化值
@objc func rtl_value(_ v: CGFloat) -> CGFloat {
isRTL ? (bounds.width - v) : v
}
}
UIScrollView+RTL
import UIKit
extension UIScrollView {
var rtl_contentInset: UIEdgeInsets {
set {
guard isRTL else {
contentInset = newValue
return
}
contentInset = UIEdgeInsets(top: newValue.top,
left: newValue.right,
bottom: newValue.bottom,
right: newValue.left)
}
get {
guard isRTL else { return contentInset }
return UIEdgeInsets(top: contentInset.top,
left: contentInset.right,
bottom: contentInset.bottom,
right: contentInset.left)
}
}
var rtl_contentOffset: CGPoint {
set {
guard isRTL else {
contentOffset = newValue
return
}
let offetX = contentSize.width - bounds.width - newValue.x
contentOffset = CGPoint(x: offetX, y: newValue.y)
}
get {
guard isRTL else { return contentOffset }
let offetX = contentSize.width - bounds.width - contentOffset.x
return CGPoint(x: offetX, y: contentOffset.y)
}
}
func rtl_setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
var offset = contentOffset
if isRTL {
let offetX = contentSize.width - bounds.width - contentOffset.x
offset = CGPoint(x: offetX, y: contentOffset.y)
}
setContentOffset(offset, animated: animated)
}
/// 相对本身的转化值
override func rtl_value(_ v: CGFloat) -> CGFloat {
isRTL ? (contentSize.width - v) : v
}
}
运用
运用我这个分类的话,首要要给设置一个参照宽度(一般是父视图的宽度)
let testView = UIView()
// 1.一定要先设置参照宽度(一般是父视图的宽度)
testView.rtl_refWidth = UIScreen.main.bounds.width
// 2.再运用rtl_frame替代frame设置布局
testView.rtl_frame = CGRect(x: 20, y: 50, width: 100, height: 100)
addSubview(testView)
RTL布局主要是针对x轴的布局做镜像处理,所以要有个参照宽度(一般是父视图的宽度)才能做x轴的镜像换算。
注意:
- 假如没有设置
rtl_refWidth
默许会取父视图的宽度,所以主张先添加到父视图再设置rtl_frame
。 - 假如父视图是
UIScrollView
,不能设置rtl_refWidth
为bounds.width
,要设置contentSize.width
。 -
UICollectionView
会主动适配,不过contentOffset
和contentInset
依旧需要进行转化。 - 另外这个参照宽度最好是不会变动的,假如变动了记住
rtl_refWidth
也更新一下。
目前适配的这几个类和属性就够用了(今后发现新的再补上),这里是我用纯frame
布局适配建立好的UI:
全程都是依照从左到右的习惯建立的UI,没毛病。
最后说两句
当然能运用AutoLayout
能省去很多费事,不过关于动态性比较强的界面,或者一些暂时穿插的控件,frame
布局比AutoLayout
好用,还有动画、交互强的地方,用frame
能够很好地去操控,而且性能也比AutoLayout
好一点。
至于frame
运用费事,其实只要编写标准,用起来也是很便利的,所以我个人是挺喜爱frame
布局的。