最近項目適配阿拉伯谆棺,記錄一下最近的工作內容魄宏。在此之前,我是沒有了解過這方面的知識台盯。
首先說說為什么要適配阿拉伯呢罢绽,是因為我們中文和英文這些是從左往右顯示的語言,但是阿拉伯的語言是從右往左顯示(RTL)静盅,恰好與我們的習慣相反良价,剛開始的時候實在很別扭寝殴,
首先在適配的項目的開始,我查找了一下網上的資料
感謝這幾位大佬的博客:
https://blog.csdn.net/a657651096/article/details/102805114
http://www.reibang.com/p/042f3db234ad
http://www.reibang.com/p/3383ca5f6de0
我的項目是OC開發(fā)明垢,布局用的masonry蚣常。
先來捋一下阿拉伯適配需要做哪些事情呢。
1阿拉伯從右往左顯示痊银,我們所有的約束需要更換抵蚊。
2所有的UIView的處理
3帶方向的圖片處理
4手勢的處理
5文字顯示方向TextAlignment(大部分是UILabel)
6UIEdgeInsets(UIButton)
7富文本AttributeString
8Unicode文字的處理
9UICollectionView的處理(水平方向的)
10UIScrollView的處理(水平方向)
我們先來看一組效果圖:
這是在中文下的效果
[圖片上傳失敗...(image-399ff5-1659059722645)]
image.png
這是阿拉伯下的效果
[圖片上傳失敗...(image-ab55c1-1659059722645)]
image.png
列了一下需要處理的問題列表,接下來就是解決問題的具體方案了:
我寫了一個公共的宏定義判斷是不是阿拉伯語言溯革,這個地方可以根據不同的需求做判斷
#define isRTL() [[[[NSBundle mainBundle] preferredLocalizations] firstObject] hasPrefix:@"ar"]
1將所有的left更換成leading贞绳,right更換成trailing,這至少解決了50%的問題致稀。是不是非常簡單NONONO~~
全部UIView處理
iOS9之后冈闭,蘋果出了API適配RTL
UIView有一個semanticContentAttribute的屬性,當我們將其設置成UISemanticContentAttributeForceRightToLeft之后抖单,UIView將強制變?yōu)镽TL布局萎攒。當然在非RTL語言下,我們需要設置它為UISemanticContentAttributeForceLeftToRight臭猜,來適配系統(tǒng)是阿拉伯語躺酒,App是其他語言不需要RTL布局的情況。
項目中有無數(shù)個UIView蔑歌,是不是需要我們一個一個去設置呢羹应,當然不是,這時候大家想到的是不是hook一下UIView的方法次屠,來達到效果呢园匹,好像是不行的呢,原因可以看我前面提到的三位的博客劫灶,我在appdelegate里面統(tǒng)一設置的裸违,當我們設置UIView的semanticContentAttribute以后,發(fā)現(xiàn)UISearchBar還沒有改變本昏,那我們再設置一下UISearchBar
if (isRTL()) {
[UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
[UISearchBar appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}
處理帶方向的圖片供汛,這個部分有兩種方式可以處理,要么讓UI切兩套圖涌穆,分別展示怔昨,或者是把圖片翻轉一下,當然宿稀,圖片不能帶文字趁舀,這里得多說一句,經過這一次的教訓祝沸,我發(fā)誓以后再也不要用帶文字的圖片了矮烹,如果只是帶方向的圖片越庇,翻轉就行了,但是圖片帶文字那就玩不轉了奉狈,只能用幾套圖卤唉,還有國際化的時候,圖片帶文字嘹吨,也不好處理搬味,很不幸,我項目中很多帶文字的圖片蟀拷,我只能一個一個去修改碰纬,言歸正傳,先來看一下處理帶方向圖片處理:
給UIImage寫了一個分類问芬,添加了一個方法悦析,在方法里面判斷是不是阿拉伯語,如果是翻轉了圖片此衅,翻轉圖片的方法用的系統(tǒng)自帶的强戴。
#import "UIImage+HALFlipped.h"
@implementation UIImage (HALFlipped)
- (UIImage *)hal_imageFlippedForRightToLeftLayoutDirection
{
if (isRTL()) {
return [UIImage imageWithCGImage:self.CGImage
scale:self.scale
orientation:UIImageOrientationUpMirrored];
}
return self;
}
@end
這樣子在帶方向的地方使用這個方法,就可以了
UIButton * backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[backBtn setImage:[[UIImage imageNamed:@"kls_Room_Box_back"] hal_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
手勢的處理
滑動返回
RTL下挡鞍,除了布局需要調整骑歹,手勢的方向也是需要調整的
正常的滑動返回手勢是右滑,在RTL下墨微,是需要變成左滑返回的道媚。為了讓滑動返回也適配RTL,我們需要修改navigationBar和UINavigationController.view的semanticContentAttribute翘县。使用[UIView appearance]修改semanticContentAttribute并不能使手勢隨之改變最域,我們需要手動修改。為了讓所有的UINavigationController都生效锈麸。我們hook了UINavigationController的initWithNibName:bundle:
#import "UINavigationController+HALRTL.h"
@implementation UINavigationController (HALRTL)
+ (void)load
{
if (isRTL()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oldMethod = class_getInstanceMethod(self, @selector(initWithNibName:bundle:));
Method newMethod = class_getInstanceMethod(self, @selector(rtl_initWithNibName:bundle:));
method_exchangeImplementations(oldMethod, newMethod);
});
}
}
- (instancetype)rtl_initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
if ([self rtl_initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
if (@available(iOS 9.0, *)) {
self.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;;
self.view.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}
}
return self;
}
@end
其他手勢
跟方向有關的手勢有2個:UISwipeGestureRecognizer和UIPanGestureRecognizer
UIPanGestureRecognizer是無法直接設置有效方向的镀脂。為了設置只對某個方向有效,一般都是通過實現(xiàn)它的delegate中的gestureRecognizerShouldBegin:方法忘伞,來指定是否生效薄翅。對于這種情況,我們只能手動修gestureRecognizerShouldBegin:中的邏輯氓奈,來適配RTL
UISwipeGestureRecognizer有一個direction的屬性匿刮,可以設置有效方向。為了適配RTL探颈,我們可以hook它的setter方法,達到自動適配的目的:
#import "UISwipeGestureRecognizer+HALRTL.h"
@implementation UISwipeGestureRecognizer (HALRTL)
+ (void)load
{
Method oldAttMethod = class_getInstanceMethod(self,@selector(setDirection:));
Method newAttMethod = class_getInstanceMethod(self,@selector(rtl_setDirection:));
method_exchangeImplementations(oldAttMethod, newAttMethod); //交換成功
}
- (void)rtl_setDirection:(UISwipeGestureRecognizerDirection)direction
{
if (isRTL()) {
if (direction == UISwipeGestureRecognizerDirectionRight) {
direction = UISwipeGestureRecognizerDirectionLeft;
} else if (direction == UISwipeGestureRecognizerDirectionLeft) {
direction = UISwipeGestureRecognizerDirectionRight;
}
}
[self rtl_setDirection:direction];
}
@end
UIButton的RTL適配
UIButton的imageEdgeInsets和titleEdgeInsets训措。正常的時候伪节,我們設置一個titleEdgeInsets的left光羞。但是當RTL的情況下,因為所有的東西都左右鏡像了怀大,應該設置titleEdgeInsets的right布局才會正常纱兑。然而系統(tǒng)卻不會自動幫我們將left和right調換。我們需要手動去適配它化借。
@implementation UIButton (HALRTL)
UIEdgeInsets RTLEdgeInsetsWithInsets(UIEdgeInsets insets) {
if (insets.left != insets.right && isRTL()) {
CGFloat temp = insets.left;
insets.left = insets.right;
insets.right = temp;
}
return insets;
}
+ (void)load{
if (isRTL()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oldMethod = class_getInstanceMethod(self, @selector(setContentEdgeInsets:));
Method newMethod = class_getInstanceMethod(self, @selector(rtl_setContentEdgeInsets:));
method_exchangeImplementations(oldMethod, newMethod);
Method oldImageMethod = class_getInstanceMethod(self, @selector(setImageEdgeInsets:));
Method newImageMethod = class_getInstanceMethod(self, @selector(rtl_setImageEdgeInsets:));
method_exchangeImplementations(oldImageMethod,newImageMethod);
Method oldTitleMethod = class_getInstanceMethod(self, @selector(setTitleEdgeInsets:));
Method newTitleMethod = class_getInstanceMethod(self, @selector(rtl_setTitleEdgeInsets:));
method_exchangeImplementations(oldTitleMethod,newTitleMethod);
});
}
}
- (void)rtl_setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
[self rtl_setContentEdgeInsets:RTLEdgeInsetsWithInsets(contentEdgeInsets)];
}
- (void)rtl_setImageEdgeInsets:(UIEdgeInsets)imageEdgeInsets {
[self rtl_setImageEdgeInsets:RTLEdgeInsetsWithInsets(imageEdgeInsets)];
}
- (void)rtl_setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets {
[self rtl_setTitleEdgeInsets:RTLEdgeInsetsWithInsets(titleEdgeInsets)];
}
@end
TextAlignment
RTL下textAlignment也是需要調整的潜慎,官方文檔中默認textAlignment是NSTextAlignmentNatural,并且NSTextAlignmentNatural可用自動適配RTL
然而蓖康,情況并沒有文檔描述的那么好铐炫,當我們在系統(tǒng)內切換語言的時候,系統(tǒng)經常會錯誤的設置textAlignment蒜焊。沒有辦法倒信,我們只有自己去適配textAlignment.
以UILabel為例,我們hook它的setter的方法泳梆,根據當前是否是RTL鳖悠,來設置正確的textAlignment,如果UILabel從未調用setTextAlignment:优妙,我們還需要給它一個正確的默認值乘综。
#import "UILabel+HALRTL.h"
@implementation UILabel (HALRTL)
+ (void)load
{
Method oldInitMethod = class_getInstanceMethod(self,@selector(initWithFrame:));
Method newInitMethod = class_getInstanceMethod(self, @selector(rtl_initWithFrame:));
method_exchangeImplementations(oldInitMethod, newInitMethod); //交換成功
Method oldTextMethod = class_getInstanceMethod(self,@selector(setTextAlignment:));
Method newTextMethod = class_getInstanceMethod(self, @selector(rtl_setTextAlignment:));
method_exchangeImplementations(oldTextMethod, newTextMethod); //交換成功
}
- (instancetype)rtl_initWithFrame:(CGRect)frame
{
if ([self rtl_initWithFrame:frame]) {
self.textAlignment = NSTextAlignmentNatural;
}
return self;
}
- (void)rtl_setTextAlignment:(NSTextAlignment)textAlignment
{
if (isRTL()) {
if (textAlignment == NSTextAlignmentNatural || textAlignment == NSTextAlignmentLeft) {
textAlignment = NSTextAlignmentRight;
} else if (textAlignment == NSTextAlignmentRight) {
textAlignment = NSTextAlignmentLeft;
}
}
[self rtl_setTextAlignment:textAlignment];
}
富文本AttributeString和Unicode字符串
以UILabel為例,對于AttributeString套硼,UILabel的textAlignment是不生效的卡辰,因為AttributeString自帶attributes。為了讓attributeString也能自動適配RTL熟菲。我們需要在RTL下看政,將Alignment的left和right互換。
attributeString的alignment一般使用NSMutableParagraphStyle設置抄罕,所以我們首先hook NSMutableParagraphStyle允蚣,在setAlignment的時候設上正確的alignment:
由于閱讀習慣的差異(阿拉伯語從右往左閱讀,其他語言從左往右閱讀)呆贿,所以字符的排序是不一樣的嚷兔,普通語言左邊是第一個字符,阿拉伯語右邊是第一個字符做入。
如果是單純某種文字冒晰,不管是阿拉伯語還是英文,系統(tǒng)都是已經幫助我們做好適配了的竟块。然而混排的情況下壶运,系統(tǒng)的適配是有問題的。對于一個string浪秘,系統(tǒng)會用第一個字符來決定當前是LTR還是RTL蒋情。
那么坑來了埠况,假設有一個這樣的字符串@"小明??? ?? ???????"(翻譯過來為:小明關注了你),在阿拉伯語的情況下棵癣,由于閱讀順序是從右往左辕翰,我們希望他顯示為@"??? ?? ???????小明"。然而按照系統(tǒng)的適配方案狈谊,是永遠無法達到我們期望的喜命。
如果"小明"放前面,第一個字符是中文河劝,系統(tǒng)識別為LTR壁榕,從左往右排序,顯示為@"小明??? ?? ???????"丧裁。
如果"小明"放后面护桦,第一個字符是阿拉伯語,系統(tǒng)識別為RTL煎娇,從右往左排序二庵,依然顯示為@"小明??? ?? ???????"。
為了適配這種情況缓呛,可以在字符串前面加一些不會顯示的字符催享,強制將字符串變?yōu)長TR或者RTL。
在字符串前面添加"\u202B"表示RTL哟绊,加"\u202A"LTR因妙。為了統(tǒng)一適配剛剛的情況,我們hook了UILabel的setText:方法票髓。當然這種方法沒法適配所有的情況攀涵,項目中具體的場景還得具體處理。
#import "UILabel+HALAttrRTL.h"
BOOL isRTLString(NSString *string) {
if ([string hasPrefix:@"\u202B"] || [string hasPrefix:@"\u202A"]) {
return YES;
}
return NO;
}
NSString * RTLString(NSString *string) {
if (string.length == 0 || isRTLString(string)) {
return string;
}
if (isRTL()) {
string = [@"\u202B" stringByAppendingString:string];
} else {
string = [@"\u202A" stringByAppendingString:string];
}
return string;
}
NSAttributedString *RTLAttributeString(NSAttributedString *attributeString ){
if (attributeString.length == 0) {
return attributeString;
}
NSRange range;
NSDictionary *originAttributes = [attributeString attributesAtIndex:0 effectiveRange:&range];
NSParagraphStyle *style = [originAttributes objectForKey:NSParagraphStyleAttributeName];
if (style && isRTLString(attributeString.string)) {
return attributeString;
}
NSMutableDictionary *attributes = originAttributes ? [originAttributes mutableCopy] : [NSMutableDictionary new];
if (!style) {
NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
UILabel *test = [UILabel new];
test.textAlignment = NSTextAlignmentLeft;
mutableParagraphStyle.alignment = test.textAlignment;
style = mutableParagraphStyle;
[attributes setValue:mutableParagraphStyle forKey:NSParagraphStyleAttributeName];
}
NSString *string = RTLString(attributeString.string);
return [[NSAttributedString alloc] initWithString:string attributes:attributes];
}
@implementation UILabel (HALAttrRTL)
+(void)load{
Method oldAttMethod = class_getInstanceMethod(self,@selector(setAttributedText:));
Method newAttMethod = class_getInstanceMethod(self, @selector(rtl_setAttributedText:));
method_exchangeImplementations(oldAttMethod, newAttMethod); //交換成功
Method oldTextMethod = class_getInstanceMethod(self,@selector(setText:));
Method newTextMethod = class_getInstanceMethod(self,@selector(rtl_setText:));
method_exchangeImplementations(oldTextMethod, newTextMethod); //交換成功
}
- (void)rtl_setAttributedText:(NSAttributedString *)attributedText
{
if (isRTL()) {
attributedText = RTLAttributeString(attributedText);
}
[self rtl_setAttributedText:attributedText];
}
- (void)rtl_setText:(NSString *)text
{
[self rtl_setText:RTLString(text)];
}
@end
以上是常見的適配了洽沟,接下來說兩個特殊的
UICollectionView在RTL下的適配
繼承UICollectionViewFlowLayout 重寫兩個方法
-(UIUserInterfaceLayoutDirection)effectiveUserInterfaceLayoutDirection {
if (isRTL()) {
return UIUserInterfaceLayoutDirectionRightToLeft;
}
return UIUserInterfaceLayoutDirectionLeftToRight;
}
- (BOOL)flipsHorizontallyInOppositeLayoutDirection{
return YES;
}
最后UIScrollView在RTL適配
普通的UIScrollView可以通過把UIScrollView的transform和scrollView的subviews翻轉一下
if (isRTL()) {
self.scrollView.transform = CGAffineTransformMakeRotation(M_PI);
NSArray *subViews = self.scrollView.subviews;
for (UIView *subView in subViews) {
if ([subView isKindOfClass:[HALUserInfoRelationshipView class]]) {
subView.transform = CGAffineTransformMakeRotation(M_PI);
}
}
}
我項目中太多的地方用到了UIScrollView,因為我們的UI設計以故,有非常多的分頁控制器,所以我們項目中使用JXCategory搭配UIScrollView裆操。
在使用的過程中遇到一個小問題怒详,例如ScrollView加載三個不同的view,每個view的寬度都是屏幕的寬踪区,這在RTL下有個問題昆烁,就是有view不顯示,我從左往右適配的時候缎岗,右邊的不顯示静尼,從右往左適配,左邊的不顯示,后來我使用了一種比較愚蠢的方法鼠渺。最左的view從左適配蜗元,最右的View從右適配
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 85 * SCREEN_SCALEIPhone6, kScreenWidth, 233 * SCREEN_SCALEIPhone6)];
self.scrollView.pagingEnabled = YES;
self.scrollView.bounces = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.backgroundColor = UIColor.clearColor;
[self.bottomView addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.bottomView.mas_leading);
make.top.mas_equalTo(self.userListView.mas_bottom);
make.width.mas_equalTo(ScreenWidth);
make.height.mas_equalTo(233*SCREEN_SCALEIPhone6);
}];
self.scrollContentView = [UIView new];
[self.scrollView addSubview:self.scrollContentView];
[self.titleArray addObject:klstring(@"label_410")];
HALRoomTempGiftView *giftView = [[HALRoomTempGiftView alloc] initWithRoomId:self.room_id GiftRoomType:self.gift_roomType IsBackPack:YES TagId:@"0"];
[self.scrollContentView addSubview:giftView];
[giftView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
if (isRTL()) {
make.trailing.mas_equalTo(0);
}else{
make.leading.mas_equalTo(0);
}
make.height.mas_equalTo(245*SCREEN_SCALEIPhone6);
make.width.mas_equalTo(ScreenWidth);
}];
HALRoomTempGiftView *giftView = [[HALRoomTempGiftView alloc] initWithRoomId:self.room_id GiftRoomType:self.gift_roomType IsBackPack:NO TagId:tempModel.tag_id];
[self.scrollContentView addSubview:giftView];
[giftView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.scrollView.mas_top);
if (isRTL()) {
make.leading.mas_equalTo(ScreenWidth *i);
}else{
make.leading.mas_equalTo(ScreenWidth*(i+1));
}
make.height.mas_equalTo(245*SCREEN_SCALEIPhone6);
make.width.mas_equalTo(ScreenWidth);
}];
@weakify(self);
giftView.spec_listBlock = ^(NSArray * _Nonnull array) {
@strongify(self);
self.selectedGiftCountNumber = [array firstObject];
self.numberView.spec_list = array;
[self.selectedNumberBtn configWithNumber:self.selectedGiftCountNumber];
};
giftView.didSelectGiftMoldeBlock = ^(EBCounterItemModel * _Nonnull model) {
@strongify(self);
self.selectedGiftModel = model;
};
[self.giftViews addObject:giftView];
if (i == 0) {
[giftView configurationGiftListData];
}
}
self.giftTitleView.titles = self.titleArray;
[self.scrollContentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.trailing.top.mas_equalTo(0);
make.width.mas_equalTo(ScreenWidth * self.titleArray.count);
make.height.mas_equalTo(233 * SCREEN_SCALEIPhone6);
}];
self.scrollView.contentSize = CGSizeMake(kScreenWidth*self.titleArray.count, 0);
self.giftTitleView.contentScrollView = self.scrollView;