注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

JavaScript 逐点突破系列 -- 变幻莫测的this指向

JavaScript 逐点突破系列 – 变幻莫测的this指向this指向事件调用环境谁触发事件,函数里面的this指向就是谁let button = document.getElemetById('button')button.onclick = funct...
继续阅读 »

JavaScript 逐点突破系列 – 变幻莫测的this指向

this指向

事件调用环境

谁触发事件,函数里面的this指向就是谁

let button = document.getElemetById('button')
button.onclick = function () {
console.log(this) //button对象
}

全局环境
浏览器环境下

console.log(this) // window

node环境下

console.log(this) // module.exports

函数内部

this最终指向的是调用的对象,和声明没有直接关系

var object = {
name: 'object',
getName: function() {
console.log(this)
}
}
var bar = object.getName // 只是函数声明并未调用
object.getName() // object对象
window.object.getName() // object对象
/* 函数被多层对象所包含,如果函数被最外层对象调用,this指向
的也只是它上一级的对象。*/
bar() // window对象

构造函数

构造函数中的this指向的是实例对象

let fn = function(){
this.id = 'xiaoMing'
console.log(this.id)
}
let fn1 = new fn() //this指向fn1对象

new 的内部原理

【1】创建一个空对象 obj;
【2】把 child 的__proto__ 指向构造函数 parent 的原型对象 prototype,此时便建立了 obj 对象的原型链:child ->parent.prototype->Object.prototype->null
【3】在 child 对象的执行环境调用 parent 函数并传递参数。
【4】考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 child 作为新对象返回;否则会将 result 作为新对象返回。this绑定的是返回的对象。

function fn(){
this.num = 10;
}
fn.num = 20;
fn.prototype.num = 30;
fn.prototype.method = function() {
console.log(this.num);
}
var prototype = fn.prototype
var method = prototype.method
new fn().method() // 10
prototype.method() // 30
method() // undefined

箭头函数

箭头函数本身没有this和arguments,箭头函数继承上下文的this关键字,也就是说指向上一层作用域的this。
注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}

var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。


修改this指向

apply、call、bind

用法

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
func.bind(this, arg1, arg2)()

apply、call的区别

call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。

bind和其他两种方法的区别

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

原文链接:https://blog.csdn.net/weixin_45495667/article/details/108801000




收起阅读 »

iOS 一个标签自动布局的view

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。 事件告警统计关注-事件列表.jpg 主要实现图中所示的功能,话不多少搞起! 第一次写大神们多包涵呀 下面这个方法计算传进来的字符...
继续阅读 »

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。









事件告警统计关注-事件列表.jpg



主要实现图中所示的功能,话不多少搞起!



第一次写大神们多包涵呀

下面这个方法计算传进来的字符串数组,实现每个字符串长度的计算,并做换行判断,每一行存进统计数组中

//将标签数组根据type以及其他参数进行分组装入数组
- (void)disposeTags:(NSArray *)aryName aryCount:(NSArray *)aryCount{
NSMutableArray *tags = [NSMutableArray new];//纵向数组
NSMutableArray *subTags = [NSMutableArray new];//横向数组

float originX = _tagOriginX;
for (NSString *tagTitle in aryName) {
NSUInteger index = [aryName indexOfObject:tagTitle];

//计算每个tag的宽度
CGSize contentSize = [tagTitle fdd_sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(self.frame.size.width-_tagOriginX*2, MAXFLOAT)];

NSMutableDictionary *dict = [NSMutableDictionary new];
dict[@"tagTitle"] = tagTitle;//标签标题
dict[@"tagCount"] =aryCount[index];
dict[@"viewWith"] = [NSString stringWithFormat:@"%f",contentSize.width+_tagSpace+30];//标签的宽度

if (index == 0) {
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
} else {
if (originX + contentSize.width > self.frame.size.width-_tagOriginX*2) {
//当前标签的X坐标+当前标签的长度>屏幕的横向总长度则换行
[tags addObject:subTags];
//换行标签的起点坐标初始化
originX = _tagOriginX;
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
subTags = [NSMutableArray new];
[subTags addObject:dict];
} else {
//如果没有超过屏幕则继续加在前一个数组里
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
}
}

if (index +1 == aryName.count) {
//最后一个标签加完将横向数组加到纵向数组中
[tags addObject:subTags];
disposeAry = tags;
}

//标签的X坐标每次都是前一个标签的宽度+标签左右空隙+标签距下个标签的距离
originX += contentSize.width+_tagHorizontalSpace+_tagSpace+30;
}
}



下面这个方法是计算字符串长度的封装方法,只限字符串计算,用的话可以直接搬走

#pragma mark - 扩展方法


@implementation NSString (FDDExtention)

- (CGSize)fdd_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
CGSize resultSize;
if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSDictionary *attributes = @{ NSFontAttributeName:font };
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
NSStringDrawingContext *context;
[invocation setArgument:&size atIndex:2];
[invocation setArgument:&options atIndex:3];
[invocation setArgument:&attributes atIndex:4];
[invocation setArgument:&context atIndex:5];
[invocation invoke];
CGRect rect;
[invocation getReturnValue:&rect];
resultSize = rect.size;
} else {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(sizeWithFont:constrainedToSize:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(sizeWithFont:constrainedToSize:)];
[invocation setArgument:&font atIndex:2];
[invocation setArgument:&size atIndex:3];
[invocation invoke];
[invocation getReturnValue:&resultSize];
}
return resultSize;
}

字符串长度计算完成了,下面就可以进行所有字符串排列的高度了。其实这个蛮简单的 ,统计数组有多少个元素就代表标签排布有多少行。


//获取处理后的tagsView的高度根据标签的数组




  • (float)getDisposeTagsViewHeight:(NSArray *)ary {


    float height = 0;

    if (disposeAry.count > 0) {

    height = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);

    }

    return height;

    }




下面这个将标签加载到view上了,并实现赋值。

-(void)setTagAryName:(NSArray *)tagAryName aryCount:(NSArray *)aryCount delegate:(id)delegate{
_tagDelegate=delegate;
[self disposeTags:tagAryName aryCount:aryCount];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, 0, 200, 30)];
label.text=@"事件类别统计:";
label.textColor=_titleColor;
label.textAlignment=NSTextAlignmentLeft;
label.adjustsFontSizeToFitWidth=YES;
[self addSubview:label];
//遍历标签数组,将标签显示在界面上,并给每个标签打上tag加以区分
for (NSArray *iTags in disposeAry) {
NSUInteger i = [disposeAry indexOfObject:iTags];

for (NSDictionary *tagDic in iTags) {
NSUInteger j = [iTags indexOfObject:tagDic];

NSString *tagTitle = tagDic[@"tagTitle"];
float originX = [tagDic[@"originX"] floatValue];
float viewWith = [tagDic[@"viewWith"] floatValue];
NSString *count = tagDic[@"tagCount"];
UIView *NCView=[[UIView alloc]initWithFrame:CGRectMake(originX, _tagOriginY+i*(_tagHeight+_tagVerticalSpace), viewWith, _tagHeight)];
[self addSubview:NCView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame=CGRectMake(0, 0, viewWith-30, _tagHeight);
button.layer.borderColor = _borderColor.CGColor;
button.layer.borderWidth = _borderWidth;
button.layer.masksToBounds = _masksToBounds;
button.layer.cornerRadius = _cornerRadius;
button.titleLabel.font = [UIFont systemFontOfSize:_titleSize];
[button setTitle:tagTitle forState:UIControlStateNormal];
[button setTitleColor:_titleColor forState:UIControlStateNormal];
[button setBackgroundImage:_normalBackgroundImage forState:UIControlStateNormal];
[button setBackgroundImage:_highlightedBackgroundImage forState:UIControlStateHighlighted];
button.tag = i*iTags.count+j;
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(viewWith-30, 0, 30, _tagHeight)];
label.text=[NSString stringWithFormat:@"X%@",count];
label.textAlignment=NSTextAlignmentCenter;
label.adjustsFontSizeToFitWidth=YES;
label.textColor=_titleColor;
[NCView addSubview:button];
[NCView addSubview:label];
}
}
self.countLabel=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, [self getDisposeTagsViewHeight:disposeAry], 100, 30)];
self.countLabel.text=[NSString stringWithFormat:@"总计:%ld次",_times];
self.countLabel.textAlignment=NSTextAlignmentLeft;
self.countLabel.adjustsFontSizeToFitWidth=YES;
self.countLabel.textColor=_titleColor;
[self addSubview:self.countLabel];
if (disposeAry.count > 0) {
float contentSizeHeight = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
self.contentSize = CGSizeMake(self.frame.size.width,contentSizeHeight);
}

if (self.frame.size.height <= 0) {
self.frame = CGRectMake(CGRectGetMinX([self frame]), CGRectGetMinY([self frame]), CGRectGetWidth([self frame]), [self getDisposeTagsViewHeight:disposeAry]+30);
}


}


说了这么多,如何调用呢? 那么亮点来了 只需要传进来数组就实现图中的功能。

//计算出全部展示的高度,让maxHeight等于计算出的高度即可,初始化不需要设置高度
NSArray *tagAryCount=@[@"10",@"8",@"7",@"9",@"2",@"4",@"5",@"3",@"4"];
NSArray* tagAryName = @[@"黑色玫瑰",@"比尔沃吉特",@"钢铁烈阳",@"德玛西亚",@"祖安",@"巨神峰",@"雷瑟守备祖安",@"诺克萨斯暗影岛",@"弗雷尔卓德"];
ZHLbView * tagsView = [[ZHLbView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 0)];
[tagsView setTagAryName:tagAryName aryCount:tagAryCount delegate:self];
[self.view addSubview:tagsView];


补充,在demo编写的过程中 ,遇到了一些小问题,就是scrollView如果是VC的第一个子视图的话 其总是要有一个64高度的空白区。
转自:https://www.jianshu.com/p/860af31a03aa 收起阅读 »

多条件筛选菜单-DropDownMenu

示例图:简介一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等我的博客 自己造轮子--android常用多条件帅选菜单实现思路(类似美团,爱奇艺电影票下拉菜单)特色支持多级菜单你可以完全自定义你的菜单样式,我这里只是封装...
继续阅读 »

示例图:



简介

一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等

我的博客 自己造轮子--android常用多条件帅选菜单实现思路(类似美团,爱奇艺电影票下拉菜单)

特色

  • 支持多级菜单
  • 你可以完全自定义你的菜单样式,我这里只是封装了一些实用的方法,Tab的切换效果,菜单显示隐藏效果等
  • 并非用popupWindow实现,无卡顿

Gradle Dependency

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

dependencies {
compile 'com.github.dongjunkun:DropDownMenu:1.0.4'
}

使用

添加DropDownMenu 到你的布局文件,如下

<com.yyydjk.library.DropDownMenu
android:id="@+id/dropDownMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:ddmenuTextSize="13sp" //tab字体大小
app:ddtextUnselectedColor="@color/drop_down_unselected" //tab未选中颜色
app:ddtextSelectedColor="@color/drop_down_selected" //tab选中颜色
app:dddividerColor="@color/gray" //分割线颜色
app:ddunderlineColor="@color/gray" //下划线颜色
app:ddmenuSelectedIcon="@mipmap/drop_down_selected_icon" //tab选中状态图标
app:ddmenuUnselectedIcon="@mipmap/drop_down_unselected_icon"//tab未选中状态图标
app:ddmaskColor="@color/mask_color" //遮罩颜色,一般是半透明
app:ddmenuBackgroundColor="@color/white" //tab 背景颜色
app:ddmenuMenuHeightPercent="0.5" 菜单的最大高度,根据屏幕高度的百分比设置
...
/>

我们只需要在java代码中调用下面的代码

 //tabs 所有标题,popupViews  所有菜单,contentView 内容
mDropDownMenu.setDropDownMenu(tabs, popupViews, contentView);

代码下载:DropDownMenu-master.zip

原文链接:https://github.com/dongjunkun/DropDownMenu

收起阅读 »

iOS 实例对象,类对象,元类对象的关联---isa/superclass指针(二)

总的来说,isa,superclass的的关系可以用一副经典的图来表示class的isa指向meta-classclass的superclass指向父类的class作者:枫紫_6174原贴链接:https://www.jianshu.com/p/26f37fb...
继续阅读 »

  • 一.isa指针

  • 上篇文章我们提到了实例对象,类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联


  • 实例对象的isa指针


    • 当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用



  • 类对象的isa指针


    • 类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用

      二.类对象的supercl

      • 先两个类,一个Person继承自NSObject,一个类继承自Person
      /// Person继承自NSObject
      @interface Person : NSObject
      -(void)perMethod;
      +(void)perEat;
      @end
      @implementation Person
      -(void)perMethod{
      }
      +(void)perEat{
      }

      @end

      /// student继承自Person
      @interface Student : Person
      -(void)StudentMethod;
      +(void)StudentEat;
      @end
      @implementation Student

      -(void)StudentMethod{
      }
      +(void)StudentEat{
      }



      • 当实例对象调用自身的对象方法时,它在自身的class对象中找到StudentMethod方法
      •         Student *student = [[Student alloc]init];
        [student StudentMethod]



        • 当实例对象调用父类的方法的时候


                Student *student = [[Student alloc]init];
        [student perMethod];当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)

        三.元类对象的superclass 指针
        当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用

      • [Student perEat];

      • 总的来说,isa,superclass的的关系可以用一副经典的图来表示


      • instance的isa指向class

        class的isa指向meta-class

        meta-class的isa指向基类的meta-class

        class的superclass指向父类的class



      • 作者:枫紫_6174
        原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

js实现函数防抖节流

一、什么是函数防抖跟节流?函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有...
继续阅读 »

一、什么是函数防抖跟节流?

函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。
函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有自己的一个执行频率。

二、JavaScript实现

1.函数防抖

代码如下(示例):

function debounce(callback, wait) {
let timer = null;
return function(){
let _this = this,
arg = arguments;
if(!!timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(function(){
callback.call(_this,arg)
},wait)
}
}
let _debounce = debounce(function() {
console.log('dshdihdi')
}, 1000)


2.函数节流

代码如下(示例):

// 函数节流,时间戳版本
function throttle(callback, wait) {
let time = new Date().getTime();
return function() {
let _this = this,
arg = arguments;
let nowTime = new Date().getTime();
if (nowTime - time >= wait){
callback.call(_this,...arg)
time = nowTime
}
}
}
let _throttle = throttle(function(e) {
console.log(e)
console.log(arguments)
}, 1000)


函数节流: 懒加载分页请求资源、音乐播放进度条更新等等

函数防抖: 频繁操作点赞、登录注册、需要提交最新信息等等

原文链接:https://blog.csdn.net/qq_45924621/article/details/115586112

收起阅读 »

iOS 实例对象,类对象,元类对象(一)

OC对象的分类 OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象) 实例对象: 实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一...
继续阅读 »

OC对象的分类


OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)




  • 实例对象:


    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:


        NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);



输出结果:


instance实例对象存储的信息:
1.isa指针

2.其他成员变量
  • 我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址


  • 类对象



  • 类对象的获取


        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140

从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个




  • 类对象的作用




类对象存储的信息:

1.isa指针

2.superclass指针

3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)


  • 元类对象




1.元类对象的获取


        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个


特别注意一点:


Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

无论class几次,它返回的始终是类对象

2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

元类存储结构:

元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:

1.isa指针

2.superclass指针

3.类方法(即加号方法)


从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空



  • 判断是否为元类

    class_isMetaClass(objcL);

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下


将原来的load方法换成initialize




先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize


相信大家在想什么叫第一次接收消息了,我们回到main()


运行程序,输出结果:

说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码


从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印
int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}

运行结果:


2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印


load是直接函数指针直接调用,类,分类,继承等等


[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用.


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}

输出结果:

2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次


我们再来看看继承关系中,initialize的调用


int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}

输出结果:


020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}


输出结果:


2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end

输出结果:


2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/f0150edc0f42
收起阅读 »

JavaScript new 操作符

new 操作符做的事情 - 01创建了一个全新的对象。将对象链接到这个函数的 prototype 对象上。执行构造函数,并将 this 绑定到新创建的对象上。判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。new...
继续阅读 »
new 操作符做的事情 - 01
  1. 创建了一个全新的对象。
  2. 将对象链接到这个函数的 prototype 对象上。
  3. 执行构造函数,并将 this 绑定到新创建的对象上。
  4. 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。

new 操作符做的事情 - 02

  1. 创建一个全新的对象 (无原型的Object.create(null)) 。
  2. 目的是保存 new 出来的实例的所有属性。
  3. 将构造函数的原型赋值给新创建的对象的原型。
  4. 目的是将构造函数原型上的属性继承下来。
  5. 调用构造函数,并将 this 指向新建的对象。
  6. 目的是让构造函数内的属性全部转交到该对象上,使得 this 指向改变,方法有三 : apply、call、bind 。
  7. 判断构造函数调用的方式,如果是 new 的调用方式,则返回经过加工后的新对象,如果是普通调用方式,则直接返回构造函数调用时的返回值。
function myNew(Constructor, ...args) {
// 判断 Constructor 参数是否是函数
if (typeof Constructor !== 'function') {
return 'Constructor.apply is not a function';
};

// 1、创建了一个全新的对象。
let newObject = {};

// 2、将对象链接到这个函数的 prototype 对象上。
newObject.__proto__ = Constructor.prototype;

// 此处是把 1 / 2 步结合到一起
// const newObject = Object.create(Constructor.prototype);

// 3、执行构造函数,
// 并将 this 绑定到新创建的对象上。
let result = Constructor.apply(newObject, args);

// 4. 判断构造函数执行返回的结果是否是引用数据类型,
// 若是则返回构造函数执行的结果,
// 否则返回创建的对象。
if ((result !== null && typeof result === 'object') || (typeof result === 'function')) {
return result;
} else {
return newObject;
};
};

// 需要被 new 的函数
function NewTest(args) {
this.dataValue = args;
return this;
};

// 定义参数
let dataObj = {
sname: '杨万里',
number: 3,
web: 'vue'
};
let dataArray = [5, 'uniApp', '范仲淹'];

// 执行 myNew 函数
let test = myNew(NewTest, 1, 'JavaScript', '辛弃疾', dataObj, dataArray);
console.log(test); // NewTest {dataValue: Array(5)}


本文链接:https://blog.csdn.net/weixin_51157081/article/details/115577751


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?  1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类 2.新建一个TCStudent类继承自TCPerson,并且给TCStuden...
继续阅读 »

首先先看几个面试问题



  • 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?


 1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类




2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


类别里面有负载方法么?


  • 答:分类里面肯定有负载


#import "TCPerson.h"

@implementation TCPerson
(void)load{

}
@end

#import "TCPerson TCtest1.h"

@implementation TCPerson (TCtest1)
(void)load{

}
@end

#import "TCPerson TCTest2.h"

@implementation TCPerson (TCTest2)
(void)load{

}
@end

load方法什么时候调用?




  • load方法在运行时加载类和分类的时候调用load

  • #import 

    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
    }


    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    @end


    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    @end
    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    @end

    可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:


    从输出结果我们可以抛光,三个负载方法都被调用



    如果不是,到底分类的方法是怎么调用的?




    • 首先我们在TCPerson申明一个方法+(void)test和在它的两个分类都重写+(void)test

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject
    + (void)test;
    @end

    NS_ASSUME_NONNULL_END


    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    + (void)test{
    NSLog(@"TCPerson +test");
    }
    @end

    分类重写测试

    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest1) +test1");
    }
    @end

    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest2) +test2");
    }
    @end

    在主要里面我们调用测试

    #import 
    #import "TCPerson.h"
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    }
    return 0;
    }

    输出结果:

    从输出结果中我们可以看到,只有分类2中的测试被调用,为什么只调用分类2中的测试了?
    因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(进行文件就行了)
    其输出结果为:

    细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test coverage了,所以才出现这种情况么?


    我们打印TCPerson的类方法

    void printMethodNamesOfClass(Class cls)
    {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);

    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];

    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
    // 获得方法
    Method method = methodList[I];
    // 获得方法名
    NSString *methodName = NSStringFromSelector(method_getName(method));
    // 拼接方法名
    [methodNames appendString:methodName];
    [methodNames appendString:@", "];
    }

    // 释放
    free(methodList);

    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    printMethodNamesOfClass(object_getClass([TCPerson class]));
    }
    return 0;
    }



    输出结果:

    可以看到,TCPerson的所有类方法名,并非覆盖,三个负载,三个测试,方法都在

    载入原始码分析:查看objc长期原始码我们可以看到:

  • void call_load_methods(void)
    {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
    call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0 || more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

    load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用

    我们查看call_class_loads()方法

    static void call_class_loads(void)
    {
    int I;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;

    if (PrintLoading) {
    _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) free(classes);
    }

    其通过的是load_method_t函数指针直接调用

    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);

    其分类load方法调用也是一样

    static bool call_category_loads(void)
    {
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Category cat = cats[i].cat;
    load_method_t load_method = (load_method_t)cats[i].method;
    Class cls;
    if (!cat) continue;

    cls = _category_getClass(cat);
    if (cls && cls->isLoadable()) {
    if (PrintLoading) {
    _objc_inform("LOAD: +[%s(%s) load]\n",
    cls->nameForLogging(),
    _category_getName(cat));
    }
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
    }
    }

    为什么test不一样了

    因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的(关于isa,可以查看我的实例对象,类对象,元类对象的关联---isa/superclass指针(2))里面有详细的关于test的方法调用原理


    load只在加载类的时候调用一次,且先调用类的load,再调用分类的


    load的继承关系调用

    首先我们先看TCStudent


    #import "TCStudent.h"

    @implementation TCStudent

    @end

    不写load方法调用

    TCStudent写上load

  • 从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的


    总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



    作者:枫紫_6174
    原贴链接:https://www.jianshu.com/p/f66921e24ffe
收起阅读 »

iOS UITableView左滑删除功能

一、概述 UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车,视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。 二、效果图 效果图.g...
继续阅读 »
一、概述

UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。


二、效果图







效果图.gif


三、技术分析


  1. 让tableView进入编辑状态,即tableView.editing = YES

// 取消
[self.tableView setEditing:YES animated:NO];



  1. 返回编辑模式,即实现UITableViewDelegate中的- tableview:editingStyleForRowAtIndexPath:方法,在里面返回删除模式。如果不实现,默认返回的就是删除模式。

-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 删除
return UITableViewCellEditingStyleDelete;
}



  1. 提交删除操作,即实现UITableViewDelegate中的- tableview:commitEditingStyle:editing StyleForRowAtIndexPath:方法。只要实现此方法,即默认实现了系统横扫出现删除按钮的删除方法。

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//只要实现这个方法,就实现了默认滑动删除!!!!!
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// 删除数据
[self _deleteSelectIndexPath:indexPath];
}
}



  1. 如果想把删除按钮改为中文,可以实现-tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:方法。

-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}


四、细节处理


  1. 以下属性必须设置为NO,默认为NO,否则会导致删除模式无效,反而成为多选模式

tableView.allowsMultipleSelection = NO;
tableView.allowsSelectionDuringEditing = NO;
tableView.allowsMultipleSelectionDuringEditing = NO;



  1. 侧滑状态下点击编辑按钮的bug。
// 编辑按钮被点击
- (
void)_rightBarButtonItemDidClicked:(UIButton *)sender
{
sender.selected = !sender.isSelected;
if (sender.isSelected) {

// 这个是fix掉:当你左滑删除的时候,再点击右上角编辑按钮, cell上的删除按钮不会消失掉的bug。且必须放在 设置tableView.editing = YES;的前面。
[
self.tableView reloadData];

// 取消
[
self.tableView setEditing:YES animated:NO];
}
else{
// 编辑
[
self.tableView setEditing:NO animated:NO];
}
}

转自:https://www.jianshu.com/p/4c53901062eb 收起阅读 »

OpenGL基础点

一、OpenGL中的坐标系1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形3. OpenGL的投影方式有:透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果正投影:只能用...
继续阅读 »

一、OpenGL中的坐标系

1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)

2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形

3. OpenGL的投影方式有:
透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果
正投影:只能用来设置平面图形

4.坐标系的分类:

① 惯性坐标系:没有什么参考价值,只要用来平移到世界坐标系

② 世界坐标系:大环境中的位置,系统的绝对坐标

③ 物体空间坐标系(局部空间):局部空间中的位置

④ 摄像机坐标系:观察空间(观察者)

5.坐标系之间的变换

① 物体坐标系,模型变换

② 转换到世界坐标系,视变换

③ 转换到观察者坐标系/摄像机坐标系,投影变换

④ 转换到裁剪坐标系,透视除法

⑤ 转换到规范化设备坐标,视口变换

⑥ 转换到屏幕坐标

注:① - ④ 是可以由开发者自定义完成的,⑤ - ⑥是有系统OpenGL来完成的

二、着色器的渲染流程

1.顶点数据

2.顶点着色器:接收顶点数据,单独处理每个顶点

3.细分着色器:
  ① 可选:描述物体形状,在管线中生成新的饿几何平面模型,生成最终形态
  ② 组分控制着色器/细分计算着色器:对所有的图形进行修改几何图元类型或者放弃所有图像

4.几何着色器

5.图元设置

6.剪切:剪切窗口之外的绘制

7.光栅化:输入图元的数学描述,转化为与屏幕对应位置像素片元,简称光栅化

8.片元着色器:片元着色器以及深度值,然后传递到片元测试和混合模块

9.效果

三、补充几个知识点

1.图片的渲染流程:
  ① GPU解码图片
  ② GPU纹理混合、顶点计算、像素点填充计算、渲染到帧缓冲区
  ③ 时钟信号:垂直同步、水平同步
  ④ iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制

2.MVP矩阵:
  ① model:物体的一些变化
  ② view:观察
  ③ projection:3D矩阵

3.强制解压缩:对图片进行重新绘制,得到一张新的压缩后的位图,其中用到的最核心的函数是:GCBitmapContextCreate

收起阅读 »

微信小程序-使用canvas绘制图片,下载,分享

接下来下选择图片// 点击选择图片按钮触发start: function() { let that = this let ctx = wx.createCanvasContext('myCanvas') // 设置canvas背景色, 否则制...
继续阅读 »

需求, 使用canvas绘制图片和文字, 生成图片

<!--pages/canvas/canvas.wxml-->
<view>
<view class="container">
<button bind:tap="start" class="start" size="mini">选择图片</button>
<button bind:tap="downloadCanvas" class="downloadCanvas" size="mini">下载</button>
<button bind:tap="share" class="share" size="mini">分享</button>
<!-- 分享图片功能 -->
</view>
<text>canvas区域</text>
<canvas style='width:{{canvasWidth}}px;height:{{canvasHeight}}px;border: 1px solid grey;' canvas-id='myCanvas'></canvas>
<image v-if="previewImage" :src="{{canvasImage}}"></image>
</view>


图片功能

先设置canvas区域的宽度和高度

js代码

data: {
title: 'canvas绘制图片',
canvasWidth: '', // canvas宽度
canvasHeight: '', // canvas高度
imagePath: '', // 分享的图片路径
leftMargin: 0,
topMargin: 0,
imgInfo: {},
ctx: [],
canvasImage: '',
previewImage: false,
imgProportion: 0.8, // 图片占canvas画布宽度百分比
imgToTop: 100 // 图片到canvas顶部的距离
},
onLoad: function(options) {
var that = this
var sysInfo = wx.getSystemInfo({
success: function(res) {
that.setData({
canvasWidth: res.windowWidth,
// 我这里选择canvas高度是系统高度的80%
canvasHeight: res.windowHeight * 0.8
})
// 根据图片比例, 使图片居中
let leftMargin = (res.windowWidth * (1 - that.data.imgProportion)) / 2
that.setData({
leftMargin
})
}
})
}


接下来下选择图片

// 点击选择图片按钮触发
start: function() {
let that = this
let ctx = wx.createCanvasContext('myCanvas')
// 设置canvas背景色, 否则制作的图片是透明的
ctx.setFillStyle('#f8f8f8')
ctx.fillRect(0, 0, that.data.canvasWidth, that.data.canvasHeight)
this.addImage(ctx)
},
// 添加图片
addImage: function(ctx) {
var that = this;
let imgInfo = that.data.imgInfo
var path
wx.chooseImage({
count: '1',
success(res) {
wx.getImageInfo({
src: res.tempFilePaths[0],
success: function(response) {
// 返回的response里有图片的临时路径和图片信息(高度/宽度)
that.setData({
imgInfo: response,
path: response.path
})
that.drawImage(ctx)
}
})
}
})
this.addTitle(ctx)
},


绘制文字和图

// 绘制图片
drawImage(ctx) {
let that = this
let imgInfo = that.data.imgInfo
let path = that.data.path
// 计算图片宽度 宽度固定 高度等比缩放
let imgWidth = that.data.canvasWidth * that.data.imgProportion
let imgHeight = imgInfo.height / imgInfo.width * imgWidth
// drawImage参数, 下面会说明
ctx.drawImage(path, 0, 0, imgInfo.width, imgInfo.height, that.data.leftMargin, that.data.imgToTop, imgWidth, imgHeight)
ctx.draw()
that.data.previewImage = true
},
//绘制文字
addTitle: function(ctx) {
var str = this.data.title
ctx.font = 'normal bold 16px sans-serif';
ctx.setTextAlign('center'); // 文字居中
ctx.setFillStyle("#222222");
ctx.fillText(str, this.data.canvasWidth / 2, 45) // 文字位置
},


drawImage方法


drawImage 方法允许在 canvas 中插入其他图像( img 和 canvas 元素) 。drawImage函数有三种函数原型:


drawImage(image, dx, dy) 在画布指定位置绘制原图

drawImage(image, dx, dy, dw, dh) 在画布指定位置上按原图大小绘制指定大小的图

drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 剪切图像,并在画布上定位被剪切的部分 从 1.9.0 起支持


参数描述

image

所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)

s

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 x 坐标

sy

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 y 坐标

sWidth

需要绘制到画布中的,image的矩形(裁剪)选择框的宽度

sHeight

需要绘制到画布中的,image的矩形(裁剪)选择框的高度

dx

image的左上角在目标 canvas 上 x 轴的位置

dy

image的左上角在目标 canvas 上 y 轴的位置

dWidth

在目标画布上绘制imageResource的宽度,允许对绘制的image进行缩放

dHeight

在目标画布上绘制imageResource的高度,允许对绘制的image进行缩放


下载图片

//点击下载按钮保存canvas图片
downloadCanvas: function() {
let that = this;
// 判断用户是否选择了图片
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
console.log(res, '保存')
}
})
}
});
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}


分享图片

// 分享图片
share() {
let that = this
if (that.data.previewImage) {

wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.showToast({
icon: 'none',
title: '长按图片分享',
duration: 1500
})
setTimeout(
() => {
wx.previewImage({
urls: [res.tempFilePath]
})
}, 1000)
}
})
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}



原文链接:https://www.jianshu.com/p/1e54146b8a26

收起阅读 »

H5之外部浏览器唤起微信分享

转自https://blog.csdn.net/qq_18976087/article/details/79095735最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说AP...
继续阅读 »

转自https://blog.csdn.net/qq_18976087/article/details/79095735

最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。

总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。

下面上代码,把这些直接放到要转发的页面里就可以了:

html部分:

<script src="mshare.js"></script>//引进mshare.js
<button data-mshare="0">点击弹出原生分享面板</button>
<button data-mshare="1">点击触发朋友圈分享</button>
<button data-mshare="2">点击触发发送给微信朋友</button>


js部分:

<script>
var mshare = new mShare({
title: 'Lorem ipsum dolor sit.',
url: 'http://m.ly.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat inventore minima voluptates.',
img: 'http://placehold.it/150x150'
});
$('button').click(function () {
// 1 ==> 朋友圈 2 ==> 朋友 0 ==> 直接弹出原生
mshare.init(+$(this).data('mshare'));
});
</script>


/**
* 此插件主要作用是在UC和QQ两个主流浏览器
* 上面触发微信分享到朋友圈或发送给朋友的功能
*/
'use strict';
var UA = navigator.appVersion;

/**
* 是否是 UC 浏览器
*/
var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0;

/**
* 判断 qq 浏览器
* 然而qq浏览器分高低版本
* 2 代表高版本
* 1 代表低版本
*/
var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0;

/**
* 是否是微信
*/
var wx = /micromessenger/i.test(UA);

/**
* 浏览器版本
*/
var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;
var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0;

/**
* 获取操作系统信息 iPhone(1) Android(2)
*/
var os = (function () {
var ua = navigator.userAgent;

if (/iphone|ipod/i.test(ua)) {
return 1;
} else if (/android/i.test(ua)) {
return 2;
} else {
return 0;
}
}());

/**
* qq浏览器下面 是否加载好了相应的api文件
*/
var qqBridgeLoaded = false;

// 进一步细化版本和平台判断
if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {
qq = 0;
} else {
if (qq && qqVs < 5.4 && os == 2) {
qq = 1;
} else {
if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {
uc = 0;
}
}
}
/**
* qq浏览器下面 根据不同版本 加载对应的bridge
* @method loadqqApi
* @param {Function} cb 回调函数
*/
function loadqqApi(cb) {
// qq == 0
if (!qq) {
return cb && cb();
}
var script = document.createElement('script');
script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';
/**
* 需要等加载过 qq 的 bridge 脚本之后
* 再去初始化分享组件
*/
script.onload = function () {
cb && cb();
};
document.body.appendChild(script);
}
/**
* UC浏览器分享
* @method ucShare
*/
function ucShare(config) {
// ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']
// 关于platform
// ios: kWeixin || kWeixinFriend;
// android: WechatFriends || WechatTimeline
// uc 分享会直接使用截图
var platform = '';
var shareInfo = null;
// 指定了分享类型
if (config.type) {
if (os == 2) {
platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';
} else if (os == 1) {
platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';
}
}
shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];
// android
if (window.ucweb) {
ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);
return;
}
if (window.ucbrowser) {
ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);
return;
}
}
/**
* qq 浏览器分享函数
* @method qqShare
*/
function qqShare(config) {
var type = config.type;
//微信好友 1, 微信朋友圈 8
type = type ? ((type == 1) ? 8 : 1) : '';
var share = function () {
var shareInfo = {
'url': config.url,
'title': config.title,
'description': config.desc,
'img_url': config.img,
'img_title': config.title,
'to_app': type,
'cus_txt': ''
};
if (window.browser) {
browser.app && browser.app.share(shareInfo);
} else if (window.qb) {
qb.share && qb.share(shareInfo);
}
};
if (qqBridgeLoaded) {
share();
} else {
loadqqApi(share);
}
}
/**
* 对外暴露的接口函数
* @method mShare
* @param {Object} config 配置对象
*/
function mShare(config) {
this.config = config;
this.init = function (type) {
if (typeof type != 'undefined') this.config.type = type;
try {
if (uc) {
ucShare(this.config);
} else if (qq && !wx) {
qqShare(this.config);
}
} catch (e) {}
}
}
// 预加载 qq bridge
loadqqApi(function () {
qqBridgeLoaded = true;
});
if (typeof module === 'object' && module.exports) {
module.exports = mShare;
} else {
window.mShare = mShare;
}
收起阅读 »

iOS打印导入字体名称

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:for (NSString *fontf...
继续阅读 »

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。

在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:

for (NSString *fontfamilyname in [UIFont familyNames]) { // font:'Avenir-Heavy
NSLog(@"family:'%@'",fontfamilyname);
for(NSString *fontName in [UIFont fontNamesForFamilyName:fontfamilyname])
{
NSLog(@"\tfont:'%@'",fontName);
}
NSLog(@"-------------");
}

对字体的使用

self.proNameLab.font = [UIFont fontWithName:@"Avenir-Heavy" size:13*ScaleSize];
收起阅读 »

iOS本地数据持久化

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案: plist文件(序列化) preference(偏好设置) NSKeyedArchiver(归档) SQLite3 FMDB CoreData 沙盒 每个APP的沙盒下面都有相似目录结构,...
继续阅读 »

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:



plist文件(序列化)

preference(偏好设置)

NSKeyedArchiver(归档)

SQLite3

FMDB

CoreData



沙盒


每个APP的沙盒下面都有相似目录结构,如图







下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!



NSString *path = NSHomeDirectory();




1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。


2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。


3、Library 目录:这个目录下有两个子目录:

Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.

Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。

可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。


4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。

// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();

//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];


plist文件(序列化)


可以被序列化的类型只有如下几种:

NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期


数据存储与读取的实例:

/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}

/**
从plist读取数据

@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}



存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。



Preference(偏好设置)


Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。


数据存储与读取的实例:

- (void)writeToPreference{

//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];

NSString *path = NSHomeDirectory();
}

- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}


使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。



Preference(偏好设置)plist文件(序列化)都是保存在 plist 文件中,但是plist文件(序列化)操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置) 可以直接通过 key-value单个读取。



归档解归档


要使用归档,其归档对象必须实现NSCoding协议



NSCoding协议声明的两个方法都必须实现。

encodeWithCoder:用来说明如何将对象编码到归档中。

initWithCoder:用来说明如何进行解档来获取一个新对象。



数据存储与读取的实例:

/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}

/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}


SQLite3


以下代码块将会介绍sqlite3.h中主要的API。

/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);

/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);

/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);

/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);


处理SQL返回结果的一些API

#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);


由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例

/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}


在使用数据库存储时主要存在以下步骤。



1、创建数据库

2、创建数据表

3、数据的“增删改查”操作

4、关闭数据库



在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。



创建表:

create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];

插入记录:

insert into 表名(字段1,……,字段n) values (值1,……,值n);

删除记录:

delete from 表名 where 条件表达式;

修改记录:

update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;

查看记录:

select 字段1,……,字段n from 表名 where 条件表达式;



FMDB


FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。


FMDB主要是使用以下三个类


FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。

FMResultSet :执行查询一个FMDatabase结果集。

FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。



一般的FMDB数据库操作4个:


创建数据库

打开数据库、关闭数据库

执行更新的SQL语句

执行查询的SQL语句



创建数据库

/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}


打开关闭数据库

/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;


执行更新的SQL语句

在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。

/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];

NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];

/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];

/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];


执行查询的SQL语句

查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。

/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;


执行查询语句实例如下

- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}


多线程安全FMDatabaseQueue

由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。

FMDatabaseQueue的使用比较简单

//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];


事务

事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。


比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。

以下是事务的例子

//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}

//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}

//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}



CoreData







CoreData核心结构图.png


以下是CoreData常用类的作用描述



PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。

ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。

PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。

ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。



CoreData主要工作原理如下



读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。

向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。

更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。



CoreData的使用步骤如下



1.添加框架。

2.数据模板和对象模型。

3.创建对象管理上下文。

4.数据的增删改查操作。



在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。

收起阅读 »

iOS KVO的原理&&KVO的isa指向

一。简单复习一下KVO的使用 定义一个类,继承自NSObject,并添加一个名称的属性 #import NS_ASSUME_NONNULL_BEGIN @interface TCPerson : NSObject @property (nonato...
继续阅读 »

一。简单复习一下KVO的使用



  • 定义一个类,继承自NSObject,并添加一个名称的属性


#import 

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END



  • 在ViewController我们简单的使用一下KVO


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

当点击屏幕的时候,控制台输出:


2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二。深入剖析KVO的突破



  • 在-(void)touchesBegan:(NSSet*)触摸事件:(UIEvent *)事件{

    self.person1.name = @“苍老市”;

    }我们知道
    self.person1.name的本质是[self.person1 setName:@“ cang lao shi”];


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}

在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:


- (void)setName:(NSString *)name{
_name = name;
}


  • 在ViewController我们新建一个person2,代码变成了:


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


  • 注意:当我们点击屏幕的时候输出的结果是:


2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}




  • 既然我们改变名称的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?




三.KVO的isa指向



  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:

    打开lldb


(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)


  • 从上面的打印我们看到self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson

  • NSKVONotifying_TCPerson是运行时动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面的代码不能在xcode中运行):


#import "NSKVONotifying_TCPerson.h"

@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context];
}
@end
未添加观察self.person2实例对象的isa指向流程图:

添加观察self.person1实例对象的isa指向流程图:


所以KVO其本质是动态生成一个NSKVONotifying_TCPerson类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/0b6083b91b04
收起阅读 »

iOS Runloop的深入理解

iOS
首先了解一下程序、进程和线程 程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可...
继续阅读 »

首先了解一下程序、进程和线程


程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。


一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。所以程序运行的时候,需要一个机制,让线程能随时处理事件但不退出。


以前写游戏的时候就写过这样的东西,通常是一个 do while 循环,让程序一直运转,直到接收到退出信息。


而 RunLoop 就是让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。


线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时默认是没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。


在 CoreFoundation 里面关于 RunLoop 有5个类:


CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef


他们的层级关系为,一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 在运作的时候,一次只能运作与一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。


而一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件(为什么要 0 和 1 来命名,每次都记不住,GG)。


Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调。


Observer(观察者)都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:


typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};


也就是可以在这几个时机去安排 RunLoop 执行一些其他的任务。


上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。


RunLoop 的 CFRunLoopMode 和 CFRunLoop 的结构大致如下:


struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};


这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。


如上文说的 RunLoop 一次循环只能运行在一个 Mode 下,是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。但如果一个 Source/Timer/Observer 想在多个 Mode 下运作,则可以分别加入到多个 Mode,或者给两个 Mode 添加 "Common" 标记,再将 Source/Timer/Observer 加入到 RunLoop 的 "commonModeItems" 。


应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,如果想让它回调则可以将这个 Timer 分别加入这两个 Mode。或将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 。


CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:


CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);


Mode 暴露的管理 mode item 的接口有下面几个:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


可以注意到 mode 并不像 Source/Timer/Observer 一样有 Remove 方法,所以 mode 只能增加,不能减少。


当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。


接下来看 RunLoop 的执行逻辑


按 1 - 10 来理理,首先要知道 Observer 是观察者,也就是下面这几种状态都会通知观察者,开发者也添加一个观察者,去在以下几种状态的时候,执行一些任务,比如将没啥实时性要求的东西,在即将进入休眠状态时执行。
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop

其中第 2 步虽然通知 Observer 即将处理 Timer,但其实并没有真的即将处理 Timer 回调,这个通知每次 Loop 循环都会调用,但 Timer 只有在注册的时间到了才会在第 9 步去执行,第 4 步处理运行的 mode 里待处理的 Source0,其中第 5 步会判断 mode item 里是否有 Source1 处于 ready 状态(也就是 Source1 的端口已经收到消息),有的话跳到第 9 步,处理 Source1 事件,然后进入下一个循环,没有的话说明 mode item 里的事件都处理完毕,线程进入休眠状态,等待 Source1,Timer 或者外部手动将 RunLoop 唤醒(上文说 Source0 并不能唤醒 RunLoop,所以一般会通过手动唤醒 RunLoop,来让 RunLoop 处理新加入进去的 Source0)。


可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,去执行加入到 RunLoop 里的 Source0,Timer 的回调,以及 Observer 回调,以及用于线程或进程间通讯的 Source1,当所有的都处理完之后,结束一次循环,进入休眠状态,休眠的时候等待 Timer 注册的时间点或者 Source1 唤醒 RunLoop(也可以手动唤醒)。


从上面我们可以了解,线程和进程之间的通讯是基于 mach port 传递消息实现的,这也是 RunLoop 的核心。有必要了解一下 mach port, OSX/iOS 的系统架构分为 4 层,从外到内为应用层,应用框架层,核心框架层,Darwin。应用层包括用户能接触到的图形应用,应用框架层即开发人员接触到的 Cocoa 等框架,核心框架层包括各种核心框架、OpenGL 等内容,Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容。

Darwin 核心的架构:


其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。


XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。


BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。

IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。


在 Mach 中,进程、线程和虚拟内存都被称为"对象"。Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。


Mach 中的对象通过一个 Mach 端口发送一个消息,消息中会携带目标端口,这个消息会从用户空间传递到内核空间,再由内核空间传递到目标端口,实现线程或进程之间的通讯。(也就是线程或进程之间的通讯不能绕过系统内核)。目标端口接收到消息,因为 RunLoop 会对 mach_port 端口源进行监听,如果 RunLoop 此时处于休眠状态,则被唤醒,便可以处理已经接收到消息的 source1 事件。

RunLoop 实现了很多功能


启动后,系统默认注册了5个Mode:


1:kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。


2:UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。


3:UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。


4:GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。


5:kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。


AutoreleasePool


App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()(因为需要设置不同的优先级,所以注册两个)。


第一个 Observer 监视的事件是 Entry(即将进入Loop),用来创建自动释放池,且设置的优先级最高,保证创建释放池发生在其他所有回调之前。


第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时去释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。


在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。


事件响应


苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。


当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的事件传递。


_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。


手势识别


当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。


苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。


当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


界面更新


当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。


苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。也就是 UI 是在界面 RunLoop 休眠之前更新的,所以如果想在 UI 更新之后做一些事情,可以注册一个 Observer 监听 kCFRunLoopAfterWaiting(刚从休眠中唤醒)。

定时器


NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。


如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。


CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。


PerformSelecter


当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。


当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。


GCD


当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。


网络请求


iOS 中,关于网络请求的接口自下至上有如下几层:


CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire


通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了多个需要手动触发的 Source0。


当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 基于 mach port 的 Source1 接收来自底层 CFSocket 的消息,当收到消息后,其会在合适的时机将 Source0 标记为待处理,同时唤醒 Delegate 线程的 RunLoop 来让其处理 Source0。完成一个由 CFSocket 线程到网络请求所在线程的数据处理。

AFNetworking


AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 创建了一个线程,并在这个线程中启动了一个 RunLoop,RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行网络请求任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了该线程的 RunLoop 中。


AsyncDisplayKit


AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:


UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。


排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。


其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。


ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务(需要将 Node 的属性同步到主线程的 UIView/CALayer 去的任务),然后一一执行。


总结:


ibireme深入理解RunLoop 前前后后我看了好几遍,每次都看的不深入,总是瞬时了解,睡一觉就记不清楚细节了,这次自己逐行去理解,并写下来,加上点自己的理解,也写了点代码去观察 RunLoop,算是对 RunLoop 的理解更加深入了。


作者:Cooci
原帖链接:https://www.jianshu.com/p/2103d15f4423

收起阅读 »

Runtime的底层原理及应用

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。也就是 Runtime 涉及三个点,面向对象消息分发消息转发。面向对象:Objective-C 的对象是...
继续阅读 »

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。



也就是 Runtime 涉及三个点,

  • 面向对象
  • 消息分发
  • 消息转发

面向对象:

Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。

Class *class = [[Class alloc] init];

alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。


union isa_t {  
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};


从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型


init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。


也就是在 Runtime 的协助之下,一个对象完成了创建。


你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?


在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。


类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。


struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;



class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。


消息分发


在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。


通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码


struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;



_mask:分配用来缓存 bucket 的总数。

_occupied:表明目前实际占用的缓存 bucket 的个数。

_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。


struct bucket_t {
private:
cache_key_t _key;
IMP _imp;



objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。


当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:


struct method_t {  
SEL name;
const char *types;
IMP imp;
};


编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。


如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。


选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。


消息转发


消息转发分为以下几步:


1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。


2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。


3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。


以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。


上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。


runtime 可以做什么:


实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。


交换两个方法的实现


    Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);


关联对象


通过下面两个方法,可以给 category 实现添加成员变量的效果。


objc_setAssociatedObject
objc_getAssociatedObject


动态添加类和方法:


objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。


将 json 转换为 model


用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。



作者:Cooci
原帖链接:https://www.jianshu.com/p/2dae81846046
收起阅读 »

Android日记之View的绘制流程(二)

Android日记之View的绘制流程(一)Measure过程Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure...
继续阅读 »


Android日记之View的绘制流程(一)

Measure过程

Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure(),下面分两种情况进行讨论。

View的measure过程

View的measure过程就是会调用View的measure()方法,它是一个final类型的方法,在measure()方法还会调用View的onMeasure()方法,我们看这个方法写了什么。


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

代码很少,这里的setMeasuredDimension()方法会设置View的宽和高的测量值,因此我们只要看getDefaultSize()即可。


public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

从源码可以看出,逻辑还是很简单的,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况,简单理解,其实getDefaultSize()返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终得大小是在layout阶段确定的,所以这里必须要区分,但几乎所有情况下View的测量大小和最终大小是相等的。

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()的第一个参数size,即宽和高为getSuggestedMinimunWidth()getSuggestedMinimunHeight()的返回值:


protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这里就只分析getSuggestedMinimumWidth()方法的实现,getSuggestedMinimunHeight()和它的实现原理是一样的,从代码中可以看出,如果View没有设置背景,那么宽度就为mMinWidth,而mMinWidth对应android:Width这个属性所指定的值。如果不指定,那么mMinWidth则默认为0,如果设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()。那mBackground.getMinimumWidth()是什么呢?



public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth()返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。这里说明一下,ShapeDrawable无原始宽和高,BitmapDrawab有原始宽和高。

从getDefaultSize可以看出,View的宽和高由specSize决定。直接继承View的自定义控件需要重写onMeasure()方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的mesaure过程以外,还会遍历去调用所有子元素的measure()方法,各个子元素在递归的去执行这个过程,和View不同的是,ViewGroup时候是一个抽象类,因此它没有重写View的onMeasure(),但是它提供了一个叫measureChildren()的方法。


protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}


protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
{
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChildren()会调用measureChild()方法,在measureChild()方法里,就是去除子元素的LayoutParams,然后在通过getChildMeasureSpec()来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()方法进行测量。

我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要由各个子类去具体实现,比如LinearLayout、RelativeLayout等,ViewGroup不做统一的onMeasure()实现,是因为不同的ViewGroup子类有不同的布局特性,导致它们的测量细节个不不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因为无法做统一,以后会专门写一篇文章来讲解每个layout的布局是怎么实现的。

Layout过程

Layout的作用就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout()方法,在layout()方法中onLayout()方法又会被调用。layout过程相比measure过程就简单多了,layout()方法确定View本身的位置,而onLayout()方法则会确定所有子元素的位置,先看View的layout()方法。


public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

final boolean wasLayoutValid = isLayoutValid();

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}

if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}

首先会通过setFrame()方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着就会调用onLayout()方法,这个方法的用途是父容器确定子元素的位置,和onMeasure()类似,onLayout()的具体实现同样和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout()方法。

Draw过程

Draw过程就简单很多了,作用就是将View会知道屏幕上面。View的绘制过程遵循着如下几步。
(1)background.draw(canvas) - 绘制背景
(2)onDraw() - 绘制自己
(3)dispatchDraw()- 绘制children
(4)onDrawScrollBars() - 绘制装饰

绘制流程从ViewRootImpl的performDraw()开始,performDraw()方法在类 ViewRootImpl内,其核心代码如下:

private void performDraw() {



boolean canUseAsync = draw(fullRedrawNeeded);
}

private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets)
{
...
mView.draw(canvas);
...
}

然后查看View的draw()方法的源代码:

public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas); //这里进行传递的实现!!!

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);

if (debugDraw()) {
debugDrawFocus(canvas);
}

// we're done...
return;
}

......
}


View的绘制过程的传递是通过dispatchDraw()来实现的,dispatchDraw()会遍历调用所有子元素的draw()方法,如此一来draw事件就一层层的传递了下去。这里补充一下View还有一个特殊的方法叫setWillNotDraw(),代码如下:

/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}


如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个标记位。这个标记位对开发的实际意义是:当我们的自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位进行后续的优化。当然如果确定ViewGroup要绘制内容的话,就要关闭这个标记位。



作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android日记之View的绘制流程(一)

前言 View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK...
继续阅读 »

前言


View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK为为Android 9.0版本。



Activity的构成


在了解绘制流程之前,我们首先要了解Activity的构成,我们都知道Activity要用setcontentView()来加载布局,但是这个方法具体是怎么实现的呢,如下所示:


public void setContentView(@LayoutRes int layoutResID) {


getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

public Window getWindow() {
return mWindow;
}

这里有一个getWindow(),返回的是一个mWindow,那这个是什么呢,我们接在在Activity原来里的attach()方法里可以看到如下代码:


final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

......
}

我们发现了原来mWindow实例化了PhoneWindow,那意思就是说Activity通过setcontentView()加载布局的方法其实就是调用了PhoneWindow的setcontentView()方法,那我们接着往下看PhoneWindow类里面具体是怎么样的。



@Overridepublic void setContentView(int layoutResID) {

// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor(); //在这里
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

点进PhoneWindow这个类我们可以知道这里的PhoneWindow是继承Window的,Window是一个抽象类。然后我们看里面的一些关键地方,比如installDecor()方法,我们看看里面具体做了啥。


private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1); //注释1
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor); //注释2

......
}
}

里面有一个标记注释为1的generateDecor()方法,我们看看具体做了啥。

protected DecorView generateDecor(int featureId) {


// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

发现创建了一个DecorView,而这个DecorView继承了Framelayout,DecorView就是Activity的根View,接着我们回到标记注释2的generateLayout()方法,看看做了啥。

    

protected ViewGroup generateLayout(DecorView decor) {


......

// Inflate the window decor.
//根据不同情况加载不同的布局给layoutResource

int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title; //在这里!!!!!!!
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}

......

mDecor.finishChanging();

return contentParent;
}

generateLayout()方法代码比较长,这里截取了一部分,主要内容就是根据不同的情况加载不同的布局给layoutresource,在这里面有一个基础的XML,就是R.layout.screen_title。这个XMl文件里面有一个ViewStub和两个FrameLayout,ViewStub主要是显示Actionbar的,两个FarmeLayout分别显示TitleView和ContentView,也就是我们的标题和内容。看到这里我们也就知道,一个Activity包含一个Window对象,而Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又分为两个区域,分别就是TitleView和ContentView,平常所显示的布局就是在ContentView中。


View的整体绘制流程路口


上一节讲了Activity的构成,最后讲到了DecorView的创建和加载它的资源,但是这个时候DecorView的内容还无法显示,因为还没用加载到Window中,接下来我们看它是怎么被加载到Window中。


还是老样子,DecorView创建完毕的时并且要加载到Window中,我们还是要先了解Activity的创建流程,当我们使用Activit的startActivity()方法时,最终就是调用ActivityThread的handleLaunchActivity方法来创建Activity。而绘制会从根视图ViewRootImpl的performTraversals()方法开始从上到下遍历整个视图树,每个 View 控制负责绘制自己,而ViewGroup还需要负责通知自己的子 View进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。


ViewRootImpl是WindowManager和DecorView的纽带,View的三大流程均是通过这里来完成的,在ActivityThread中,当Activity对象被创建,会将DecorView加入到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。


刚刚也说过了,View的绘制流程是从ViewRootImpl类的performTraversals()方法开始的,它会依次调用performMeasure()performLayout()performDraw()三个方法。其中在performTraversals()调用measure()方法,在measure()方法又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了依次measure过程,接着子元素会重复父容器的measure过程,如此返回完成了整个View数的遍历。



理解MeasureSpec


为了理解View的测量过程,我们还需理解MeasureSpec,它在很大程度上决定了一个View的尺寸规格,而且也参与到了View的measure过程中。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽和高。


MeasureSpec是View的内部类,它代表哦啦一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小,如以下代码所示:


public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

......

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

public static final int EXACTLY = 1 << MODE_SHIFT;

public static final int AT_MOST = 2 << MODE_SHIFT;

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode
int mode)
{
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}


@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}


public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}


......
}

MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而也可以通过解包得到SpecMode和SpecSize。这里说一下打包成的MeasureSpec是一个int值,不是这个类本身。


对于SpecMode,有三种类型:



  • UPSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。


  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。


  • AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。


Android日记之View的绘制流程(二)

作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

OpenGL中的图片渲染流程

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据传递数据的三...
继续阅读 »

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?

我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据


传递数据的三种方式

1.传递数据处理的流程:顶点着色器--->光栅化/图元装配--->片元着色器--->渲染完成

2.TextureData(纹理)、Uniforms:可以直接的传递到顶点着色器或者片元着色器中。
    Attributes(属性):只能传递到顶点着色器中,进过处理后的数据可以传递到片元着色器中

3.着色器中的是我们可以控制的,但是光栅化/图元装配是由系统来完成的,不可控

三种传输方式详解

1.Attributes:只能传递到顶点着色器中,通过处理可以传递到片元着色器中
   使用场景:当数据不停的进行变换时
   经常传递的数据:颜色数据、顶点数据、纹理坐标、关照法线

2.Uniform:可以直接传递数据到顶点/片元着色器中
   使用场景:比较统一的处理方式,不会发生太多变化时
   顶点着色器处理的场景:图形的旋转操作,每个顶点乘以旋转矩阵来完成(旋转矩阵基本是不怎么改变的)
   片元着色器处理场景:处理视频,视频解码之后是由一帧帧的图片来组成的,对视频的颜色空间进行渲染处理(在视频中常使用的颜色空间为YUV)将YUV颜色空间乘以矩阵转化为RGB颜色来处理视频

3.TextureData(纹理):颜色的填充,视频的处理。一般这里的数据不会传递到顶点着色器的,这里主要是用来处理图片的,一般不涉及到顶点数据的处理

收起阅读 »

Android日记之线程池

前言 在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池...
继续阅读 »

前言


在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池来对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给RUnnable或者Callable,而Executor框架用来处理任务。Executor框架中最核心的成员就是ThreadPoolExecutor,它是线程池的核心实现类。本篇文章就着重讲解ThreadPoolExecutor。


ThreadPoolExecutor介绍


可以通过ThreadPoolExecutor开创建一个线程池,ThreadPoolExecutor类一共有四个构造方法。下面展示的都是拥有最多参数的的构造方法。



public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}


  • corePoolSize:核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则会创建新线程来处理任务;如果等于或者多于corePoolSize,则不会创建,如果调用线程池的prestartAllcoreThread()方法,线程池会提前创建并启动所有核心线程来等待任务。


  • maximumPoolSize:线程池允许创建的最大线程数,如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。


  • keepAliveTime:非核心线程闲置的超时时间,超过这个事件则回收,如果任务很多,并且每个任务的执行事件很短,则可以调用keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上。


  • TimeUnit:keepAliveTime参数的时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLOSECONDS)等。


  • BlockingQueue任务队列,如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockiingQueue类型,也就是阻塞队列。


  • ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置参数。


  • RejectedExecutionHandler :饱和策略,这是当任务队列中和线程池都满了时所采取的对应策略,默认是ABordPolicy,表示无法处理新任务,并抛出RejetctedExecutionException异常。此外还有3种策略,它们分别如下:



(1)CallerRunsPolicy:用调用者所在的线程来处理任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

(2)DiscardPolicy:不能执行的任务,并将该任务删除。

(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。


ThreadPoolExecutor的基本使用

package com.ju.executordemo;


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity{


private Button btnStart;
private final int CORE_POOL_SIZE = 4;//核心线程数
private final int MAX_POOL_SIZE = 5;//最大线程数
private final long KEEP_ALIVE_TIME = 10;//空闲线程超时时间
private ThreadPoolExecutor executorPool;
private int songIndex = 0;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
//创建线程池
initExec();
}

private void initView() {
btnStart = findViewById(R.id.btn_start);
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
begin();
}
});
}


public void begin() {
songIndex++;
try {
executorPool.execute(new WorkerThread("歌曲" + songIndex));
} catch (Exception e) {
Log.e("threadtest", "AbortPolicy...已超出规定的线程数量,不能再增加了....");
}

// 所有任务已经执行完毕,我们在监听一下相关数据
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20 * 1000);
} catch (Exception e) {

}
sout("monitor after");
}
}).start();

}

private void sout(String msg) {
Log.i("threadtest", "monitor " + msg
+ " CorePoolSize:" + executorPool.getCorePoolSize()
+ " PoolSize:" + executorPool.getPoolSize()
+ " MaximumPoolSize:" + executorPool.getMaximumPoolSize()
+ " ActiveCount:" + executorPool.getActiveCount()
+ " TaskCount:" + executorPool.getTaskCount()
);
}



private void initExec() {
executorPool = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingDeque(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}

class WorkerThread implements Runnable {

private String threadName;

public WorkerThread (String name){
threadName = name;
}


@Override
public void run() {
boolean flag = true;
try {
while (flag){
String tn = Thread.currentThread().getName();
//模拟耗时操作
Random random = new Random();
long time = (random.nextInt(5) + 1) * 1000;
Thread.sleep(time);
Log.e("threadtest","线程\"" + tn + "\"耗时了(" + time / 1000 + "秒)下载了第<" + threadName + ">");
//下载完毕跳出循环
flag = false;
}
}catch (Exception e){
e.printStackTrace();
}
}
}

}


上述代码模拟一个下载音乐的例子来演示ThreadPoolExecutor的基本使用,启动ThreadPoolExecutor的函数是execute()方法,然后他需要一个Runnable的参数来进行启动。



ThreadPoolExecutor的其它种类


通过直接或者间接地配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中有 4 种线程池比较常用,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor和 ScheduledThreadPool。下面分别介绍这4种线程池。



  • FixedThreadPool


FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建FixedThreadPool的方法, 如下所示:


public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}

FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,也就意味着FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime设置为0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime是无效的参数。另外,任 务队列采用了无界的阻塞队列LinkedBlockingQueue。



当执行execute()方法时,如果当前运行的线程未达到corePoolSize(核心线程数)时 就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到LinkedBlockingQueue中。 FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行




  • CachedThreadPool


CachedThreadPool是一个根据需要创建线程的线程池,创建CachedThreadPool的代码如下所示:


public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}

CachedThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,这意味着 CachedThreadPool没有核心线程,非核心线程是无界的。keepAliveTime设置为60L,则空闲线程等待新任务 的最长时间为 60s。在此用了阻塞队列 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作 必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。



当执行execute()方法时,首先会执行SynchronousQueue的offer()方法来提交任务,并且查询线程池中是否有空闲的线程执行SynchronousQueue的poll()方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行 SynchronousQueue的poll()方法,等待SynchronousQueue中新提交的任务。如果超过 60s 没有新任务提交到 SynchronousQueue,则这个空闲线程将终止。因为maximumPoolSize 是无界的,所以如果提交的任务大于线 程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务。




  • SingleThreadExecutor


SingleThreadExecutor是使用单个工作线程的线程池,其创建源码如下所示:


public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}

corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都 和FixedThreadPool一样,这里就不赘述了。SingleThreadExecutor的execute()方法的执行示意图如图5所示。


当执行execute()方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列LinkedBlockingQueue中。因此,SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。




  • ScheduledThreadPool


ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的创建源码如下所示



static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);

}

这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:

public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}

从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是ThreadPoolExecutor的 构造方法。corePoolSize是传进来的固定数值,maximumPoolSize的值是Integer.MAX_VALUE。因为采用的 DelayedWorkQueue是无界的,所以maximumPoolSize这个参数是无效的。


当执行ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWithFixedDelay()方法时,会向DelayedWorkQueue添加一个 实现RunnableScheduledFuture接口的ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去DelayedWorkQueue中取ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中time变量改为下次要执行的时间并放回到DelayedWorkQueue中。


作者:居居居居居居x
链接:https://www.jianshu.com/p/70a8d302fa0f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

安卓选择器类库-AndroidPicker

UI
示例图:AndroidPicker安卓选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)、双项选择器、颜色选择器、文件及目录选择器等…...
继续阅读 »

示例图:



AndroidPicker

安卓选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)、双项选择器、颜色选择器、文件及目录选择器等……

Install

“app”是测试用例;“library”包括WheelPicker、ColorPicker、FilePicker、MultiplePicker, WheelPicker包括DatePicker、TimePicker、OptionPicker、LinkagePicker、AddressPicker、NumberPicker、DoublePicker等。 其中WheelPicker、FilePicker及ColorPicker是独立的,需要用哪个就只依赖哪个,

 具体步骤如下: 第一步,在项目根目录下的build.gradle里加:

repositories {
maven {
url "https://jitpack.io"
}
}

第二步,在项目的app模块下的build.gradle里加: 滚轮选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:版本号'
}

文件目录选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:FilePicker:版本号'
}

颜色选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:ColorPicker:版本号'
}

注:Support版本截止1.5.6,从2.0.0开始为AndroidX版本。

Support版本依赖:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:1.5.6.20181018'
}

AndroidX版本依赖:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:Common:2.0.0'
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:2.0.0'
}

ProGuard

由于地址选择器使用了fastjson来解析,混淆时候需要加入以下类似的规则,不混淆Province、City等实体类。

-keepattributes InnerClasses,Signature
-keepattributes *Annotation*

-keep class cn.qqtheme.framework.entity.** { *;}

Sample (更多用法详见示例项目)

各种设置方法:

picker.setXXX(...);

如:
设置选项偏移量,可用来要设置显示的条目数,范围为1-5,1显示3行、2显示5行、3显示7行……

picker.setOffset(...);

设置启用循环

picker.setCycleDisable(false);

设置每项的高度,范围为2-4

picker.setLineSpaceMultiplier(...);
picker.setItemHeight(...);

设置文字颜色、字号、字体等

picker.setTextColor(...);
picker.setTextSize(...);
picker.setTextPadding(...);
picker.setTextSizeAutoFit(...);
picker.setTypeface(...);

设置单位标签

picker.setLabel(...);
picker.setOnlyShowCenterLabel(...))

设置默认选中项

picker.setSelectedItem(...);
picker.setSelectedIndex(...);

设置滚轮项填充宽度,分割线最长

picker.setUseWeight(true);
picker.setDividerRatio(WheelView.DividerConfig.FILL);

设置触摸弹窗外面是否自动关闭

picker.setCanceledOnTouchOutside(...);

设置分隔线配置项,设置null将隐藏分割线及阴影

picker.setDividerConfig(...);
picker.setDividerColor(...);
picker.setDividerRatio(...);
picker.setDividerVisible(...);

设置内容边距

picker.setContentPadding(...);

设置选中项背景色

picker.setShadowColor(...)

自定义顶部及底部视图

picker.setHeaderView(...);
picker.setFooterView(...);

获得内容视图(不要调用picker.show()方法),可以将其加入到其他容器视图(如自定义的Dialog的视图)中

picker.getContentView();

获得按钮视图(需要先调用picker.show()方法),可以调用该视图相关方法,如setVisibility()

picker.getCancelButton();
picker.getSubmitButton();

自定义选择器示例:

        CustomHeaderAndFooterPicker picker = new CustomHeaderAndFooterPicker(this);
picker.setOnOptionPickListener(new OptionPicker.OnOptionPickListener() {
@Override
public void onOptionPicked(int position, String option) {
showToast(option);
}
});
picker.show();

核心滚轮控件为WheelView,可以参照SinglePicker、DateTimePicker及LinkagePicker自行扩展。


代码下载:AndroidPicker-master.zip

原文链接:https://github.com/gzu-liyujiang/AndroidPicker

收起阅读 »

揭开 LiveData 的通知机制的神秘面纱

LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 ...
继续阅读 »

LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以,就这个机会我们也来分析一下 LiveData 通知的实现过程。

1、一个 LiveData 的问题

LiveData的问题
有两个页面 A 和 B,A 是一个 Fragment ,是一个列表的展示页;B 是其他的页面。首先,A 会更新页面,并且为了防止连续更新,在每次更新之前需要检查一个布尔值,只有为 false 的时候才允许从网络加载数据。每次加载数据之前会将该布尔值置为 true,拿到了结果之后置为 false. 这里拿到的结果是借助 LiveData 来通知给页面进行更新的。

现在,A 打开了 B,B 中对列表中的数据进行了更新,然后发了一条类似于广播的消息。此时,A 接收了消息并进行数据加载。过了一段时间,B 准备退出,再退出的时候又对列表中的项目进行了更新,所以此时又发出了一条消息。

B 关闭了,我们回到了 A 页面。但是,此时,我们发现 A 页面中的数据只包含了第一次的数据更新,第二次的数据更新没有体现在列表中。

用代码来描述的话大致是下面这样,

//  A
public class A extends Fragment {

private boolean loading = false;

private MyViewModel vm;

// ......

/**
* Register load observer.
*/
public void registerObservers() {
vm.getData().observe(this, resources -> {
loading = false;
// ... show in list
})
}

/**
* Load data from server.
*/
public void loadData() {
if (loading) return;
loading = true;
vm.load();
}

/**
* On receive message.
*/
public void onReceive() {
loadData();
}
}

public class B extends Activity {

public void doBusiness1() {
sendMessage(MSG); // Send message when on foreground.
}

@Override
public void onBackpressed() {
// ....
sendMessage(MSG); // Send message when back
}
}

public class MyViewModel extends ViewModel {

private MutableLiveData> data;

public MutableLiveData> getData() {
if (data == null) {
data = new MutableLiveData<>();
}
return data;
}

public void load() {
Object result = AsyncGetData.getData(); // Get data
if (data != null) {
data.setValue(Resouces.success(result));
}
}
}


A 打开了 B 之后,A 处于后台,B 处于前台。此时,B 调用 doBusiness1() 发送了一条消息 MSG,A 中在 onReceive() 中收到消息,并调用 loadData() 加载数据。然后,B 处理完了业务,准备退出的时候发现其他数据发生了变化,所以又发了一条消息,然后 onReceive() 中收到消息,并调用 loadData(). 但此时发现 loading 为 true. 所以,我们后来对数据的修改没有体现到列表上面。

2、问题的原因

如果用上面的示例代码作为例子,那么出现问题的原因就是当 A 处于后台的时候。虽然调用了 loadData() 并且从网络中拿到了数据,但是调用 data.setValue() 方法的时候无法通知到 A 中。所以,loading = false 这一行无法被调用到。第二次发出通知的时候,一样调用到了 loadData(),但是因为此时 loading 为 true,所以并没有执行加载数据的操作。而当从 B 中完全回到 A 的时候,第一次加载的数据被 A 接收到。所以,列表中的数据是第一次加载时的数据,第二次加载事件丢失了。

解决这个问题的方法当然比较简单,可以当接收到事件的时候使用布尔变量监听,然后回到页面的时候发现数据发生变化再执行数据加载:

// 类 A
public class A extends Fragment {

private boolean dataChanged;

/**
* On receive message.
*/
public void onReceive() {
dataChanged = true;
}

@Override
public void onResume() {
// ...
if (dataChanged) {
loadData();
}
}
}


对于上面的问题,当我们调用了 setValue() 之后将调用到 LiveData 类的 setValue() 方法,

@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}


这里表明该方法必须在主线程中被调用,最终事件的分发将会交给 dispatchingValue() 方法来执行:

private void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
// 发送事件
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}


然后,会调用 considerNotify() 方法来最终将事件传递出去,

private void considerNotify(ObserverWrapper observer) {
// 这里会因为当前的 Fragment 没有处于 active 状态而退出方法
if (!observer.mActive) {
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}


这里会因为当前的 Fragment 没有处于 active 状态而退出 considerNotify() 方法,从而消息无法被传递出去。

3、LiveData 的通知机制

LiveData 的通知机制并不复杂,它的类主要包含在 livedata-core 包下面,总共也就 3 个类。LiveData 是一个抽象类,它有一个默认的实现就是 MutableLiveData.

LiveData 主要依靠内部的变量 mObservers 来缓存订阅的对象和订阅信息。其定义如下,使用了一个哈希表进行缓存和映射,

private SafeIterableMap, ObserverWrapper> mObservers = new SafeIterableMap<>();
每当我们调用一次 observe() 方法的时候就会有一个映射关系被加入到哈希表中,

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// 持有者当前处于被销毁状态,因此可以忽略此次观察
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


从上面的代码我们可以看出,添加到映射关系中的类会先被包装成 LifecycleBoundObserver 对象。然后使用该对象对 owner 的生命周期进行监听。

这的 LifecycleBoundObserver 和 ObserverWrapper 两个类的定义如下,

class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull final LifecycleOwner mOwner;

LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) {
super(observer);
mOwner = owner;
}

@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}

@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}

@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}

private abstract class ObserverWrapper {
final Observer mObserver;
boolean mActive;
int mLastVersion = START_VERSION;

ObserverWrapper(Observer observer) {
mObserver = observer;
}

abstract boolean shouldBeActive();

boolean isAttachedTo(LifecycleOwner owner) {
return false;
}

void detachObserver() {}

void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this);
}
}
}


上面的类中我们先来关注 LifecycleBoundObserver 中的 onStateChanged() 方法。该方法继承自 LifecycleObserver. 这里的 Lifecycle.Event 是一个枚举类型,定义了一些与生命周期相关的枚举值。所以,当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出,该方法内部又调用了基类的 activeStateChanged() 方法,该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。

继续看 activeStateChanged() 方法,我们可以看出在最后的几行中,它调用了 dispatchingValue(this) 方法。所以,当 Fragment 从处于后台切换到前台之后,会将当前缓存的值通知给观察者。

那么值是如何缓存的,以及缓存了多少值呢?回到之前的 setValue() 和 dispatchingValue() 方法中,我们发现值是以一个单独的变量进行缓存的,

private volatile Object mData = NOT_SET;
因此,在我们的示例中,当页面从后台切换到前台的时候,只能将最后一次缓存的结果通知给观察者就真相大白了。

总结

从上面的分析中,我们对 LiveData 总结如下,

当调用 observe() 方法的时候,我们的观察者将会和 LifecycleOwner (Fragment 或者 Activity) 一起被包装到一个类中,并使用哈希表建立映射关系。同时,还会对 Fragment 或者 Activity 的生命周期方法进行监听,以此来达到监听观察者是否处于 active 状态的目的。

当 Fragment 或者 Activity 处于后台的时候,其内部的观察者将处于非 active 状态,此时使用 setValue() 设置的值会缓存到 LiveData 中。但是这种缓存只能缓存一个值,新的值会替换旧的值。因此,当页面从后台恢复到前台的时候只有最后设置的一个值会被传递给观察者。

在 2 中的当 Fragment 或者 Activity 从后台恢复的时候进行通知也是通过监听其生命周期方法实现的。

调用了 observe() 之后,Fragment 或者 Activity 被缓存了起来,不会造成内存泄漏吗?答案是不会的。因为 LiveData 可以对其生命周期进行监听,当其处于销毁状态的时候,该映射关系将被从缓存中移除。

通俗地讲,

LiveData 里面保存了一个值,当 Fragment 处于后台更新这个值的时候,会直接把这个值改掉,但是不会通知到观察者,直到 Fragment 从后台回来的时候才通知给观察者。而这里 LiveData 感知 Fragment 处于后台还是前台是依靠 LifecycleObserver 的通知机制来完成的。

收起阅读 »

揭开 ViewModel 的生命周期控制的神秘面纱

1、从一个 Bug 说起想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。起因是这样的,昨天在...
继续阅读 »

1、从一个 Bug 说起

想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。

起因是这样的,昨天在修复程序中的 Bug 的时候遇到了一个异常,是从 ViewModel 中获取存储的数据的时候报了空指针。我启用了开发者模式的 “不保留活动” 之后很容易地重现了这个异常。出现错误的原因也很简单,相关的代码如下:

private ReceiptViewerViewModel viewModel;

@Override
protected void doCreateView(Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
handleIntent(savedInstanceState);
// ...
}

private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}


在方法 doCreateView() 中我获取了 viewModel 实例,然后在 handleIntent() 方法中从 Intent 中取出传入的参数。当然,还要使用 viewModel 的 getter 方法从其中取出 loadingStatus 并使用。在使用的时候抛了空指针。

显然,一般情况下是不会出现问题的,但是如果 Activity 在后台被销毁了,那么再重建的时候就会出现空指针异常。

解决方法也比较简单,在 onSaveInstanceState() 方法中将数据缓存起来即可,即:

private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
} else {
loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
}


现在的问题是 ViewModel 的生命周期问题,有人说在 doCreateView() 方法的 1 处得到的不是之前的 ViewModel 吗,数据不是之前已经设置过了吗?所以,这牵扯 ViewModel 是在什么时候被销毁和重建的问题。

2、ViewModel 的生命周期

有的人希望使用 ViewModel 缓存 Activity 的信息,然后在 doCreateView() 方法的 1 处得到之前的 ViewModel 实例,这样 ViewModel 的数据就是 Activity 销毁之前的数据,这可行吗?我们从源码角度来看下这个问题。

首先,每次获取 viewmodel 实例的时候都会调用下面的方法来获取 ViewModel 实例。从下面的 get() 方法中可以看出,实例化过的 ViewModel 是从 mViewModelStore 中获取的。如果由 ViewModelStores.of(activity) 方法得到的 mViewModelStore 不是同一个,那么得到的 ViewModel 也不是同一个。

下面方法中的 get() 方法中后续的逻辑是如果之前没有缓存过 ViewModel,那么就构建一个新的实例并将其放进 mViewModelStore 中。这部分代码逻辑比较简单,我们不继续分析了。

// ViewModelProviders#of()
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
ViewModelProvider.AndroidViewModelFactory factory =
ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
}

// ViewModelProvider#get()
public T get(@NonNull String key, @NonNull Class modelClass) {
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}

viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}


我们回到上述 of() 方法的 1 处,来看下 ViewModelStores.of() 方法,其定义如下:

// ViewModelStores#of()
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();
}

// HolderFragment#holderFragmentFor()
public static HolderFragment holderFragmentFor(FragmentActivity activity) {
return sHolderFragmentManager.holderFragmentFor(activity);
}


这里会从 holderFragmentFor() 方法中获取一个 HolderFragment 实例,它是一个 Fragment 的实现类。然后从该实例中获取 ViewModelStore 的实例。所以,ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致,就是使用一个空的 Fragment 来进行生命周期管理。

对于 HolderFragment,其定义如下。从下面的代码我们可以看出,上述用到的 ViewModelStore 实例就是 HolderFragment 的一个局部变量。所以,ViewModel 使用空的 Fragment 管理生命周期实锤了。

public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
private ViewModelStore mViewModelStore = new ViewModelStore();

public HolderFragment() {
setRetainInstance(true);
}

// ...
}


此外,我们注意到上面的 HolderFragment 的构造方法中还调用了 setRetainInstance(true) 这一行代码。我们进入该方法看它的注释:

Control whether a fragment instance is retained across Activityre-creation (such as from a configuration change). This can onlybe used with fragments not in the back stack. If set, the fragmentlifecycle will be slightly different when an activity is recreated:

就是说,当 Activity 被重建的时候该 Fragment 会被保留,然后传递给新创建的 Activity. 但是,这只适用于不处于后台的 Fragment. 所以,如果 Activity 处于后台的时候,Fragment 不会保留,那么它得到的 ViewModelStore 实例就不同了。

所以,总结下来,准确地讲:当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。


回到之前的 holderFragmentFor() 方法,我们看下这里具体做了什么,其定义如下。

// HolderFragmentManager#holderFragmentFor()
HolderFragment holderFragmentFor(FragmentActivity activity) {
// 使用 FragmentManager 获取 HolderFragment
FragmentManager fm = activity.getSupportFragmentManager();
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
// 从哈希表中获取 HolderFragment
holder = mNotCommittedActivityHolders.get(activity);
if (holder != null) {
return holder;
}

if (!mActivityCallbacksIsAdded) {
mActivityCallbacksIsAdded = true;
activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
}
holder = createHolderFragment(fm);
// 将新的实例放进哈希表中
mNotCommittedActivityHolders.put(activity, holder);
return holder;
}


首先,尝试使用 FragmentManager 来获取 HolderFragment,如果获取不到就从 mNotCommittedActivityHolders 中进行获取。这里的 mNotCommittedActivityHolders 是一个哈希表,每次实例化的新的 HolderFragment 会被添加到哈希表中。

另外,上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下,

private ActivityLifecycleCallbacks mActivityCallbacks =
new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
}
};


当 Activity 被销毁的时候会从哈希表中移除映射关系。所以,每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 setRetainInstance(true) 和 findHolderFragment(fm) 来实现的。

总结
以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程,实际上内部还有许多小细节,逻辑也比较简单,我们就不一一说明了。

viewmodel-lifecycle
这里使用了 Activity rotated,也就是 Activity 处于前台的时候配置发生变化的情况,而不是处于后台,不知道你之前有没有注意这一点呢?

原文链接:揭开 ViewModel 的生命周期控制的神秘面纱

收起阅读 »

一文说透 Android 应用架构 MVC、MVP、MVVM 和 组件化

MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和...
继续阅读 »

MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后,才能在进行开发的时候选择适合自己项目的架构模式,这也是本文的目的。

1、MVC

MVC (Model-View-Controller, 模型-视图-控制器),标准的 MVC 是这个样子的:

模型层 (Model):业务逻辑对应的数据模型,无 View 无关,而与业务相关;

视图层 (View):一般使用 XML 或者 Java 对界面进行描述;

控制层 (Controllor):在 Android 中通常指 Activity 和 Fragment,或者由其控制的业务类。

Activity 并非标准的 Controller,它一方面用来控制了布局,另一方面还要在 Activity 中写业务代码,造成了 Activity 既像 View 又像Controller。

在 Android 开发中,就是指直接使用 Activity 并在其中写业务逻辑的开发方式。显然,一方面 Activity 本身就是一个视图,另一方面又要负责处理业务逻辑,因此逻辑会比较混乱。

这种开发方式不太适合 Android 开发。

2、MVP

2.1 概念梳理

MVP (Model-View-Presenter) 是 MVC 的演化版本,几个主要部分如下:

模型层 (Model):主要提供数据存取功能。

视图层 (View):处理用户事件和视图。在 Android 中,可能是指 Activity、Fragment 或者 View。

展示层 (Presenter):负责通过 Model 存取数据,连接 View 和 Model,从 Model 中取出数据交给 View。

所以,对于 MVP 的架构设计,我们有以下几点需要说明:

这里的 Model 是用来存取数据的,也就是用来从指定的数据源中获取数据,不要将其理解成 MVC 中的 Model。在 MVC 中 Model 是数据模型,在 MVP 中,我们用 Bean 来表示数据模型。

Model 和 View 不会直接发生关系,它们需要通过 Presenter 来进行交互。在实际的开发中,我们可以用接口来定义一些规范,然后让我们的 View 和 Model 实现它们,并借助 Presenter 进行交互即可。

为了说明 MVP 设计模式,我们给出一个示例程序。你可以在 Android-references 中获取到它的源代码。

2.2 示例程序

在该示例中,我们使用了:

开眼视频的 API 作为数据源;

Retrofit 进行数据访问;

使用 ARouter 进行路由;

使用 MVP 设计模式作为程序架构。



这里我们首先定义了 MVP 模式中的最顶层的 View 和 Presenter,在这里分别是 BaseView 和 BasePresenter,它们在该项目中是两个空的接口,在一些项目中,我们可以根据自己的需求在这两个接口中添加自己需要的方法。

然后,我们定义了 HomeContract。它是一个抽象的接口,相当于一层协议,用来规定指定的功能的 View 和 Presenter 分别应该具有哪些方法。通常,对于不同的功能,我们需要分别实现一个 MVP,每个 MVP 都会有一个对应的 Contract。笔者认为它的好处在于,将指定的 View 和 Presenter 的接口定义在一个接口中,更加集中。它们各自需要实现的方法也一目了然地展现在了我们面前。

这里根据我们的业务场景,该接口的定义如下:

public interface HomeContract {

interface IView extends BaseView {
void setFirstPage(List itemLists);
void setNextPage(List itemLists);
void onError(String msg);
}

interface IPresenter extends BasePresenter {
void requestFirstPage();
void requestNextPage();
}
}


HomeContract 用来规定 View 和 Presenter 应该具有的操作,在这里它用来指定主页的 View 和 Presenter 的方法。从上面我们也可以看出,这里的 IView 和 IPresenter 分别实现了 BaseView 和 BasePresenter。

上面,我们定义了 V 和 P 的规范,MVP 中还有一项 Model,它用来从网络中获取数据。这里我们省去网络相关的具体的代码,你只需要知道 APIRetrofit.getEyepetizerService() 是用来获取 Retrofit 对应的 Service,而 getMoreHomeData() 和 getFirstHomeData() 是用来从指定的接口中获取数据就行。下面是 HomeModel 的定义:

public class HomeModel {

public Observable getFirstHomeData() {
return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());
}

public Observable getMoreHomeData(String url) {
return APIRetrofit.getEyepetizerService().getMoreHomeData(url);
}
}


OK,上面我们已经完成了 Model 的定义和 View 及 Presenter 的规范的定义。下面,我们就需要具体去实现 View 和 Presenter。

首先是 Presenter,下面是我们的 HomePresenter 的定义。在下面的代码中,为了更加清晰地展示其中的逻辑,我删减了一部分无关代码:

public class HomePresenter implements HomeContract.IPresenter {

private HomeContract.IView view;

private HomeModel homeModel;

private String nextPageUrl;

// 传入View并实例化Model
public HomePresenter(HomeContract.IView view) {
this.view = view;
homeModel = new HomeModel();
}



// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

@Override
public void requestFirstPage() {
Disposable disposable = homeModel.getFirstHomeData()
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}



// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

@Override
public void requestNextPage() {
Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}
}


从上面我们可以看出,在 Presenter 需要将 View 和 Model 建立联系。我们需要在初始化的时候传入 View,并实例化一个 Model。Presenter 通过 Mode l获取数据,并在拿到数据的时候,通过 View 的方法通知给 View 层。

然后,就是我们的 View 层的代码,同样,我对代码做了删减:

@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity implements HomeContract.IView {

// 实例化Presenter
private HomeContract.IPresenter presenter;
{
presenter = new HomePresenter(this);
}

@Override
protected int getLayoutResId() {
return R.layout.activity_eyepetizer_menu;
}

@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...
// 使用Presenter请求数据
presenter.requestFirstPage();
loading = true;
}

private void configList() {
// ...
getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 请求下一页的数据
presenter.requestNextPage();
}
}
});
}



// 当请求到结果的时候在页面上做处理,展示到页面上

@Override
public void setFirstPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}



// 当请求到结果的时候在页面上做处理,展示到页面上

@Override
public void setNextPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}

@Override
public void onError(String msg) {
ToastUtils.makeToast(msg);
}

// ...
}


从上面的代码中我们可以看出实际在 View 中也要维护一个 Presenter 的实例。当需要请求数据的时候会使用该实例的方法来请求数据,所以,在开发的时候,我们需要根据请求数据的情况,在 Presenter 中定义接口方法。

实际上,MVP 的原理就是 View 通过 Presenter 获取数据,获取到数据之后再回调 View 的方法来展示数据。

另外一个值得注意的地方就是,在实际的使用过程中,尤其是进行异步请求的时候,为了给 Model 和 View 之间解耦,我们会在 Presenter 中使用 Handler 收发消息来建立两者之间的桥梁。

2.3 MVC 和 MVP 的区别

MVC 中是允许 Model 和 View 进行交互的,而MVP中,Model 与 View 之间的交互由Presenter完成;

MVP 模式就是将 P 定义成一个接口,然后在每个触发的事件中调用接口的方法来处理,也就是将逻辑放进了 P 中,需要执行某些操作的时候调用 P 的方法就行了。

2.4 MVP的优缺点

优点:

降低耦合度,实现了 Model 和 View 真正的完全分离,可以修改 View 而不影响 Modle;

模块职责划分明显,层次清晰;

隐藏数据;

Presenter 可以复用,一个 Presenter 可以用于多个 View,而不需要更改 Presenter 的逻辑;

利于测试驱动开发,以前的 Android 开发是难以进行单元测试的;

View 可以进行组件化,在 MVP 当中,View 不依赖 Model。

缺点:

Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难;

由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁;

如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密,一旦视图需要变更,那么 Presenter 也需要变更了。

3、MVVM (分手大师)

3.1 基础概念

MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

模型层 (Model):负责从各种数据源中获取数据;

视图层 (View):在 Android 中对应于 Activity 和 Fragment,用于展示给用户和处理用户交互,会驱动 ViewModel 从 Model 中获取数据;

ViewModel 层:用于将 Model 和 View 进行关联,我们可以在 View 中通过 ViewModel 从 Model 中获取数据;当获取到了数据之后,会通过自动绑定,比如 DataBinding,来将结果自动刷新到界面上。

使用 Google 官方的 Android Architecture Components ,我们可以很容易地将 MVVM 应用到我们的应用中。下面,我们就使用它来展示一下 MVVM 的实际的应用。你可以在 Android-references 中获取到它的源代码。

3.2 示例程序

在该项目中,我们使用了:

果壳网的 API 作为数据源;

使用 Retrofit 进行网络数据访问;

使用 ViewMdeol 作为整体的架构设计。

mvvm
这里的 model.data 下面的类是对应于网络的数据实体的,由 JSON 自动生成,这里我们不进行详细描述。这里的 model.repository 下面的两个类是用来从网络中获取数据信息的,我们也忽略它的定义。

上面就是我们的 Model 的定义,并没有太多的内容,基本与 MVP 一致。

下面的是 ViewModel 的代码,我们选择了其中的一个方法来进行说明。当我们定义 ViewModel 的时候,需要继承 ViewModel 类。

public class GuokrViewModel extends ViewModel {

public LiveData> getGuokrNews(int offset, int limit) {
MutableLiveData> result = new MutableLiveData<>();
GuokrRetrofit.getGuokrService().getNews(offset, limit)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onError(Throwable e) {
result.setValue(Resource.error(e.getMessage(), null));
}

@Override
public void onComplete() { }

@Override
public void onSubscribe(Disposable d) { }

@Override
public void onNext(GuokrNews guokrNews) {
result.setValue(Resource.success(guokrNews));
}
});
return result;
}
}


这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,所以,为了使用它,我们还需要加入下面的依赖:

api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"


在 ViewModel 的定义中,我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候,我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,我们只需要调用该方法,并对返回的 LiveData 进行"监听"即可。这里,我们将错误信息和返回的数据信息进行了封装,并且封装了一个代表当前状态的枚举信息,你可以参考源代码来详细了解下这些内容。

上面我们定义完了 Model 和 ViewModel,下面我们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。

@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment {

private GuokrViewModel guokrViewModel;

private int offset = 0;

private final int limit = 20;

private GuokrNewsAdapter adapter;

@Override
protected int getLayoutResId() {
return R.layout.fragment_news_list;
}

@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...

guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);

fetchNews();
}

private void fetchNews() {
guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
if (guokrNewsResource == null) {
return;
}
switch (guokrNewsResource.status) {
case FAILED:
ToastUtils.makeToast(guokrNewsResource.message);
break;
case SUCCESS:
adapter.addData(guokrNewsResource.data.getResult());
adapter.notifyDataSetChanged();
break;
}
});
}
}


以上就是我们的 View 层的定义,这里我们先使用了

这里的view.fragment包下面的类对应于实际的页面,这里我们 ViewModelProviders 的方法来获取我们需要使用的 ViewModel,然后,我们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”即可。

以上就是 MVVM 的基本使用,当然,这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。

3.3 MVVM 的优点和缺点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

低耦合:视图(View)可以独立于Model变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。

可重用性:你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。

独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

可测试:界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。

4、组件化

4.1 基础概念

所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。

简单了解了组件化之后,让我们来看一下如何实现组件化开发。你可能之前听说过组件化开发,或者被其高大上的称谓吓到了,但它实际应用起来并不复杂,至少借助了现成的框架之后并不复杂。这里我们先梳理一下,在应用组件化的时候需要解决哪些问题:

如何分成各个模块?我们可以根据业务来进行拆分,对于比较大的功能模块可以作为应用的一个模块来使用,但是也应该注意,划分出来的模块不要过多,否则可能会降低编译的速度并且增加维护的难度。

各个模块之间如何进行数据共享和数据通信?我们可以把需要共享的数据划分成一个单独的模块来放置公共数据。各个模块之间的数据通信,我们可以使用阿里的 ARouter 进行页面的跳转,使用封装之后的 RxJava 作为 EventBus 进行全局的数据通信。

如何将各个模块打包成一个独立的 APP 进行调试?首先这个要建立在2的基础上,然后,我们可以在各个模块的 gradle 文件里面配置需要加载的 AndroidManifest.xml 文件,并可以为每个应用配置一个独立的 Application 和启动类。

如何防止资源名冲突问题?遵守命名规约就能规避资源名冲突问题。

如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题?可以将各个模块公用的依赖的版本配置到 settings.gradle 里面,并且可以建立一个公共的模块来配置所需要的各种依赖。

Talk is cheap,下面让我们动手实践来应用组件化进行开发。你可以在Github中获取到它的源代码。

4.2 组件化实践

1. 包结构

首先,我们先来看整个应用的包的结构。如下图所示,该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径,我推荐使用这种方式,而不是将各个模块放置在 app 下面,因为这样看起来更加的清晰。为了达到这个目的,你只需要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径即可。注意在实际应用的时候模块的路径的关系,不要搞错了。

组件化
然后,我们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖,这里我们将两者放在了一个模块中以减少模块的数量。下面是它的 gradle 的部分配置。这里我们使用了 api 来引入各个依赖,以便在其他的模块中也能使用这些依赖。

dependencies {
api fileTree(include: ['*.jar'], dir: 'libs')
// ...
// router
api 'com.alibaba:arouter-api:1.3.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
// walle
api 'com.meituan.android.walle:library:1.1.6'
// umeng
api 'com.umeng.sdk:common:1.5.3'
api 'com.umeng.sdk:analytics:7.5.3'
api files('libs/pldroid-player-1.5.0.jar')
}


2. 路由

接着,我们来看一下路由框架的配置。这里,我们使用阿里的 ARouter 来进行页面之间的跳转,你可以在 Github 上面了解该框架的配置和使用方式。这里我们只讲解一下在组件化开发的时候需要注意的地方。注意到 ARouter 是通过注解来进行页面配置的,并且它的注解是在编译的时候进行处理的。所以,我们需要引入 arouter-compiler 来使用它的编译时处理功能。需要注意的地方是,我们只要在公共的模块中加入 arouter-api 就可以使用 ARouter 的 API 了,但是需要在每个模块中引入 arouter-compiler 才能使用编译时注解。也就是说,我们需要在每个模块中都加入 arouter-compiler 依赖。

ARouter 的实现原理使用了 Java 中的注解处理,你可以通过阅读我之前写的一篇文章来了解 《Java 开发者核心技能之 Java 注解及其典型的使用方法》。另外,我们也会在后续的文章中分析 ARouter 的实现原理,欢迎关注我的公众号「Code Brick」。

3. 模块独立

为了能够将各个模块编译成一个独立的 APP,我们需要在 Gradle 里面做一些配置。

首先,我们需要在 gradle.properties 定义一些布尔类型的变量用来判断各个模块是作为一个 library 还是 application 进行编译。这里我的配置如下面的代码所示。也就是,我为每个模块都定义了这么一个布尔类型的变量,当然,你也可以只定义一个变量,然后在各个模块中使用同一个变量来进行判断。
isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false


然后,我们来看一下各个模块中的 gradle 该如何配置,这里我们以开眼视频的功能模块作为例子来进行讲解。首先,一个模块作为 library 还是 application 是根据引用的 plugin 来决定的,所以,我们要根据之前定义的布尔变量来决定使用的 plugin:

if (isEyepetizerModuleApp.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}


假如我们要将某个模块作为一个独立的 APP,那么启动类你肯定需要配置。这就意味着你需要两个 AndroidManifest.xml 文件,一个用于 library 状态,一个用于 application 状态。所以,我们可以在 main 目录下面再定义一个 AndroidManifest.xml,然后,我们在该配置文件中不只指定启动类,还使用我们定义的 Application。指定 Application 有时候是必须的,比如你需要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了,可以参考源码,这里我们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。

如下所示,我们可以根据之前定义的布尔值来决定使用哪一个配置文件:

sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (isEyepetizerModuleApp.toBoolean()) {
manifest.srcFile "src/main/debug/AndroidManifest.xml"
} else {
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}


此外,还需要注意的是,如果我们希望在每个模块中都能应用 DataBinding 和 Java 8 的一些特性,那么你需要在每个模块里面都加入下面的配置:

// use data binding
dataBinding {
enabled = true
}
// use java 8 language
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}


对于编译时注解之类的配置,我们也需要在每个模块里面都进行声明。

完成了以上的配置,我们只要根据需要编译的类型,修改之前定义的布尔值,来决定是将该模块编译成 APP 还是作为类库来使用即可。

以上就是组件化在 Android 开发当中的应用。

总结
MVC、MVP 和 MVVM 各有各自的特点,可以根据应用开发的需要选择适合自己的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协作。它们之间的关系就是,你可以在组件化的各个模块中应用前面三种架构模式的一种或者几种。
原文链接:https://mp.weixin.qq.com/s?__biz=MzA3MzgzMzgyNw==&mid=2247483866&idx=1&sn=5b3746c7b82e779882cc5c670a30b217&chksm=9f084dd6a87fc4c0453eeb10f1f8442bdae225fb1f6cad7b2655a20383507d5709d9e3aa749f&token=1361179115&lang=zh_CN#rd

收起阅读 »

时间选择器和选项选择器-Android-PickerView

Android-PickerView介绍这是一款仿iOS的PickerView控件,有时间选择器和选项选择器,新版本的详细特性如下:——TimePickerView 时间选择器,支持年月日时分,年月日,年月,时分等格式。——OptionsPickerView ...
继续阅读 »

Android-PickerView

介绍

这是一款仿iOS的PickerView控件,有时间选择器和选项选择器,新版本的详细特性如下:

——TimePickerView 时间选择器,支持年月日时分,年月日,年月,时分等格式。
——OptionsPickerView 选项选择器,支持一,二,三级选项选择,并且可以设置是否联动 。

  • 支持三级联动
  • 设置是否联动
  • 设置循环模式
  • 支持自定义布局。
  • 支持item的分隔线设置。
  • 支持item间距设置。
  • 时间选择器支持起始和终止日期设定。
  • 支持“年,月,日,时,分,秒”,“省,市,区”等选项的单位(label)显示、隐藏和自定义。
  • 支持自定义文字、颜色、文字大小等属性
  • Item的文字长度过长时,文字会自适应缩放到Item的长度,避免显示不完全的问题
  • 支持Dialog 模式。
  • 支持自定义设置容器。
  • 实时回调。

使用注意事项

  • 注意:当我们进行设置时间的启始位置时,需要特别注意月份的设定
  • 原因:Calendar组件内部的月份,是从0开始的,即0-11代表1-12月份
  • 错误使用案例: startDate.set(2013,1,1);  endDate.set(2020,12,1);
  • 正确使用案例: startDate.set(2013,0,1);  endDate.set(2020,11,1);

如何使用:

Android-PickerView 库使用示例:

1.添加Jcenter仓库 Gradle依赖:

compile 'com.contrarywind:Android-PickerView:4.1.9'


或者

Maven


com.contrarywind
Android-PickerView
4.1.9
pom


2.在项目中添加如下代码:

//时间选择器
TimePickerView pvTime = new TimePickerBuilder(MainActivity.this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date, View v) {
Toast.makeText(MainActivity.this, getTime(date), Toast.LENGTH_SHORT).show();
}
}).build();

//条件选择器
OptionsPickerView pvOptions = new OptionsPickerBuilder(MainActivity.this, new OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}).build();
pvOptions.setPicker(options1Items, options2Items, options3Items);
pvOptions.show();


大功告成~

3.如果默认样式不符合你的口味,可以自定义各种属性:

Calendar selectedDate = Calendar.getInstance();
Calendar startDate = Calendar.getInstance();
//startDate.set(2013,1,1);
Calendar endDate = Calendar.getInstance();
//endDate.set(2020,1,1);

//正确设置方式 原因:注意事项有说明
startDate.set(2013,0,1);
endDate.set(2020,11,31);

pvTime = new TimePickerBuilder(this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date,View v) {//选中事件回调
tvTime.setText(getTime(date));
}
})
.setType(new boolean[]{true, true, true, true, true, true})// 默认全部显示
.setCancelText("Cancel")//取消按钮文字
.setSubmitText("Sure")//确认按钮文字
.setContentSize(18)//滚轮文字大小
.setTitleSize(20)//标题文字大小
.setTitleText("Title")//标题文字
.setOutSideCancelable(false)//点击屏幕,点在控件外部范围时,是否取消显示
.isCyclic(true)//是否循环滚动
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF666666)//标题背景颜色 Night mode
.setBgColor(0xFF333333)//滚轮背景颜色 Night mode
.setDate(selectedDate)// 如果不设置的话,默认是系统时间*/
.setRangDate(startDate,endDate)//起始终止年月日设定
.setLabel("年","月","日","时","分","秒")//默认设置为年月日时分秒
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.isDialog(true)//是否显示为对话框样式
.build();
pvOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}) .setOptionsSelectChangeListener(new OnOptionsSelectChangeListener() {
@Override
public void onOptionsSelectChanged(int options1, int options2, int options3) {
String str = "options1: " + options1 + "\noptions2: " + options2 + "\noptions3: " + options3;
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
})
.setSubmitText("确定")//确定按钮文字
.setCancelText("取消")//取消按钮文字
.setTitleText("城市选择")//标题
.setSubCalSize(18)//确定和取消文字大小
.setTitleSize(20)//标题文字大小
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF333333)//标题背景颜色 Night mode
.setBgColor(0xFF000000)//滚轮背景颜色 Night mode
.setContentTextSize(18)//滚轮文字大小
.setLinkage(false)//设置是否联动,默认true
.setLabels("省", "市", "区")//设置选择的三级单位
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.setCyclic(false, false, false)//循环与否
.setSelectOptions(1, 1, 1) //设置默认选中项
.setOutSideCancelable(false)//点击外部dismiss default true
.isDialog(true)//是否显示为对话框样式
.isRestoreItem(true)//切换时是否还原,设置默认选中第一项。
.build();

pvOptions.setPicker(options1Items, options2Items, options3Items);//添加数据源


4.如果需要自定义布局:

// 注意:自定义布局中,id为 optionspicker 或者 timepicker 的布局以及其子控件必须要有,否则会报空指针
// 具体可参考demo 里面的两个自定义布局
pvCustomOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3, View v) {
//返回的分别是三个级别的选中位置
String tx = cardItem.get(options1).getPickerViewText();
btn_CustomOptions.setText(tx);
}
})
.setLayoutRes(R.layout.pickerview_custom_options, new CustomListener() {
@Override
public void customLayout(View v) {
//自定义布局中的控件初始化及事件处理
final TextView tvSubmit = (TextView) v.findViewById(R.id.tv_finish);
final TextView tvAdd = (TextView) v.findViewById(R.id.tv_add);
ImageView ivCancel = (ImageView) v.findViewById(R.id.iv_cancel);
tvSubmit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.returnData(tvSubmit);
}
});
ivCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.dismiss();
}
});

tvAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getData();
pvCustomOptions.setPicker(cardItem);
}
});

}
})
.build();
pvCustomOptions.setPicker(cardItem);//添加数据


5.对使用还有疑问的话,可参考demo代码

请戳我查看demo代码

6.若只需要WheelView基础控件自行扩展实现逻辑,可直接添加基础控件库,Gradle 依赖:

compile 'com.contrarywind:wheelview:4.1.0'


WheelView 使用代码示例:

xml布局:

            android:id="@+id/wheelview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


Java 代码:

WheelView wheelView = findViewById(R.id.wheelview);

wheelView.setCyclic(false);

final List mOptionsItems = new ArrayList<>();
mOptionsItems.add("item0");
mOptionsItems.add("item1");
mOptionsItems.add("item2");

wheelView.setAdapter(new ArrayWheelAdapter(mOptionsItems));
wheelView.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(int index) {
Toast.makeText(MainActivity.this, "" + mOptionsItems.get(index), Toast.LENGTH_SHORT).show();
}
});


代码下载:Android-PickerView-master.zip

原文链接:https://github.com/Bigkoo/Android-PickerView

收起阅读 »

今日头条屏幕适配方案终极版正式发布!

前言 我在前面两篇文章中详细介绍了 今日头条适配方案 和 SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定...
继续阅读 »

前言


我在前面两篇文章中详细介绍了 今日头条适配方案SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定的用户基数


但是对于一些才接触这两个方案的朋友,肯定或多或少还是不知道如何选择这两个方案,我虽然在之前的文章中给出了它们各自的优缺点,但是并没有用统一的标准对它们进行更细致的对比,所以也就没办法更形象的体现它们的优劣,那下面我就用统一的标准对它们进行对比,看看它们的对比情况


方案对比


我始终坚定地认为在这两个方案中,并不能以单个标准就能评判出谁一定比谁好,因为它们都有各自的优缺点,都不是完美的,从更客观的角度来看,它们谁都不能成为最好的那个,只有可能明确了它们各自的优缺点,知道在它们的优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,从而选择出一个最适合自己项目的屏幕适配方案


单纯的争论谁是最好的 Android 屏幕适配方案没有任何意义,每个人的需求不一样,站的角度不一样,评判标准也不一样,你能接受的东西他不一定能接受,你觉得不可接受的东西他却觉得可以接受,你有你的理由,他有他的理由,想让一个观点让所有人都能接受太难了!所以我在这里只是列出它们的对比项和对比结果,尽可能的做到客观,最后的选择结果请自行决定,如果还有什么遗漏的对比项,请补充!


































































对比项目 对比对象 A 对比结果 对比对象 B
适配效果(越高越好) 今日头条适配方案 SW 限定符适配方案(在未覆盖的机型上会存在一定的误差)
稳定性(越高越好) 今日头条适配方案 < SW 限定符适配方案
灵活性(越高越好) 今日头条适配方案 > SW 限定符适配方案
扩展性(越高越好) 今日头条适配方案 > SW 限定符适配方案
侵入性(越低越好) 今日头条适配方案 < SW 限定符适配方案
使用成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
维护成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
性能损耗 今日头条适配方案没有性能损耗 = SW 限定符适配方案没有性能损耗
副作用 今日头条适配方案会影响一些三方库和系统控件 SW 限定符适配方案会影响 App 的体积

可以看到 SmallestWidth 限定符适配方案今日头条适配方案 的适配效果其实都是差不多的,我在前面的文章中也通过公式计算过它们的精确度,SmallestWidth 限定符适配方案 运行在未覆盖的机型上虽然也可以适配,但是却会出现一定的误差,所以 今日头条适配方案 的适配精确度确实要比 SmallestWidth 限定符适配方案 略高的,不过只要 SmallestWidth 限定符适配方案 合理的分配资源文件,适配效果的差距应该也不大


SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 今日头条适配方案 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求,简单一句话概括就是,这两兄弟,一个求稳,一个求快,好了,我就介绍这么多了,自己选择吧!



由来


下面就开始介绍我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize,大家千万不要认为,我推出的屏幕适配框架 AndroidAutoSize 是根据 今日头条屏幕适配方案 优化的,我本人就一定支持 今日头条屏幕适配方案 是最好的 Android 屏幕适配方案这个观点,它确实很优秀,但同样也有很多不足,我最真实的观点在上面就已经表述咯,至于我为什么要根据 今日头条屏幕适配方案 再封装一个屏幕适配框架,无外乎就以下几点原因:



  • SmallestWidth 限定符适配方案 已经有多个优秀的开源解决方案了,它们已经能满足我们日常开发中的所有需求


  • 今日头条 官方技术团队只公布了 今日头条屏幕适配方案文章 以及核心代码,但并没有在 Github 上创建公开的仓库,一个新的方案必定要有一个成长迭代的过程,在此期间,一定需要一个可以把所有使用者聚集起来的公共社区,可以让所有使用该方案的使用者在上面交流,大家一起总结、一起填坑,这样才能让该方案更成熟稳定,这就是开源的力量


  • 今日头条 官方技术团队公布的核心代码并不能满足我的所有需求,已经开源的其他基于 今日头条屏幕适配方案 的开源项目以及解决方案也不能满足我的所有需求,而我有更好的实现想法


  • MVPArms 需要一个适配效果还不错并且切换维护成本也比较低的屏幕适配框架,以帮助使用者用较低的成本、工作量将已经停止维护的 AndroidAutoLayout 快速替换掉



我建议大家都可以去实际体验一下 今日头条屏幕适配方案SmallestWidth 限定符适配方案,感受下它们的异同,我给的建议是,可以在项目中先使用 今日头条屏幕适配方案,感受下它的使用方式以及适配效果,今日头条屏幕适配方案 的侵入性非常低,如果在使用过程中遇到什么不能解决的问题,马上可以切换为其他的屏幕适配方案,在切换的过程中也花费不了多少工作量,试错成本非常低


但如果你在项目中先使用 SmallestWidth 限定符适配方案,之后在使用的过程中再遇到什么不能解决的问题,这时想切换为其他的屏幕适配方案,这工作量可就大了,每个 Layout 文件都含有大量的 dimens 引用,改起来这工作量得有多大,想想都觉得后怕,这就是侵入性太高导致的最致命的问题


与今日头条屏幕适配方案的关系


AndroidAutoSize今日头条屏幕适配方案 的关系,相当于汽车和发动机的关系,今日头条屏幕适配方案 官方公布的代码,只实现了修改系统 density 的相关逻辑,这的确在屏幕适配中起到了最关键的作用,但这还远远还不够


要想让使用者能够更傻瓜式的使用该方案,并且能够应对日常开发中的所有复杂需求,那在架构框架时,还需要考虑 API 的易用性以及合理性、框架的扩展性以及灵活性、功能的全面性、注释和文档的易读性等多个方面的问题


于是我带着我的这些标准在网上搜寻了很久,发现并没有任何一个开源框架或解决方案能够达到我的所有标准,它们大多数还只是停留在将 今日头条屏幕适配方案 封装成工具类来引入项目的阶段,这样在功能的扩展上有限制,并且对用户的使用体验也不好,而我想做的是一个全面性的产品级屏幕适配框架,这离我最初的构想,差距还非常大,于是我只好自己动手,将我的所有思想实现,这才有了 AndroidAutoSize


写完 AndroidAutoSize 框架后,因为对 今日头条屏幕适配方案 有了更加深入的理解,所以才写了 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案,以帮助大家更清晰的理解 今日头条屏幕适配方案


与 AndroidAutoLayout 的关系


AndroidAutoSize 因为名字和 鸿神AndroidAutoLayout 非常相似,并且在填写设计图尺寸的方式上也极为相似,再加上我写的屏幕适配系列的文章也发布在了 鸿神 的公众号上,所以很多人以为 AndroidAutoSize鸿神 写的 AndroidAutoLayout 的升级版,这里我哭笑不得 😂,我只好在这里说一句,大家好,我叫 JessYan,的确可以理解为 AndroidAutoSizeAndroidAutoLayout 的升级版,但是它是我写的,关注一波呗


AndroidAutoSizeAndroidAutoLayout 的原理,却天差地别,比如 AndroidAutoLayout 只能使用 px 作为布局单位,而 AndroidAutoSize 恰好相反,在布局中 dp、sp、pt、in、mm 所有的单位都能支持,唯独不支持 px,但这也意味着 AndroidAutoSizeAndroidAutoLayout 在项目中可以共存,互不影响,所以使用 AndroidAutoLayout 的老项目也可以放心的引入 AndroidAutoSize,慢慢的完成屏幕适配框架的切换


之所以将框架取名为 AndroidAutoSize,第一,是想致敬 AndroidAutoLayoutAndroid 屏幕适配领域的贡献,第二,也想成为在 Android 屏幕适配领域有重要影响力的框架


结构


我在上面就已经说了很多开源框架以及解决方案,只是把 今日头条屏幕适配方案 简单的封装成一个工具类然后引入项目,这时很多人就会说了 今日头条屏幕适配方案 官方公布的全部代码都只有 30 行不到,你不把它封装成工具类,那封装成什么?该怎么封装?下面就来看看 AndroidAutoSize 的整体结构

├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java

AndroidAutoSize 根据 今日头条屏幕适配方案 官方公布的 30 行不到的代码,经过不断的优化和扩展,发展成了现在拥有 18 个类文件,上千行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比 今日头条屏幕适配方案 官方公布的原始代码,AndroidAutoSize 更加稳定、更加易用、更加强大,欢迎阅读源码,注释非常详细哦!


功能介绍


AndroidAutoSize 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点



  • 主单位: 使用 dp、sp 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 AndroidAutoSize 也通过这个特性,使用 ExternalAdaptManager 实现了在不修改三方库源码的情况下适配三方库的功能


  • 副单位: 使用 pt、in、mm 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 density 所造成的所有未知和已知问题,但这样 AndroidAutoSize 也就无法对三方库进行适配



大家可以根据自己的应用场景在 主单位副单位 中选择一个作为布局单位,建议想引入老项目并且注重稳定性的人群使用 副单位,只是想试试本框架,随时可能切换为其他屏幕适配方案的人群使用 主单位


其实 AndroidAutoSize 可以同时支持 主单位副单位,但 AndroidAutoSize 可以同时支持 主单位副单位 的目的,只是为了让使用者可以在 主单位副单位 之间灵活切换,因为切换单位的工作量可能非常巨大,不能立即完成,但领导又要求马上打包上线,这时就可以起到一个很好的过渡作用


主单位


主单位Demodemo


基本使用


AndroidAutoSize 引入项目后,只要在 appAndroidManifest.xml 中填写上设计图尺寸,无需其他过多配置 (如果你没有其他自定义需求的话),AndroidAutoSize 即可自动运行,像下面这样👇

                





在使用主单位时,design_width_in_dpdesign_height_in_dp 的单位必须是 dp,如果设计师给你的设计图,只标注了 px 尺寸 (现在已经有很多 UI 工具可以自动标注 dp 尺寸了),那请自行根据公式 dp = px / (DPI / 160)px 尺寸转换为 dp 尺寸,如果你不知道 DPI 是多少?那请以自己测试机的 DPI 为准,如果连怎么得到设备的 DPI 都不知道?百度吧好伐,如果你实在找不到设备的 DPI 那就直接将 px 尺寸除以 3 或者 2 也是可以的


如果你只是想使用 AndroidAutoSize 的基础功能,AndroidAutoSize 的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入 AndroidAutoSize,但是作为一个全面性的屏幕适配框架,在保证基础功能的简易性的同时,也必须保证复杂的需求也能在框架内被解决,从而达到一个小闭环,所以下面介绍的内容全是前人踩坑踩出来的一些必备功能,如果你没这个需求,或者觉得麻烦,可以按需查看或者跳过,下面的内容建议和 Demo 配合起来阅读,效果更佳


注意事项



  • 你在 AndroidManifest.xml 中怎么把设计图的 px 尺寸转换为 dp 尺寸,那在布局时,每个控件的大小也需要以同样的方式将设计图上标注的 px 尺寸转换为 dp 尺寸,千万不要在 AndroidManifest.xml 中填写的是 dp 尺寸,却在布局中继续填写设计图上标注的 px 尺寸




  • design_width_in_dpdesign_height_in_dp 虽然都需要填写,但是 AndroidAutoSize 只会将高度和宽度其中的一个作为基准进行适配,一方作为基准,另一方就会变为备用,默认以宽度为基准进行适配,可以通过 AutoSizeConfig#setBaseOnWidth(Boolean) 不停的切换,这意味着最后运行到设备上的布局效果,在高度和宽度中只有一方可以和设计图上一模一样,另外一方会和设计图出现偏差,为什么不像 AndroidAutoLayout 一样,高和宽都以设计图的效果等比例完美呈现呢?这也很简单,你无法保证所有设备的高宽比例都和你设计图上的高宽比例一致,特别是在现在全面屏全面推出的情况下,如果这里不这样做的话,当你的项目运行在与设计图高宽比例不一致的设备上时,布局会出现严重的变形,这个几率非常大,详情请看 这里


自动运行是如何做到的?


很多人有疑惑,为什么使用者只需要在 AndroidManifest.xml 中填写一下 meta-data 标签,其他什么都不做,AndroidAutoSize 就能自动运行,并在 App 启动时自动解析 AndroidManifest.xml 中填写的设计图尺寸,这里很多人不敢相信,问我真的只需要填写下设计图尺寸框架就可以正常运行吗?难道使用了什么 黑科技?


其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明一个 ContentProvider,在它的 onCreate 方法中启动框架即可,在 App 启动时,系统会在 App 的主进程中自动实例化你声明的这个 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作,get 到了吗?


这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的 ContentProvider,并不会在其他非主进程中实例化 ContentProvider,如果在当前进程中 ContentProvider 没有被实例化,那 ContentProvider#onCreate 就不会被调用,你的初始化代码在当前进程中也就不会执行,这时就需要在 Application#onCreate 中调用下 ContentProvider#query 执行一下查询操作,这时 ContentProvider 就会在当前进程中实例化 (每个进程中只会保证有一个实例),所以应用到框架中就是,如果你需要在多个进程中都进行屏幕适配,那就需要在 Application#onCreate 中调用 AutoSize#initCompatMultiProcess 方法


进阶使用


虽然 AndroidAutoSize 不需要其他过多的配置,只需要在 AndroidManifest.xml 中填写下设计图尺寸就能正常运行,但 AndroidAutoSize 还是为大家准备了很多可配置选项,尽最大可能满足大家日常开发中的所有扩展需求


所有的全局配置选项在 Demo 中都有介绍,每个 API 中也都有详细的注释,在这里就不过多介绍了


自定义 Activity


AndroidManifest.xml 中填写的设计图尺寸,是整个项目的全局设计图尺寸,但是如果某些 Activity 页面由于某些原因,设计师单独出图,这个页面的设计图尺寸和在 AndroidManifest.xml 中填写的设计图尺寸不一样该怎么办呢?不要急,AndroidAutoSize 已经为你考虑好了,让这个页面的 Activity 实现 CustomAdapt 接口即可实现你的需求,CustomAdapt 接口的第一个方法可以修改当前页面的设计图尺寸,第二个方法可以切换当前页面的适配基准,下面的注释都解释的很清楚

public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {

/**
* 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
*
*
@return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
*/

@Override
public boolean isBaseOnWidth() {
return false;
}

/**
* 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
*


* 返回设计图上的设计尺寸, 单位 dp
* {
@link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
* 如果 {
@link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
* 如果 {
@link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
* 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {
@link #getSizeInDp} 则返回 {@code 0}
*
*
@return 设计图上的设计尺寸, 单位 dp
*/

@Override
public float getSizeInDp() {
return 667;
}
}


如果某个 Activity 想放弃适配,让这个 Activity 实现 CancelAdapt 接口即可,比如修改 density 影响到了老项目中的某些 Activity 页面的布局效果,这时就可以让这个 Activity 实现 CancelAdapt 接口

public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {

}

自定义 Fragment


Fragment 的自定义方式和 Activity 是一样的,只不过在使用前需要先在 App 初始化时开启对 Fragment 的支持

AutoSizeConfig.getInstance().setCustomFragment(true);

实现 CustomAdapt

public class CustomAdaptFragment extends Fragment implements CustomAdapt {

@Override
public boolean isBaseOnWidth() {
return false;
}

@Override
public float getSizeInDp() {
return 667;
}
}

实现 CancelAdapt

public class CancelAdaptFragment extends Fragment implements CancelAdapt {

}

适配三方库页面


在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)


由于 AndroidAutoSize 要求需要自定义适配参数或取消适配的页面必须实现 CustomAdaptCancelAdapt,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 AndroidAutoSize 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!



  • 通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释


  • 通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释



需要注意的是 ExternalAdaptManager 的方法虽然可以添加任何类,但是只能支持 Activity、Fragment,并且 ExternalAdaptManager 是支持链式调用的,以便于持续添加多个页面


当然 ExternalAdaptManager 不仅可以对三方库的页面使用,也可以让自己项目中的 Activity、Fragment 不用实现 CustomAdaptCancelAdapt 即可达到自定义适配参数和取消适配的功能


副单位


前面已经介绍了 副单位 的应用场景,这里就直接介绍 副单位 如何使用,副单位Demodemo-subunits


基本使用


首先和 主单位 一样也需要先在 appAndroidManifest.xml 中填写上设计图尺寸,但和 主单位 不一样的是,当在使用 副单位design_width_in_dpdesign_height_in_dp 的单位不需要一定是 dp,可以直接填写设计图的 px 尺寸,在布局文件中每个控件的大小也可以直接填写设计图上标注的 px 尺寸,无需再将 px 转换为 dp,这是 副单位的 特性之一,可以帮助大家提高开发效率








由于 AndroidAutoSize 提供了 pt、in、mm 三种类型的 副单位 供使用者选择,所以在使用 副单位 时,还需要在 APP 初始化时,通过 UnitsManager#setSupportSubunits(Subunits) 方法选择一个你喜欢的副单位,然后在布局文件中使用这个副单位进行布局,三种类型的副单位,其实效果都是一样,大家按喜欢的名字选择即可


由于使用副单位是为了彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,所以在使用副单位时建议调用 UnitsManager#setSupportDP(false)UnitsManager#setSupportSP(false),关闭 AndroidAutoSizedpsp 的支持,AndroidAutoSize 为什么不在使用 副单位 时默认关闭对 dpsp 的支持?因为允许同时支持 主单位副单位 可以帮助使用者在 主单位副单位 之间切换时更好的过渡,这点在前面就已经提到过


UnitsManager 的详细使用方法,在 demo-subunits 中都有展示,注释也十分详细


自定义 ActivityFragment


在使用 副单位 时自定义 ActivityFragment 的方式是和 主单位 是一样的,这里就不再过多介绍了


适配三方库页面


如果你的项目在使用 副单位 并且关闭了对 主单位 (dp、sp) 的支持,这时 ExternalAdaptManager 对三方库的页面是不起作用的,只对自己项目中的页面起作用,除非三方库的页面也使用了副单位 (pt、in、mm) 进行布局


其实 副单位 之所以能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,就是因为三方库页面、三方库控件以及系统控件基本上使用的都是 dp、sp 进行布局,所以只要 AndroidAutoSize 关闭了对 dp、sp 的支持,转而使用 副单位 进行布局,就能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响


但这也同样意味着使用 副单位 就不能适配三方库的页面了,ExternalAdaptManager 也就对三方库的页面不起作用了


布局实时预览


在开发阶段布局时的实时预览是一个很重要的环节,很多情况下 Android Studio 提供的默认预览设备并不能完全展示我们的设计图,所以我们就需要自己创建模拟设备,dp、pt、in、mm 这四种单位的模拟设备创建方法请看 这里


总结


AndroidAutoSize 在经历了 240+ commit60+ issues6 个版本 的洗礼后,逐渐的稳定了下来,已经在上个星期发布了首个正式版,在这里要感谢将 AndroidAutoSize 接入到自己项目中的上千个使用者,感谢他们的信赖,AndroidAutoSize 创建的初衷就是为了让所有使用 今日头条屏幕适配方案 的使用者能有一个可以一起交流、沟通的聚集地,所以后面也会持续的收集并解决 今日头条屏幕适配方案的常见问题,让 今日头条屏幕适配方案 变得更加成熟、稳定


至此本系列的第三篇文章也就完结了,这也预示着这个系列连载的终结,这篇文章建议结合系列的第一篇文章 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案 一起看,这样可以对 今日头条屏幕适配方案 有一个更深入的理解,如果你能将整个系列的文章都全部认真看完,那你对 Android 屏幕适配领域的相关知识绝对会有一个飞速的提升!


当你的项目需要切换某个框架时,你会怎么去考察、分析、对比现有的开源方案,并有足够的理由去选择或优化一个最适合自己项目的方案呢?其实整个系列文章可以看作是我怎么去选择同类型开源方案的过程,你以后当遇到同样的选择也可以参照我的思维方式去处理,当然如果以后面试官问到你屏幕适配相关的问题,你能将我如何选择、分析、对比已有方案的过程以及文章中的核心知识点告诉给面试官,那肯定比你直接说一句我使用的是某某开源库有价值得多



作者:JessYan
链接:https://www.jianshu.com/p/4aa23d69d481
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

骚年你的屏幕适配方式该升级了!-SmallestWidth 限定符适配方案

前言 ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案 了解我的朋友一定知道,MVPArms 一直使用的是 鸿神 的 AndroidA...
继续阅读 »

前言


ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案


了解我的朋友一定知道,MVPArms 一直使用的是 鸿神AndroidAutoLayout 屏幕适配方案,得益于 AndroidAutoLayout 的便捷,所以我对屏幕适配领域研究的不是很多,AndroidAutoLayout 停止维护后,我也一直在找寻着替代方案,直到 今日头条屏幕适配方案 刷屏,后来又无意间看到了 smallestWidth 限定符屏幕适配方案,这才慢慢的将研究方向转向了屏幕适配领域


最近一个月才开始慢慢恶补 Android 屏幕适配的相关知识,对这两个方案也进行了更深入的研究,可以说从一个小白慢慢成长而来,所以我明白小白的痛,因此在上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 中,把 今日头条屏幕适配方案 讲得非常的细,尽量把每一个知识点都描述清晰,深怕小白漏掉每一个细节,这篇文章我也会延续上一篇文章的优良传统,将 smallestWidth 限定符屏幕适配方案 的每一个知识点都描述清晰


顺便说一句,感谢大家对 AndroidAutoSize 的支持,我只是在上一篇文章中提了一嘴我刚发布的屏幕适配框架 AndroidAutoSize,还没给出详细的介绍和原理剖析 (原计划在本系列的第三篇文章中发布),AndroidAutoSize 就被大家推上了 Github Trending,一个多星期就拿了 2k+ stars,随着关注度的增加,我在这段时间里也累坏了,issues 就没断过,不到半个月就提交了 200 多次 commit,但累并快乐着,在这里要再次感谢大家对 AndroidAutoSize 的认可




大家要注意了!这些观点其实针对的是所有以百分比缩放布局的库,而不只是今日头条屏幕适配方案,所以这些观点也同样适用于 smallestWidth 限定符屏幕适配方案,这点有很多人存在误解,所以一定要注意!





Android系统碎片化机型以及屏幕尺寸碎片化屏幕分辨率碎片化 有多严重大家可以通过 友盟指数 了解一下,有些时候在某些事情的决断标准上,并不能按照事情的对错来决断,大多数情况还是要分析成本,收益等多种因素,通过利弊来决断,每个人的利弊标准又都不一样,所以每个人的观点也都会有差别,但也都应该得到尊重,所以我只是说说自己的观点,也不否认任何人的观点


方案是死的人是活的,在某些大屏手机或平板电脑上,您也可以采用其他适配方案和百分比库结合使用,比如针对某个屏幕区间的设备单独出一套设计图以显示比小屏幕手机更多更精细的内容,来达到与百分比库互补的效果,没有一个方案可以说自己是完美的,但我们能清晰的认识到不同方案的优缺点,将它们的优点相结合,才能应付更复杂的开发需求,产出最好的产品


友情提示: 下面要介绍的 smallestWidth 限定符屏幕适配方案,原理也同样是按照百分比缩放布局,理论上也会存在上面所说的 大屏手机和小屏手机显示的内容相同 的问题,选择与否请仔细斟酌


简介 smallestWidth 限定符适配方案


这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样👇


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp

原理


其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备


如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-swdp 文件夹,则会去寻找与之 最小宽度 (smallestWidth) 相近的 values-swdp 文件夹,系统只会寻找小于或等于当前设备 最小宽度 (smallestWidth)values-swdp,这就是优于 宽高限定符屏幕适配方案 的容错率,并且也可以少生成很多 values-swdp 文件夹,减轻 App 的体积


什么是 smallestWidth


smallestWidth 翻译为中文的意思就是 最小宽度,那这个 最小宽度 是什么意思呢?


系统会根据当前设备屏幕的 最小宽度 来匹配 values-swdp,为什么不是根据 宽度 来匹配,而要加上 最小 这两个字呢?


这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度,这个 最小宽度 是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为 最小宽度,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为 最小宽度


如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据 values-wdp (去掉 sw 中的 s) 生成一套资源文件


如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据 屏幕方向限定符 生成一套资源文件咯,后缀加上 -land-port 即可,像这样,values-sw400dp-land (最小宽度 400 dp 横向)values-sw400dp-port (最小宽度 400 dp 纵向)


smallestWidth 的值是怎么算的


要先算出当前设备的 smallestWidth 值我们才能知道当前设备该匹配哪个 values-swdp 文件夹


ok,还是按照上一篇文章的叙述方式,现在来举栗说明,帮助大家更好理解


我们假设设备的屏幕信息是 1920 * 1080480 dpi


根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,1080 < 1920,明显 1080 px 就是我们要找的 最小宽度 的值,但 最小宽度 的单位是 dp,所以我们要把 px 转换为 dp


帮助大家再巩固下基础,下面的公式一定不能再忘了!


px / density = dpDPI / 160 = density,所以最终的公式是 px / (DPI / 160) = dp


所以我们得到的 最小宽度 的值是 360 dp (1080 / (480 / 160) = 360)


现在我们已经算出了当前设备的最小宽度是 360 dp,我们晓得系统会根据这个 最小宽度 帮助我们匹配到 values-sw360dp 文件夹下的 dimens.xml 文件,如果项目中没有 values-sw360dp 这个文件夹,系统才会去匹配相近的 values-swdp 文件夹


dimens.xml 文件是整个方案的核心所在,所以接下来我们再来看看 values-sw360dp 文件夹中的这个 dimens.xml 是根据什么原理生成的


dimens.xml 生成原理


因为我们在项目布局中引用的 dimens 的实际值,来源于根据当前设备屏幕的 最小宽度 所匹配的 values-swdp 文件夹中的 dimens.xml,所以搞清楚 dimens.xml 的生成原理,有助于我们理解 smallestWidth 限定符屏幕适配方案


说到 dimens.xml 的生成,就要涉及到两个因数,第一个因素是 最小宽度基准值,第二个因素就是您的项目需要适配哪些 最小宽度,通俗理解就是需要生成多少个 values-swdp 文件夹


第一个因素


最小宽度基准值 是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的 最小宽度基准值 定为 360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 360 份,方案会帮您在 dimens.xml 文件中生成 1360dimens 引用,比如 values-sw360dp 中的 dimens.xml 是长这样的

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<dimen name="dp_8">8dp</dimen>
<dimen name="dp_9">9dp</dimen>
<dimen name="dp_10">10dp</dimen>
...
<dimen name="dp_356">356dp</dimen>
<dimen name="dp_357">357dp</dimen>
<dimen name="dp_358">358dp</dimen>
<dimen name="dp_359">359dp</dimen>
<dimen name="dp_360">360dp</dimen>
</resources>



values-sw360dp 指的是当前设备屏幕的 最小宽度360dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 360dp),把屏幕宽度分为 360 份,刚好每份等于 1dp,所以每个引用都递增 1dp,值最大的 dimens 引用 dp_360 值也是 360dp,刚好覆盖屏幕宽度


下面再来看看将 最小宽度基准值 定为 360 时,values-sw400dp 中的 dimens.xml 长什么样

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.1111dp</dimen>
<dimen name="dp_2">2.2222dp</dimen>
<dimen name="dp_3">3.3333dp</dimen>
<dimen name="dp_4">4.4444dp</dimen>
<dimen name="dp_5">5.5556dp</dimen>
<dimen name="dp_6">6.6667dp</dimen>
<dimen name="dp_7">7.7778dp</dimen>
<dimen name="dp_8">8.8889dp</dimen>
<dimen name="dp_9">10.0000dp</dimen>
<dimen name="dp_10">11.1111dp</dimen>
...
<dimen name="dp_355">394.4444dp</dimen>
<dimen name="dp_356">395.5556dp</dimen>
<dimen name="dp_357">396.6667dp</dimen>
<dimen name="dp_358">397.7778dp</dimen>
<dimen name="dp_359">398.8889dp</dimen>
<dimen name="dp_360">400.0000dp</dimen>
</resources>



values-sw400dp 指的是当前设备屏幕的 最小宽度400dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 400dp),把屏幕宽度同样分为 360份,这时每份就等于 1.1111dp 了,每个引用都递增 1.1111dp,值最大的 dimens 引用 dp_360 同样刚好覆盖屏幕宽度,为 400dp


通过两个 dimens.xml 文件的比较,dimens.xml 的生成原理一目了然,方案会先确定 最小宽度基准值,然后将每个 values-swdp 中的 dimens.xml 文件都分配与 最小宽度基准值 相同的份数,再根据公式 屏幕最小宽度 / 份数 (最小宽度基准值) 求出每份占多少 dp,保证不管在哪个 values-swdp 中,份数 (最小宽度基准值) * 每份占的 dp 值 的结果都是刚好覆盖屏幕宽度,所以在 份数 不变的情况下,只需要根据屏幕的宽度在不同的设备上动态调整 每份占的 dp 值,就能完成适配


这样就能保证不管将项目运行到哪个设备上,只要当前设备能匹配到对应的 values-swdp 文件夹,那布局中的 dimens 引用就能根据当前屏幕的情况进行缩放,保证能完美适配,如果没有匹配到对应的 values-swdp 文件夹,也没关系,它会去寻找与之相近的 values-swdp 文件夹,虽然在这种情况下,布局中的 dimens 引用的值可能有些许误差,但是也能保证最大程度的完成适配


说到这里,那大家就应该就会明白我为什么会说 smallestWidth 限定符屏幕适配方案 的原理也同样是按百分比进行布局,如果在布局中,一个 View 的宽度引用 dp_100,那不管运行到哪个设备上,这个 View 的宽度都是当前设备屏幕总宽度的 360分之100,前提是项目提供有当前设备屏幕对应的 values-swdp,如果没有对应的 values-swdp,就会去寻找相近的 values-swdp,这时就会存在误差了,至于误差是大是小,这就要看您的第二个因数怎么分配了


其实 smallestWidth 限定符屏幕适配方案 的原理和 今日头条屏幕适配方案 挺像的,今日头条屏幕适配方案 是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),而 smallestWidth 限定符屏幕适配方案 同样是根据屏幕的宽度动态调整每个设备 每份占的 dp 值


第二个因素


第二个因数是需要适配哪些 最小宽度?比如您想适配的 最小宽度320dp360dp400dp411dp480dp,那方案就会为您的项目生成 values-sw320dpvalues-sw360dpvalues-sw400dpvalues-sw411dpvalues-sw480dp 这几个资源文件夹,像这样👇


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp

方案会为您需要适配的 最小宽度,在项目中生成一系列对应的 values-swdp,在前面也说了,如果某个设备没有为它提供对应的 values-swdp,那它就会去寻找相近的 values-swdp,但如果这个相近的 values-swdp 与期望的 values-swdp 差距太大,那适配效果也就会大打折扣


那是不是 values-swdp 文件夹生成的越多,覆盖越多市面上的设备,就越好呢?


也不是,因为每个 values-swdp 文件夹其实都会占用一定的 App 体积,values-swdp 文件夹越多,App 的体积也就会越大


所以一定要合理分配 values-swdp,以越少的 values-swdp 文件夹,覆盖越多的机型


验证方案可行性


原理讲完了,我们还是按照老规矩,来验证一下这个方案是否可行?


假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133)


在使用 smallestWidth 限定符屏幕适配方案 时,需要提供 最小宽度基准值 和需要适配哪些 最小宽度,我们就把 最小宽度基准值 设置为 375 (和 设计图 一致),这时方案就会为我们需要适配的 最小宽度 生成对应的 values-swdp 文件夹,文件夹中的 dimens.xml 文件是由从 1375 组成的 dimens 引用,把所有设备的屏幕宽度都分为 375 份,所以在布局文件中我们应该把这个 View 的高宽都引用 dp_50


下面就来验证下在使用 smallestWidth 限定符屏幕适配方案 的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


验证设备 1


设备 1 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI480


设备 1 的屏幕高度大于屏幕宽度,所以 设备 1最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 1最小宽度 的值为 360 dp (1080 / (480 / 160) = 360)


根据 设备 1最小宽度 应该匹配的是 values-sw360dp 这个文件夹,假设 values-sw360dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,360 / 375 = 0.96,所以每份占的 dp 值为 0.96dimens.xml 里面的内容是长下面这样的

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">0.96dp</dimen>
<dimen name="dp_2">1.92dp</dimen>
<dimen name="dp_3">2.88dp</dimen>
<dimen name="dp_4">3.84dp</dimen>
<dimen name="dp_5">4.8dp</dimen>
...
<dimen name="dp_50">48dp</dimen>
...
<dimen name="dp_371">356.16dp</dimen>
<dimen name="dp_372">357.12dp</dimen>
<dimen name="dp_373">358.08dp</dimen>
<dimen name="dp_374">359.04dp</dimen>
<dimen name="dp_375">360dp</dimen>
</resources>



可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw360dp 中定格在了 48 dp,所以这个 View设备 1 上的高宽都为 48 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 144 px (48 * (480 / 160) = 144)


144 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


某些设备的高宽是和 设备 1 相同的,但是 DPI 可能不同,而由于 smallestWidth 限定符屏幕适配方案 并没有像 今日头条屏幕适配方案 一样去自行修改 density,所以系统就会使用默认的公式 DPI / 160 求出 densitydensity 又会影响到 dppx 的换算,因此 DPI 的变化,是有可能会影响到 smallestWidth 限定符屏幕适配方案


所以我们再来试试在这种特殊情况下 smallestWidth 限定符屏幕适配方案 是否也能完成适配


验证设备 2


设备 2 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI420


设备 2 的屏幕高度大于屏幕宽度,所以 设备 2最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 2最小宽度 的值为 411.429 dp (1080 / (420 / 160) = 411.429)


根据 设备 2最小宽度 应该匹配的是 values-sw411dp 这个文件夹,假设 values-sw411dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,411 / 375 = 1.096,所以每份占的 dp 值为 1.096dimens.xml 里面的内容是长下面这样的👇

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.096dp</dimen>
<dimen name="dp_2">2.192dp</dimen>
<dimen name="dp_3">3.288dp</dimen>
<dimen name="dp_4">4.384dp</dimen>
<dimen name="dp_5">5.48dp</dimen>
...
<dimen name="dp_50">54.8dp</dimen>
...
<dimen name="dp_371">406.616dp</dimen>
<dimen name="dp_372">407.712dp</dimen>
<dimen name="dp_373">408.808dp</dimen>
<dimen name="dp_374">409.904dp</dimen>
<dimen name="dp_375">411dp</dimen>
</resources>



可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw411dp 中定格在了 54.8dp,所以这个 View设备 2 上的高宽都为 54.8 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 143.85 px (54.8 * (420 / 160) = 143.85)


143.85 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


虽然 View设备 2 上的高宽是 143.85 px,比 设备 1144 px 少了 0.15 px,但是误差非常小,整体的比例并没有发生太大的变化,是完全可以接受的


这个误差是怎么引起的呢,因为 设备 2最小宽度 的实际值是 411.429 dp,但是匹配的 values-sw411dp 舍去了小数点后面的位数 (切记!系统会去寻找小于或等于 411.429 dp 的 values-swdp,所以 values-sw412dp 这个文件夹,设备 2 是匹配不了的),所以才存在了一定的误差,因此上面介绍的第二个因数是非常重要的,这直接决定误差是大还是小


可以看到即使在高宽一样但 DPI 不一样的设备上,smallestWidth 限定符屏幕适配方案 也能完成等比例适配,证明这个方案是可行的,如果大家还心存疑虑,也可以再试试其他分辨率的设备,其实到最后得出的比例都是在 0.133 左右,唯一的变数就是第二个因数,如果您生成的 values-swdp 与设备实际的 最小宽度 差别不大,那误差也就在能接受的范围内,如果差别很大,那就直接 GG


优点



  1. 非常稳定,极低概率出现意外


  2. 不会有任何性能的损耗


  3. 适配范围可自由控制,不会影响其他三方库


  4. 在插件的配合下,学习成本低



缺点



  1. 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦


  2. 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂


  3. 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择


  4. 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积


  5. 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积


  6. 不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 最小宽度限定符适配方案,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配,请看 这里



使用中的问题


这时有人就会问了,设计师给的设计图只标注了 px,使用这个方案时,那不是还要先将 px 换算成 dp


其实也可以不用换算的,那这是什么骚操作呢?


很简单,你把设计图的 px 总宽度设置成 最小宽度基准值 就可以了,还是以前面验证可行性的例子


我们在前面验证可行性时把 最小宽度基准值 设置成了 375,为什么是 375 呢?因为设计图的总宽度为 375 dp,如果换算成 px,总宽度就是 750 px,我们这时把 最小宽度基准值 设置成 750,然后看看 values-sw360dp 中的 dimens.xml 长什么样👇<?xml version="1.0" encoding="UTF-8"?>

<resources>
<dimen name="px_1">0.48dp</dimen>
<dimen name="px_2">0.96dp</dimen>
<dimen name="px_3">1.44dp</dimen>
<dimen name="px_4">1.92dp</dimen>
<dimen name="px_5">2.4dp</dimen>
...
<dimen name="px_50">24dp</dimen>
...
<dimen name="px_100">48dp</dimen>
...
<dimen name="px_746">358.08dp</dimen>
<dimen name="px_747">358.56dp</dimen>
<dimen name="px_748">359.04dp</dimen>
<dimen name="px_749">359.52dp</dimen>
<dimen name="px_750">360dp</dimen>
</resources>



360 dp 被分成了 750 份,相比之前的 375 份,现在 每份占的 dp 值 正好减少了一半,还记得在验证可行性的例子中那个 View 的尺寸是多少吗?50dp * 50dp,如果设计图只标注 px,那这个 View 在设计图上的的尺寸应该是 100px * 100px,那我们直接根据设计图上标注的 px,想都不用想直接在布局中引用 px_100 就可以了,因为在 375 份时的 dp_50 刚好等于 750 份时的 px_100 (值都是 48 dp),所以这时的适配效果和之前验证可行性时的适配效果没有任何区别


看懂了吗?直接将 最小宽度基准值 和布局中的引用都以 px 作为单位就可以直接填写设计图上标注的 px


总结


关于文中所列出的优缺点,列出的缺点数量确实比列出的优点数量多,但 缺点 3缺点 4缺点 5 其实都可以归纳于 占用 App 体积 这一个缺点,因为他们都可以通过增加资源文件来解决问题,而 缺点 6 则是这个方案的特色,只能以宽度为基准进行适配,这个从这个方案的名字就能看出


请大家千万不要曲解文章的意思,不要只是单纯的对比优缺点的数量,缺点的数量大于优点的数量就一定是这个方案不行?没有一个方案是完美的,每个人的需求也都不一样,作为一篇科普类文章我只可能把这个方案描述得尽可能的全面


这个方案能给你带来什么,不能给你带来什么,我必须客观的描述清楚,这样才有助你做出决定,你应该注重的是在这些优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,而不只是单纯的去看数量,这样毫无意义,有些人就是觉得稳定性最重要,其他的都可以做出妥协,那其他缺点对于他来说都是无所谓的


好了,这个系列的第二篇文章讲完了,这篇文章也是按照上篇文章的优良传统,写的非常详细,哪怕是新手我相信也应该能看懂,为什么这么多人都不知道自己该选择什么样的方案,就是因为自己都没搞懂这些方案的原理,懂了原理过后才知道这些方案是否是自己想要的


接下来的第三篇文章会详细讲解两个方案的深入对比以及该如何选择,并剖析我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize 的原理,敬请期待


如果大家想使用 smallestWidth 限定符屏幕适配方案,可以参考 这篇文章,里面提供有自动生成资源文件的插件和 Demo,由于我并没有在项目中使用 smallestWidth 限定符屏幕适配方案,所以如果在文章中有遗漏的知识点请谅解以及补充,感谢!



作者:JessYan
链接:https://www.jianshu.com/p/2aded8bb6ede
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

骚年你的屏幕适配方式该升级了!-今日头条适配方案

前言 这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案 上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方...
继续阅读 »

前言


这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案


上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方案,并开源了相关源码


上个星期 拉丁吴 老师在 鸿神 的公众号上发布了一篇 文章,详细描述了市面上主流的几种屏幕适配方案,并发布了他的 smallestWidth 限定符适配方案和相关源码 (其实早就发布了),文章写的很好,建议大家去看看


其实大家最关注的不是市面上有多少种屏幕适配方案,而是自己的项目该选择哪种屏幕适配方案,可以看出两位老师最终选择的屏幕适配方案都是不同的


我下面就来分析分析,我作为一个才接触这两个屏幕适配方案的吃瓜群众,我是怎么来验证这两种屏幕适配方案是否可行,以及怎样根据它们的优缺点来选择一个最适合自己项目的屏幕适配方案


浅谈适配方案


拉丁吴 老师的文章中谈到了两个比较经典的屏幕适配方案,在我印象中十分深刻,我想大多数兄弟都用过,在我的开发生涯里也是有很长一段时间都在用这两种屏幕适配方案


第一种就是宽高限定符适配,什么是宽高限定符适配呢

├── src/main

│   ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

就是这种,在资源文件下生成不同分辨率的资源文件,然后在布局文件中引用对应的 dimens,大家一定还有印象


第二种就是 鸿神AndroidAutoLayout


这两种方案都已经逐渐退出了历史的舞台,为什么想必大家都知道,不知道的建议看看 拉丁吴 老师的文章,所以这两种方案我在文章中就不在阐述了,主要讲讲现在最主流的两种屏幕适配方案,今日头条适配方案smallestWidth 限定符适配方案


建议大家不清楚这两个方案的先看看这两篇文章,才清楚我在讲什么,后面我要讲解它们的原理,以及验证这两种方案是否真的可行,最后对他们进行深入对比,对于他们的一些缺点给予对应的解决方案,绝对干货


今日头条屏幕适配方案


原理


上面已经告知,不了解这两个方案的先看看上面的两篇文章,所以这里我就假设大家已经看了上面的文章或者之前就了解过这两个方案,所以在本文中我就不再阐述 DPIDensity 以及一些比较基础的知识点,上面的文章已经阐述的够清楚了


今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density


当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


density 的意思就是 1 dp 占当前设备多少像素


为什么要算出 density,这和屏幕适配有什么关系呢?


public static float applyDimension(int unit, float value,
DisplayMetrics metrics
)

{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}

大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px


所以我们常用的 pxdp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步


要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,同时进行适配呢


这就引出了一个现在比较棘手的问题,大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致


这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题


明白这个后,我再来说说 densitydensity 在每个设备上都是固定的,DPI / 160 = density屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度



  • 设备 1,屏幕宽度为 1080px480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp


  • 设备 2,屏幕宽度为 1440560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp



可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的


这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)


但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差


这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的


这时我们该怎么做呢?改变每个 Viewdp 值?不现实,在每个设备上都要通过代码动态计算 Viewdp 值,工作量太大


如果每个 Viewdp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp


屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度


在这个公式中我们要保证 屏幕的总 dp 宽度设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了


当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配


验证方案可行性


上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?


假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


验证设备 1


屏幕总宽度为 1080 px,根据今日头条的的公式求出 density1080 / 375 = 2.88 (density)


这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 2.88 = 144 px (根据公式 dp * density = px)


144 / 1080 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


某些设备总宽度为 1080 px,但是 DPI 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI 求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响


上面只能确定在所有屏幕总宽度为 1080 px 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备


验证设备 2


屏幕总宽度为 1440 px,根据今日头条的的公式求出 density1440 / 375 = 3.84 (density)


这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 3.84 = 192 px (根据公式 dp * density = px)


192 / 1440 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放


两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133


优点



  1. 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案


  2. 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0


  3. 可适配三方库的控件和系统的控件(不止是 ActivityFragmentDialogToast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益


  4. 不会有任何性能的损耗



缺点


暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的


只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的


这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样


当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


举个栗子


假设一个三方库的 View,作者在设计时,把它设计为 100dp * 100dp,设计图的最大宽度为 1000dp,这个 View 在设计图中的比例是 100 / 1000 = 0.1,意思是这个 View 的宽度在设计图中占整个宽度的 10%,如果我们要完成等比例适配,那这个三方库 View 在所有的设备上与屏幕的总宽度的比例,都必须保持在 10%


这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 1000dp,那这个三方库 View,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 1000dp,是 500dp 时,100 / 500 = 0.2,可以看到,比例发生了较大的变化,从 10% 上升为 20%,明显这个三方库 View 高于作者的预期,比之前更大了


这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了


解决方案


方案 1


调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配


这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变


但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的


但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题


方案 2


第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案


使用中的问题


有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 px 为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 px 值,省掉了将 px 换算为 dp 的时间 (大部分公司的设计图都只标注 px 值),而且照样能完美适配


但是我建议大家千万不要这样做,还是老老实实的以 dp 为单位填写 dp 值,为什么呢?


直接填写 px 虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?


第一个坑


这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,layout 文件里曾经填写的 px 值都会作为 dp


比如你的设计图实际宽度为 1080px,你不换算为 360dp (1080 / 3 = 360),却直接将 1080px 作为这个方案的设计图尺寸,那你在 layout 文件中,填写的也都是设计图上标注的 px 值,但是单位却是 dp


一个在设计图上 300px * 300pxView,你可以直接在 layout 文件中填写为 300dp,而由于这个方案可以动态改变 density 的原因还是可以做到等比例适配,非常爽!


但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,density 是不可变的!


举个栗子


使用这个方案时,在屏幕宽度为 1080px 的设备上,将设计图宽度直接填写为 1080,根据今日头条公式


当前设备屏幕总宽度 / 设计图总宽度 = density


这时得出 density 为 1 (1080 / 1080 = 1),所以你在 layout 文件中你填写的 300dp 最后转换为 px 也是 300px (300dp * 1 = 300px 根据公式 dp * density = px)


在这个方案的帮助下非常完美,和设计图一模一样完成了适配


但当你不使用这个方案时,density 的换算公式就变为官方的 DPI / 160 = density

在这个屏幕宽度为 1080px480dpi 的设备上,density 就固定为 3 (480 / 160 = 3)


这时再来看看你之前在 layout 文件中填写的 dp,换算成 px900 px (300dp * 3 = 900px 根据公式 dp * density = px)


原本在在设计图上为 300pxView,这时却达到了惊人的 900px,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 layout 文件都改一遍,要不继续使用这个方案


第二个坑


第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


你如果直接填写以 px 为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 Toast 就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 View,也是一样的,会小很多


因为你以 px 为单位填写设计图尺寸,人家却用的 dp,差距能不大吗,你如果老老实实用 dp,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样


总结


可以看到我讲的非常详细,可以说比今日头条官方以及任何博客写的都清楚,从原理到优缺点再到解决方案一应俱全,因为篇幅有限,如果我还想把 smallestWidth 限定符适配方案写的这么详细,那估计这篇文章得有一万字了


所以我把这次的屏幕适配文章归位一个系列,一共分为三篇,第一篇详细的讲 今日头条屏幕适配方案,第二篇详细的讲 smallestWidth 限定符适配方案,第三篇详细讲两个方案的深入对比以及如何选择,并发布我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize


今日头条屏幕适配方案 官方公布的核心源码只有 30 行不到,但我这个框架的源码有 1500 行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了









只要这一步填写了设计图的高宽以 dp 为单位,你什么都不做,框架就开始适配了


大家可以提前看看我是怎么封装和优化的,我后面的第三篇文章会给出这个框架的原理分析,敬请期待




关于大家的评论以及关注的问题,我在这里统一回复一下:


感谢,大家的关注和回复,我介绍这个今日头条的屏幕适配方案并不是说他有多么完美,只是他确实有效而且能帮我们减少很多开发成本


对于很多人说的 DPI 的存在,不就是为了让大屏能显示更多的内容,如果一个大屏手机和小屏手机,显示的内容都相同,那用户买大屏手机又有什么意义呢,我觉得大家对 DPI 的理解是对的,这个观点我也是认同的,Google 设计 DPI 时可能也是这么想的,但是有一点大家没考虑到,Android 的碎片化太严重了


为什么 Android 诞生这么多年,Android 的百分比库层出不穷,按理说他们都违背了上面说的这个理念,但为什么还有这么多人去研究百分比库通过各种方式去实现百分比布局 (谷歌官方也曾出过百分比库)?为什么?很简单,因为需求啊!为什么需求,因为开发成本低啊!为什么今日头条的这个屏幕适配方案现在能这么火,因为他的开发成本是目前所有屏幕适配方案中最低的啊!


DPI 的意义谁又不懂呢?难道就你懂,今日头条这种大公司的程序员不懂这个道理吗?今日头条这么大的公司难道不想把每个机型每个版本的设备都适配完美,让自己的 App 体验更好,哪怕付出更大的成本,有些时候想象是美好的,但是这个投入的成本,谁又能承担呢,连今日头条这么大的公司,这么雄厚的资本都没选择投入更大的成本,对每个机型进行更精细化的适配,难道市面上的中小型公司又有这个能力投入这么大的成本吗?


鱼和熊掌不可兼得,DPI 的意义在 Google 的设计理念中是完全正确的,但不是所有公司都能承受这个成本,想必今日头条的程序员,也是因为今日头条 App 的用户量足够多,机型分布足够广,也是被屏幕适配这个问题折磨的不要不要的,才想出这么个不这么完美但是却很有效的方案



作者:JessYan
链接:https://www.jianshu.com/p/55e0fca23b4f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android屏幕适配方案-AndroidAutoLayout

AndroidAutoLayoutAndroid屏幕适配方案,直接填写设计图上的像素尺寸即可完成适配。引入Android Studio将autolayout引入dependencies { compile project(':autolayout') ...
继续阅读 »

AndroidAutoLayout

Android屏幕适配方案,直接填写设计图上的像素尺寸即可完成适配。

引入

  • Android Studio

autolayout引入

dependencies {
compile project(':autolayout')
}

也可以直接

dependencies {
compile 'com.zhy:autolayout:1.4.5'
}
  • Eclipse

建议使用As,方便版本更新。实在不行,只有复制粘贴源码了。

用法

第一步:

在你的项目的AndroidManifest中注明你的设计稿的尺寸。

<meta-data android:name="design_width" android:value="768">
meta-data>
<meta-data android:name="design_height" android:value="1280">
meta-data>

第二步:

让你的Activity继承自AutoLayoutActivity.

非常简单的两个步骤,你就可以开始愉快的编写布局了,详细可以参考sample。

其他用法

如果你不希望继承AutoLayoutActivity,可以在编写布局文件时,将

  • LinearLayout -> AutoLinearLayout
  • RelativeLayout -> AutoRelativeLayout
  • FrameLayout -> AutoFrameLayout

这样也可以完成适配。

目前支持属性

  • layout_width
  • layout_height
  • layout_margin(left,top,right,bottom)
  • pading(left,top,right,bottom)
  • textSize
  • maxWidth, minWidth, maxHeight, minHeight

配置

默认使用的高度是设备的可用高度,也就是不包括状态栏和底部的操作栏的,如果你希望拿设备的物理高度进行百分比化:

可以在Application的onCreate方法中进行设置:

public class UseDeviceSizeApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
AutoLayoutConifg.getInstance().useDeviceSize();
}
}

扩展

对于其他继承系统的FrameLayout、LinearLayout、RelativeLayout的控件,比如CardView,如果希望再其内部直接支持"px"百分比化,可以自己扩展,扩展方式为下面的代码,也可参考issue#21

package com.zhy.sample.view;

import android.content.Context;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;

import com.zhy.autolayout.AutoFrameLayout;
import com.zhy.autolayout.utils.AutoLayoutHelper;

/**
* Created by zhy on 15/12/8.
*/

public class AutoCardView extends CardView
{
private final AutoLayoutHelper mHelper = new AutoLayoutHelper(this);

public AutoCardView(Context context)
{
super(context);
}

public AutoCardView(Context context, AttributeSet attrs)
{
super(context, attrs);
}

public AutoCardView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}

@Override
public AutoFrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new AutoFrameLayout.LayoutParams(getContext(), attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isInEditMode())
{
mHelper.adjustChildren();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


}

注意事项

ListView、RecyclerView类的Item的适配

sample中包含ListView、RecyclerView例子,具体查看sample

  • 对于ListView

对于ListView这类控件的item,默认根局部写“px”进行适配是无效的,因为外层非AutoXXXLayout,而是ListView。但是,不用怕,一行代码就可以支持了:

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder = null;
if (convertView == null)
{
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
convertView.setTag(holder);
//对于listview,注意添加这一行,即可在item上使用高度
AutoUtils.autoSize(convertView);
} else
{
holder = (ViewHolder) convertView.getTag();
}

return convertView;
}

注意 AutoUtils.autoSize(convertView);这行代码的位置即可。demo中也有相关实例。

  • 对于RecyclerView
public ViewHolder(View itemView)
{
super(itemView);
AutoUtils.autoSize(itemView);
}

//...
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View convertView = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item, parent, false);
return new ViewHolder(convertView);
}

一定要记得LayoutInflater.from(mContext).inflate使用三个参数的方法!

指定设置的值参考宽度或者高度

由于该库的特点,布局文件中宽高上的1px是不相等的,于是如果需要宽高保持一致的情况,布局中使用属性:

app:layout_auto_basewidth="height",代表height上编写的像素值参考宽度。

app:layout_auto_baseheight="width",代表width上编写的像素值参考高度。

如果需要指定多个值参考宽度即:

app:layout_auto_basewidth="height|padding"

用|隔开,类似gravity的用法,取值为:

  • width,height
  • margin,marginLeft,marginTop,marginRight,marginBottom
  • padding,paddingLeft,paddingTop,paddingRight,paddingBottom
  • textSize.

TextView的高度问题

设计稿一般只会标识一个字体的大小,比如你设置textSize="20px",实际上TextView所占据的高度肯定大于20px,字的上下都会有一定的间隙,所以一定要灵活去写字体的高度,比如对于text上下的margin可以选择尽可能小一点。或者选择别的约束条件去定位(比如上例,选择了marginBottom)

常见问题

###(1)导入后出现org/gradle/api/publication/maven/internal/DefaultMavenFactory

最简单的方式,通过compile 'com.zhy:autolayout:x.x.x'进行依赖使用,

###(2)RadioGroup,Toolbar等控件中的子View无法完成适配

这个其实上文已经提到过了,需要自己扩展。不过这个很多使用者贡献了他们的扩展类可以直接使用, 参考autolayout-widget, 如果没有发现你需要的容器类,那么你就真的需要自行扩展了,当然如果你完成了扩展,可以给我发个PR,或者让我知道,我可以加入到 autolayout-widget中方便他人,ps:需要用到哪个copy就好了,不要直接引用autolayout-widget,因为其引用了大量的库,可能很多 库你是用不到的。

###(3)java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

这个问题是因为默认AutoLayoutActivity会继承自AppCompatActivity,所以默认需要设置 Theme.AppCompat的theme;

如果你使用的依旧是FragmentActivity等,且不考虑使用AppCompatActivity, 你可以选择自己编写一个MyAutoLayoutActivity extends 目前你使用的Activity基类,例如 MyAutoLayoutActivity extends FragmentActivity,然后将该库中AutoLayoutActivity中的逻辑 拷贝进去即可,以后你就继承你的MyAutoLayoutActivity就好了。

ps:还是建议尽快更新SDK版本使用AppCompatActivity.


代码下载:AndroidAutoLayout-master.zip

原文链接:https://github.com/hongyangAndroid/AndroidAutoLayout

收起阅读 »

Android 优雅的为RecyclerView添加HeaderView和FooterView

1、概述RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或...
继续阅读 »

1、概述

RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。

如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或者footerView,当你敲出.addHeaderView,你会发现并没有添加头部或者底部View的相关API。

那么本文主要的内容很明显了,完成以下工作:

如何为RecyclerView添加HeaderView(支持多个)
如何为RecyclerView添加FooterView(支持多个)
如何让HeaderView或者FooterView适配各种LayoutManager

那我只能考虑自己换种思路来解决这个问题,并且提供尽可能多的功能了~

2 、思路

(1)原理

对于添加headerView或者footerView的思路

其实HeaderView实际上也是Item的一种,只不过显示在顶部的位置,那么我们完全可以通过为其设置ItemType来完成。

有了思路以后,我们心里就妥了,最起码我们的内心中想想是可以实现的,接下来考虑一些细节。

(2)一些细节

假设我们现在已经完成了RecyclerView的编写,忽然有个需求,需要在列表上加个HeaderView,此时我们该怎么办呢?

打开我们的Adapter,然后按照我们上述的原理,添加特殊的ViewType,然后修改代码完成。

这是比较常规的做法了,但是有个问题是,如果需要添加viewType,那么可能我们的Adapter需要修改的幅度就比较大了,比如getItemType、getItemCount、onBindViewHolder、onCreateViewHolder等,几乎所有的方法都要进行改变。

这样来看,出错率是非常高的。

况且一个项目中可能多个RecyclerView都需要在其列表中添加headerView。

这么来看,直接改Adapter的代码是非常不划算的,最好能够设计一个类,可以无缝的为原有的Adapter添加headerView和footerView。

本文的思路是通过类似装饰者模式,去设计一个类,增强原有Adapter的功能,使其支持addHeaderView和addFooterView。这样我们就可以不去改动我们之前已经完成的代码,灵活的去扩展功能了。

我希望的用法是这样的:

mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
t1.setText("Header 1");
TextView t2 = new TextView(this);
mHeaderAndFooterWrapper.addHeaderView(t2);


在不改变原有的Adapter基础上去增强其功能。

3、初步的实现

(1) 基本代码



首先我们编写一个Adapter的子类,我们叫做HeaderAndFooterWrapper,然后再其内部添加了addHeaderView,addFooterView等一些辅助方法。

这里你可以看到,对于多个HeaderView,讲道理我们首先想到的应该是使用List,而这里我们为什么要使用SparseArrayCompat呢?

public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;

private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFootViews = new SparseArrayCompat<>();

private RecyclerView.Adapter mInnerAdapter;

public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}

private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}

private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}


public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}

public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}

public int getHeadersCount()
{
return mHeaderViews.size();
}

public int getFootersCount()
{
return mFootViews.size();
}
}


SparseArrayCompat有什么特点呢?它类似于Map,只不过在某些情况下比Map的性能要好,并且只能存储key为int的情况。

并且可以看到我们对每个HeaderView,都有一个特定的key与其对应,第一个headerView对应的是BASE_ITEM_TYPE_HEADER,第二个对应的是BASE_ITEM_TYPE_HEADER+1;

为什么要这么做呢?

这两个问题都需要到复写onCreateViewHolder的时候来说明。

(2)复写相关方法

public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{

ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;

} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}

@Override
public int getItemViewType(int position)
{
if (isHeaderViewPos(position))
{
return mHeaderViews.keyAt(position);
} else if (isFooterViewPos(position))
{
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}

private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}


@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}

@Override
public int getItemCount()
{
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
}



getItemViewType
由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。

getItemCount很好理解;

对于getItemType,可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们addHeaderView时的key,footerView是一样的处理方式,这里可以看出我们为每一个headerView创建了一个itemType。

onCreateViewHolder
可以看到,我们分别判断viewType,如果是headview或者是footerview,我们则为其单独创建ViewHolder,这里的ViewHolder是我之前写的一个通用的库里面的类,文末有链接。当然,你也可以自己写一个ViewHolder的实现类,只需要将对应的headerView作为itemView传入ViewHolder的构造即可。

这个方法中,我们就可以解答之前的问题了:

为什么我要用SparseArrayCompat而不是List?
为什么我要让每个headerView对应一个itemType,而不是固定的一个?
对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。

但是问题来了,该方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。

所以,我利用SparseArrayCompat,将其key作为itemType,value为我们的headerView,在onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。而且我们的取值是从100000开始的,正常的itemType是从0开始取值的,所以正常情况下,是不可能发生冲突的。

需要说明的是,这里的意思并非是一定不能用List,通过一些特殊的处理,List也能达到上述我描述的效果。

onBindViewHolder
onBindViewHolder比较简单,发现是HeaderView或者FooterView直接return即可,因为对于头部和底部我们仅仅做展示即可,对于事件应该是在addHeaderView等方法前设置。

这样就初步完成了我们的装饰类,我们分别添加两个headerView和footerView:

大家都知道RecyclerView比较强大,可以设置不同的LayoutManager,那么我们换成GridLayoutMananger再看看效果。



好像发现了不对的地方,我们的headerView果真被当成普通的Item处理了,不过由于我们的编写方式,出现上述情况是可以理解的。

那么我们该如何处理呢?让每个headerView独立的占据一行?

4、进一步的完善

好在RecyclerView里面为我们提供了一些方法。

(1)针对GridLayoutManager

在我们的HeaderAndFooterWrapper中复写onAttachedToRecyclerView方法,如下:

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
innerAdapter.onAttachedToRecyclerView(recyclerView);

RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager)
{
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
{
@Override
public int getSpanSize(int position)
{
int viewType = getItemViewType(position);
if (mHeaderViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
} else if (mFootViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
}
if (oldLookup != null)
return oldLookup.getSpanSize(position);
return 1;
}
});
gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
}
}



当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();


(2)对于StaggeredGridLayoutManager

在刚才的代码中我们好像没有发现StaggeredGridLayoutManager的身影,StaggeredGridLayoutManager并没有setSpanSizeLookup这样的方法,那么该如何处理呢?

依然不复杂,重写onViewAttachedToWindow方法,如下:

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderViewPos(position) || isFooterViewPos(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams)
{

StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;

p.setFullSpan(true);
}
}
}



这样就完成了对StaggeredGridLayoutManager的处理,效果图就不贴了。

到此,我们就完成了整个HeaderAndFooterWrapper的编写,可以在不改变原Adapter代码的情况下,为其添加一个或者多个headerView或者footerView,以及完成了如何让HeaderView或者FooterView适配各种LayoutManager。

源码地址:https://github.com/hongyangAndroid/baseAdapter
————————————————
版权声明:本文为CSDN博主「鸿洋_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/lmj623565791/article/details/51854533

收起阅读 »

小谈 Kotlin 的空处理

近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。 ...
继续阅读 »

近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。



一、上手的确容易



先扯一扯 Kotlin 学习本身。

之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇是否有这个必要。现在因为工作关系直接上手 Kotlin,感受是 真香(上手的确容易)

首先在代码阅读层面,对于有 Java 基础的程序员来说阅读 Kotlin 代码基本无障碍,除去一些操作符、一些顺序上的变化,整体上可以直接阅读。

其次在代码编写层面,仅需要改变一些编码习惯。主要是:语句不要写分号、变量需要用 var val 声明、类型写在变量之后、实例化一个对象时不用 "new" …… 习惯层面的改变只需要多写代码,自然而然就适应了。

最后在学习方式层面,由于 Kotlin 最终都会被编译成字节码跑在 JVM
上,所以初入手时完全可以用 Java 作为对比。比如你可能不知道
Kotlin
companion object 是什么意思,但你知道既然
Kotlin
最终会转成 jvm 可以跑的字节码,那 Java 里必然可以找到与之对应的东西。

Android Studio 也提供了很方便的工具。选择菜单 Tools -> Kotlin -> Show
Kotlin Bytecode
即可看到 Kotlin 编译成的字节码,点击窗口上方的 "Decompile" 即可看到这份字节码对应的 Java 代码。
—— 这个工具特别重要,假如一段
Kotlin
代码让你看得云里雾里,看一下它对应的 Java 代码你就能知道它的含义。


当然这里仅仅是说上手或入门(仅入门的话可以忽略诸如协程等高级特性),真正熟练应用乃至完全掌握肯定需要一定时间。



二、针对 NPE 的强规则



有些文章说 Kotlin 帮开发者解决了 NPENullPointerException),这个说法是不对的。在我看来,Kotlin 没有帮开发者解决了 NPE Kotlin: 臣妾真的做不到啊),而是通过在语言层面增加各种强规则,强制开发者去自己处理可能的空指针问题,达到尽量减少(只能减少而无法完全避免)出现 NPE 的目的。

那么 Kotlin 具体是怎么做的呢?别着急,我们可以先回顾一下在 Java 中我们是怎么处理空指针问题的。

Java 中对于空指针的处理总体来说可以分为防御式编程契约式编程两种方案。

防御式编程大家应该不陌生,核心思想是不信任任何外部输入 —— 不管是真实的用户输入还是其他模块传入的实参,具体点就是各种判空。创建一个方法需要判空,创建一个逻辑块需要判空,甚至自己的代码内部也需要判空(防止对象的回收之类的)。示例如下:

   public void showToast(Activity activity) {

       if (activity == null) {

           return;

       }

       

       ......

   }

另一种是契约式编程,各个模块之间约定好一种规则,大家按照规则来办事,出了问题找没有遵守规则的人负责,这样可以避免大量的判空逻辑。Android 提供了相关的注解以及最基础的检查来协助开发者,示例如下:

   public void showToast(@NonNull Activity activity) {

       ......

   }

在示例中我们给 Activity 增加了 @NonNull 的注解,就是向所有调用这个方法的人声明了一个约定,调用方应该保证传入的 activity 非空。当然聪明的你应该知道,这是一个很弱的限制,调用方没注意或者不理会这个注解的话,程序就依然还有 NPE 导致的 crash 的风险。

回过头来,对于 Kotlin,我觉得就是一种把契约式编程和防御式编程相结合且提升到语言层面的处理方式。(听起来似乎比 Java 中各种判空或注解更麻烦?继续看下去,你会发现的确是更麻烦……

Kotlin 中,有以下几方面约束:

1.  
在声明阶段,变量需要决定自己是否可为空,比如
var time: Long? 可接受 null,而 var time: Long 则不能接受 null

2.  
在变量传递阶段,必须保持可空性一致,比如形参声明是不为空的,那么实参必须本身是非空或者转为非空才能正常传递。示例如下:

   fun main() {

       ......

       //  test(isOpen) 直接这样调用,编译不通过


       // 可以是在空检查之内传递,证明自己非空


       isOpen?.apply { 

           test(this)

       }

       // 也可以是强制转成非空类型


       test(isOpen!!)

   }





   private fun test(open: Boolean) {

       ......

   }

3.  
在使用阶段,需要严格判空:

   var time: Long? = 1000

     //尽管你才赋值了非空的值,但在使用过程中,你无法这样:


     //time.toInt()

     //必须判空


     time?.toInt()

总的来说 Kotlin 为了解决 NPE 做了大量语言层级的强限制,的确可以做到减少 NPE 的发生。但这种既契约式(判空)又防御式(声明空与非空)的方案会让开发者做更多的工作,会更麻烦一点。

当然,Kotlin 为了减少麻烦,用 "?" 简化了判空逻辑 —— "?" 的实质还是判空,我们可以通过工具查看 time?.toInt() Java 等价代码是:

     if (time != null) {

        int var10000 = (int)time;

     }

这种简化在数据层级很深需要写大量判空语句时会特别方便,这也是为什么虽然逻辑上 Kotlin 让开发者做了更多工作,但写代码过程中却并没有感觉到更麻烦。



三、强规则之下的 NPE 问题



Kotlin 这么严密的防御之下,NPE 问题是否已经被终结了呢?答案当然是否定的。 在实践过程中我们发现主要有以下几种容易导致 NPE 的场景:

1. data class(含义对应
Java
中的 model)声明了非空

例如从后端拿 json 数据的场景,后端的哪个字段可能会传空是客户端无法控制的,这种情况下我们的预期必须是每个字段都可能为空,这样转成 json object 时才不会有问题:

data class User(

       var id: Long?,

       var gender: Long?,

       var avatar: String?)

假如有一个字段忘了加上"?",后端没传该值就会抛出空指针异常。

2. 过分依赖 Kotlin 的空值检查

private lateinit var mUser: User



...



private fun initView() {

   mUser = intent.getParcelableExtra<User>("key_user")

}

Kotlin 的体系中久了会过分依赖于
Android Studio
的空值检查,在代码提示中
Intent
getParcelableExtra 方法返回的是非空,因此这里你直接用方法结果赋值不会有任何警告。但点击进 getParcelableExtra 方法内部你会发现它的实现是这样的:

   public <T extends Parcelable> T getParcelableExtra(String name) {

       return mExtras == null ? null : mExtras.<T>getParcelable(name);

   }

内部的其他代码不展开了,总之它是可能会返回 null 的,直接赋值显然会有问题。

我理解这是 Kotlin 编译工具对 Java 代码检查的不足之处,它无法准确判断 Java 方法是否会返回空就选择无条件信任,即便方法本身可能还声明了 @Nullable

3. 变量或形参声明为非空

这点与第一、第二点都很类似,主要是使用过程中一定要进一步思考传递过来的值是否真的非空。

有人可能会说,那我全部都声明为可空类型不就得了么 —— 这样做会让你在使用该变量的所有地方都需要判空,Kotlin 本身的便利性就荡然无存了。

我的观点是不要因噎废食,使用时多注意点就可以避免大部分问题。

4. !! 强行转为非空

当将可空类型赋值给非空类型时,需要有对空类型的判断,确保非空才能赋值(Kotlin 的约束)。

我们使用!! 可以很方便得将可空转为非空但可空变量值为 null,则会 crash

因此使用上建议在确保非空时才用 !!:

   param!!

否则还是尽量放在判空代码块里:

   param?.let {

      doSomething(it)

   }


四、实践中碰到的问题



Java 的空处理转到 Kotlin 的空处理,我们可能会下意识去寻找对标 Java 的判空写法:

   if (n != null) {

      //非空如何 

   } else {

      //为空又如何


   }

Kotlin 中类似的写法的确有,那就是结合高阶函数 letapplyrun …… 来处理判空,比如上述 Java 代码就可以写成:

   n?.let {

      //非空如何


   } ?: let {

      //为空又如何


   }

但这里有几个小坑。

1. 两个代码块不是互斥关系

假如是 Java 的写法,那么不管 n 的值怎样,两个代码块都是互斥的,也就是非黑即白。但 Kotlin 的这种写法不是(不确定这种写法是否是最佳实践,假如有更好的方案可以留言指出)。

?: 这个操作符可以理解为 if (a != null) a else b,也就是它之前的值非空返回之前的值,否则返回之后的值。

假如用的是 let, 注意看它的返回值是指定 return 或函数里最后一行,那么碰到以下情况:

   val n = 1

   var a = 0

   n?.let {

      a++

      ...

      null  //最后一行为 null


   } ?: let {

      a++

   }

你会很神奇地发现 a 的值是 2,也就是既执行了前一个代码块,也执行了后一个代码块

上面这种写法你可能不以为然,因为很明显地提醒了诸位需要注意最后一行,但假如是之前没注意这个细节或者是下面这种写法呢?

   n?.let {

      ...

      anMap.put(key, value) // anMap 是一个 HashMap


   } ?: let {

      ...

   }

应该很少人会注意到 Map put 方法是有返回值的,且可能会返回 null。那么这种情况下很容易踩坑。

2. 两个代码块的对象不同

let 为例,在 let 代码块里可以用 it 指代该对象(其他高阶函数可能用
this
,类似的),那么我们在写如下代码时可能会顺手这样写:

   activity {

      n?.let {

         it.hashCode() // it n


      } ?: let {

         it.hashCode() // it activity


      }  

   }

结果自然会发现值不一样。前一个代码块 it 指代的是 n,而后一个代码块里 it 指代的是整个代码块指向的
this

原因是 ?: let 之间是没有 . 的,也就是说后一个代码块调用 let 的对象并不是被判空的对象,而是 this。(不过这种场景会出错的概率不大,因为在后一个代码块里很多对象 n 的方法用不了,就会注意到问题了)



后记



总的来说切换到 Kotlin 还是比预期顺利和舒服,写惯了
Kotlin
后再回去写 Java 反倒有点不习惯。今天先写这点,后面有其他需要总结的再分享。


收起阅读 »

Activity启动流程

Activity启动流程很多文章都已经说过了,这里说一下自己的理解。Activity启动流程分两种,一种是启动正在运行的app的Activity,即启动子Activity。如无特殊声明默认和启动该activity的activity处于同一进程。如果有声明在一个...
继续阅读 »

Activity启动流程很多文章都已经说过了,这里说一下自己的理解。

Activity启动流程分两种,一种是启动正在运行的appActivity,即启动子Activity如无特殊声明默认和启动该activityactivity处于同一进程。如果有声明在一个新的进程中,则处于两个进程。另一种是打开新的app,即为Launcher启动新的Activity后边启动Activity的流程是一样的,区别是前边判断进程是否存在的那部分。

Activity启动的前提是已经开机,各项进程和AMS等服务已经初始化完成,在这里也提一下那些内容。

Activity启动之前

init进程init是所有linux程序的起点,是Zygote的父进程。解析init.rc孵化出Zygote进程。

Zygote进程Zygote是所有Java进程的父进程,所有的App进程都是由Zygote进程fork生成的。

SystemServer进程System
Server
Zygote孵化的第一个进程。SystemServer负责启动和管理整个Java framework,包含AMSPMS等服务。

LauncherZygote进程孵化的第一个App进程是Launcher

1.init进程是什么?

Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。

init
进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。

2.Zygote进程是什么?

所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socketOS层会采用socket进行IPC通信。

3.为什么是Zygote来孵化进程,而不是新建进程呢?

每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。

Activity启动阶段

涉及到的概念

进程Android系统为每个APP分配至少一个进程

IPC:跨进程通信,Android中采用Binder机制。

涉及到的类

ActivityStackActivityAMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。

ActivitySupervisor:管理 activity 任务栈

ActivityThreadActivityThread
运行在UI线程(主线程),App的真正入口。

ApplicationThread:用来实现AMSActivityThread之间的交互。

ApplicationThreadProxyApplicationThread
在服务端的代理。AMS就是通过该代理与ActivityThread进行通信的。

IActivityManager:继承与IInterface接口,抽象出跨进程通信需要实现的功能

AMN:运行在server端(SystemServer进程)。实现了Binder类,具体功能由子类AMS实现。

AMSAMN的子类,负责管理四大组件和进程,包括生命周期和状态切换。AMS因为要和ui交互,所以极其复杂,涉及window

AMPAMSclient端代理(app进程)。了解Binder知识可以比较容易理解server端的stubclient端的proxyAMPAMS通过Binder通信。

Instrumentation:仪表盘,负责调用ActivityApplication生命周期。测试用到这个类比较多。

流程图

这个图来源自网上,之前也看过很多类似讲流程的文章,但是大都是片段的。这个图是目前看到的最全的,自己去画一下也应该不会比这个全了,所以在这里直接引用一下,可以去浏览器上放大看。


涉及到的进程

·      
Launcher所在的进程

·      
AMS所在的SystemServer进程

·      
要启动的Activity所在的app进程

如果是启动根Activity,就涉及上述三个进程。

如果是启动子Activity,那么就只涉及AMS进程和app所在进程。

具体流程

1. LauncherLauncher通知AMS要启动activity

·      
startActivitySafely->startActivity->Instrumentation.execStartActivity()(AMP.startActivity)->AMS.startActivity

2. AMS:PMSresoveIntent验证要启动activity是否匹配。如果匹配,通过ApplicationThread发消息给Launcher所在的主线程,暂停当前Activity(Launcher)

3. 暂停完,在该activity还不可见时,通知AMS,根据要启动的Activity配置ActivityStack。然后判断要启动的Activity进程是否存在?

·      
存在:发送消息LAUNCH_ACTIVITY给需要启动的Activity主线程,执行handleLaunchActivity

·      
不存在:通过socketzygote请求创建进程。进程启动后,ActivityThread.attach

4. 判断Application是否存在,若不存在,通过LoadApk.makeApplication创建一个。在主线程中通过thread.attach方法来关联ApplicationThread

5. 在通过ActivityStackSupervisor来获取当前需要显示的ActivityStack

6. 继续通过ApplicationThread来发送消息给主线程的Handler来启动ActivityhandleLaunchActivity)。

7. handleLauchActivity:调用了performLauchActivity,里边Instrumentation生成了新的activity对象,继续调用activity生命周期。

IPC过程:

双方都是通过对方的代理对象来进行通信。

1.app
AMS通信:app通过本进程的AMPAMS进行Binder通信

2.AMS
和新app通信:通过ApplicationThreadProxy来通信,并不直接和ActivityThread通信

参考函数流程

Activity启动流程(从Launcher开始):

第一阶段: Launcher通知AMS要启动新的Activity(在Launcher所在的进程执行)

·      
Launcher.startActivitySafely //首先Launcher发起启动Activity的请求

·      
Activity.startActivity

·      
Activity.startActivityForResult

·      
Instrumentation.execStartActivity //交由Instrumentation代为发起请求

·      
ActivityManager.getService().startActivity
//
通过IActivityManagerSingleton.get()得到一个AMP代理对象

·      
ActivityManagerProxy.startActivity
//
通过AMP代理通知AMS启动activity

第二阶段:AMS先校验一下Activity的正确性,如果正确的话,会暂存一下Activity的信息。然后,AMS会通知Launcher程序pause Activity(在AMS所在进程执行)

·      
ActivityManagerService.startActivity

·      
ActivityManagerService.startActivityAsUser

·      
ActivityStackSupervisor.startActivityMayWait

·      
ActivityStackSupervisor.startActivityLocked
:检查有没有在AndroidManifest中注册

·      
ActivityStackSupervisor.startActivityUncheckedLocked

·      
ActivityStack.startActivityLocked :判断是否需要创建一个新的任务来启动Activity

·      
ActivityStack.resumeTopActivityLocked :获取栈顶的activity,并通知Launcher应该pause掉这个Activity以便启动新的activity

·      
ActivityStack.startPausingLocked

·      
ApplicationThreadProxy.schedulePauseActivity

第三阶段: pause LauncherActivity,并通知AMS已经paused(在Launcher所在进程执行)

·      
ApplicationThread.schedulePauseActivity

·      
ActivityThread.queueOrSendMessage

·      
H.handleMessage

·      
ActivityThread.handlePauseActivity

·      
ActivityManagerProxy.activityPaused

第四阶段:检查activity所在进程是否存在,如果存在,就直接通知这个进程,在该进程中启动Activity;不存在的话,会调用Process.start创建一个新进程(执行在AMS进程)

·      
ActivityManagerService.activityPaused

·      
ActivityStack.activityPaused

·      
ActivityStack.completePauseLocked

·      
ActivityStack.resumeTopActivityLocked

·      
ActivityStack.startSpecificActivityLocked

·      
ActivityManagerService.startProcessLocked

·      
Process.start //在这里创建了新进程,新的进程会导入ActivityThread类,并执行它的main函数

第五阶段: 创建ActivityThread实例,执行一些初始化操作,并绑定Application。如果Application不存在,会调用LoadedApk.makeApplication创建一个新的Application对象。之后进入Loop循环。(执行在新创建的app进程)

·      
ActivityThread.main

·      
ActivityThread.attach(false) //声明不是系统进程

·      
ActivityManagerProxy.attachApplication

第六阶段:处理新的应用进程发出的创建进程完成的通信请求,并通知新应用程序进程启动目标Activity组件(执行在AMS进程)

·      
ActivityManagerService.attachApplication //AMS绑定本地ApplicationThread对象,后续通过ApplicationThreadProxy来通信。

·      
ActivityManagerService.attachApplicationLocked

·      
ActivityStack.realStartActivityLocked //真正要启动Activity了!

·      
ApplicationThreadProxy.scheduleLaunchActivity
//AMS
通过ATP通知app进程启动Activity

第七阶段: 加载MainActivity类,调用onCreate声明周期方法(执行在新启动的app进程)

·      
ApplicationThread.scheduleLaunchActivity
//ApplicationThread
发消息给AT

·      
ActivityThread.queueOrSendMessage

·      
H.handleMessage //ATHandler来处理接收到的LAUNCH_ACTIVITY的消息

·      
ActivityThread.handleLaunchActivity

·      
ActivityThread.performLaunchActivity

·      
Instrumentation.newActivity //调用Instrumentation类来新建一个Activity对象

·      
Instrumentation.callActivityOnCreate

·      
MainActivity.onCreate

·      
ActivityThread.handleResumeActivity

·      
AMP.activityResumed

·      
AMS.activityResumed(AMS进程)

参考文章

http://gityuan.com/2016/03/12/start-activity/

https://blog.csdn.net/luoshengyang/article/details/6689748

收起阅读 »

一个Android TabLayout库,目前有3个TabLayout

FlycoTabLayout示例图:一个Android TabLayout库,目前有3个TabLayoutSlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.新增部分属性新增支持多种Indicator显示器新增支持未读消...
继续阅读 »

FlycoTabLayout


示例图:



一个Android TabLayout库,目前有3个TabLayout

  • SlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.

    • 新增部分属性
    • 新增支持多种Indicator显示器
    • 新增支持未读消息显示
    • 新增方法for懒癌患者
        /** 关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况 */
    public void setViewPager(ViewPager vp, String[] titles)

    /** 关联ViewPager,用于连适配器都不想自己实例化的情况 */
    public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList<Fragment> fragments)
  • CommonTabLayout:不同于SlidingTabLayout对ViewPager依赖,它是一个不依赖ViewPager可以与其他控件自由搭配使用的TabLayout.

    • 支持多种Indicator显示器,以及Indicator动画
    • 支持未读消息显示
    • 支持Icon以及Icon位置
    • 新增方法for懒癌患者
        /** 关联数据支持同时切换fragments */
    public void setTabData(ArrayList<CustomTabEntity> tabEntitys, FragmentManager fm, int containerViewId, ArrayList<Fragment> fragments)
  • SegmentTabLayout


Gradle

dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.roundview:FlycoRoundView_Lib:1.1.2@aar'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:1.5.0@aar'
}

After v2.0.0(support 2.2+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.0@aar'
}

After v2.0.2(support 3.0+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
}

代码下载:FlycoTabLayout-master.zip

原文链接:https://github.com/H07000223/FlycoTabLayout

收起阅读 »

iOS 实例对象,类对象,元类对象

OC对象的分类OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)实例对象:实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据...
继续阅读 »

OC对象的分类

OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)

  • 实例对象:

    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:
 NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);
输出结果:



instance实例对象存储的信息:
1.isa指针

2.其他成员变量


我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址

类对象

类对象的获取
        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个

类对象的作用

类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)



元类对象

1.元类对象的获取

        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个

特别注意一点:

Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

无论class几次,它返回的始终是类对象

2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)



从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空

是否为元类 class_isMetaClass(objcL);

收起阅读 »

iOS开发 - 编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »
对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余。

编译流程分析

现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World
复制代码

其实上述过程可以分解为四步:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)



预编译

首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i
复制代码

还可以下面的表达

$ cpp hello.c > hello.i
复制代码

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

  • 将所有的#define删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释///**/
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

截图个大家看看效果

经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)

编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s



通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o
复制代码

或者

gcc –c hello.s –o hello.o
复制代码

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o
复制代码

链接(linking)

  链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc
复制代码

链接相应的库




下面在贴出我们的写出的源代码是如何变成目标代码的流程图:




主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看



iOS的编译器

iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。




编译器前端的任务是进行:

  • 语法分析
  • 语义分析
  • 生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程

  • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
  • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
  • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
  • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
  • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
  • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
  • 10.生成 .app 包
  • 11.将 Swift 标准库拷贝到包中
  • 12.对包进行签名
  • 13.完成打包
编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


作者:Cooci
原贴链接:https://juejin.cn/post/6844903841842872327


收起阅读 »

iOS开发 Fastlane 自动打包技术

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
继续阅读 »

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

Github

官网

文档

我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

  • 一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

  • 避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

  • 通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect。

  • 证书的同步更新,管理,在新电脑能够迅速具备项目打包环境

如果你也有上述需求,那我相信Fastlane是一个好的选择。

那既然说Fastlane是一套工具的集合,那认识并了解其中的工具的作用是必不可少的环节。按照功能属性Fastlane对工具进行了如下分类(链接至官网详细介绍):

Testing 测试相关

Building 打包

Screenshots 截图

Project 项目配置

Code Signing 代码签名

Documentation 文档

Beta 内测相关

Push 推送

Releasing your app 发布

Source Control Git工作流

Notifications 通知相关

Misc 其他的杂七杂八

分类下对应的就是具体的每一个工具的介绍,在这里每一个工具Fastlane叫做action,下文我们也统一叫action。这里我会列举一些我认为常用的action,其他的大家可以去官网看下

gym:是fastlane提供的打包工具

snapshot: 生成多个设备的截图文件

frameit :对截图加一层物理边框

increment_build_number:自增build number 然后与之对应的get_build_number。Version number同理。

cert:创建一个新的代码签名证书

sigh:生成一个provisioning profile并保存打当前文件

pem:确保当前的推送证书是活跃的,如果没有会帮你生成一个新的

match:在团队中同步证书和描述文件。(这是一种管理证书的全新方式,需要重点关注下)

testflight:上传ipa到testflight

deliver:上传ipa到AppStore

当然官网里面其实是有很多可以划等号的Action,大家在看的时候注意下。Actions官网关于Action的介绍

多说无益,开始上手

当前最新版本是2.8.4

一、安装xcode命令行工具

xcode-select --install,如果没有安装,会弹出对话框,点击安装。如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

二、安装Fastlane

sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

三、初始化Fastlane

cd到你的项目目录执行 

fastlane init


这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。



接着会提示你输入开发者账号和密码。

[20:48:55]: Please enter your Apple ID developer credentials

[20:48:55]: Apple ID Username:

登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。


四、文件系统

初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。


其中metadata和screenshots分别对应App元数据和商店应用截图。

Appfile主要存放App的apple_id team_id app_identifier等信息

Deliverfile中为发布的配置信息,一般情况用不到。

Fastfile是我们最应该关注的文件,也是我们的工作文件。

Fastfile



之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

打包到蒲公英

这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

打开终端输入fastlane add_plugin pgyer


新建一个lane


desc "打包到pgy"


lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end

这样一个打包到蒲公英的lane就完成了。

option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

执行

在工作目录的终端执行

fastlane test desc:测试打包



然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置

其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解。

match

开头已经说了,match是一种全新的证书同步管理机制,也是我认为在fastlane中相对重要的一环,介于篇幅这篇就不细说了,有兴趣的可以去官网看下,有机会我也会更新一篇关于match的文章。match

其他的一些小提示

  1. 可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来

  2. 如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

  3. 如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

  4. 如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据skip_screenshots:true,#不上传屏幕截图

关于fastlane的一些想法

其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!



作者:Cooci
原贴链接:https://www.jianshu.com/p/59725c52e0fa





收起阅读 »

FloatWindow 安卓任意界面悬浮窗

效果图:特性:1.支持拖动,提供自动贴边等动画2.内部自动进行权限申请操作3.可自由指定要显示悬浮窗的界面4.应用退到后台时,悬浮窗会自动隐藏5.除小米外,4.4~7.0 无需权限申请6.位置及宽高可设置百分比值,轻松适配各分辨率7.支持权限申请结果、位置等状...
继续阅读 »

效果图:



特性:

1.支持拖动,提供自动贴边等动画

2.内部自动进行权限申请操作

3.可自由指定要显示悬浮窗的界面

4.应用退到后台时,悬浮窗会自动隐藏

5.除小米外,4.4~7.0 无需权限申请

6.位置及宽高可设置百分比值,轻松适配各分辨率

7.支持权限申请结果、位置等状态监听

8.链式调用,简洁清爽

集成:

第 1 步、在工程的 build.gradle 中添加:

	allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

第 2 步、在应用的 build.gradle 中添加:

	dependencies {
compile 'com.github.yhaolpz:FloatWindow:1.0.9'
}

使用:

0.声明权限

     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

1.基础使用

        FloatWindow
.with(getApplicationContext())
.setView(view)
.setWidth(100) //设置控件宽高
.setHeight(Screen.width,0.2f)
.setX(100) //设置控件初始位置
.setY(Screen.height,0.3f)
.setDesktopShow(true) //桌面显示
.setViewStateListener(mViewStateListener) //监听悬浮控件状态改变
.setPermissionListener(mPermissionListener) //监听权限申请结果
.build();

宽高及位置可设像素值或屏幕宽/高百分比,默认宽高为 wrap_content;默认位置为屏幕左上角,x、y 为偏移量。

2.指定界面显示

              .setFilter(true, A_Activity.class, C_Activity.class)

此方法表示 A_Activity、C_Activity 显示悬浮窗,其他界面隐藏。

              .setFilter(false, B_Activity.class)

此方法表示 B_Activity 隐藏悬浮窗,其他界面显示。

注意:setFilter 方法参数可以识别该 Activity 的子类

也就是说,如果 A_Activity、C_Activity 继承自 BaseActivity,你可以这样设置:

              .setFilter(true, BaseActivity.class)

3.可拖动悬浮窗及回弹动画

              .setMoveType(MoveType.slide)
.setMoveStyle(500, new AccelerateInterpolator()) //贴边动画时长为500ms,加速插值器

共提供 4 种 MoveType :

MoveType.slide : 可拖动,释放后自动贴边 (默认)

MoveType.back : 可拖动,释放后自动回到原位置

MoveType.active : 可拖动

MoveType.inactive : 不可拖动

setMoveStyle 方法可设置动画效果,只在 MoveType.slide 或 MoveType.back 模式下设置此项才有意义。默认减速插值器,默认动画时长为 300ms。

4.后续操作

        //手动控制
FloatWindow.get().show();
FloatWindow.get().hide();

//修改显示位置
FloatWindow.get().updateX(100);
FloatWindow.get().updateY(100);

//销毁
FloatWindow.destroy();

以上操作应待悬浮窗初始化后进行。

5.多个悬浮窗

        FloatWindow
.with(getApplicationContext())
.setView(imageView)
.build();

FloatWindow
.with(getApplicationContext())
.setView(button)
.setTag("new")
.build();


FloatWindow.get("new").show();
FloatWindow.get("new").hide();
FloatWindow.destroy("new");

创建第一个悬浮窗不需加 tag,之后再创建就需指定唯一 tag ,以此区分,方便进行后续操作。


代码下载:FloatWindow-master.zip

原文链接:https://github.com/yhaolpz/FloatWindow

收起阅读 »

iOS 超强富文本编辑库

YYText功能强大的 iOS 富文本编辑与显示框架特性API 兼容 UILabel 和 UITextView支持高性能的异步排版和渲染扩展了 CoreText 的属性以支持更多文字效果支持 UIImage、UIView、CALayer 作为图文混排元素支持添...
继续阅读 »

YYText

功能强大的 iOS 富文本编辑与显示框架

特性

  • API 兼容 UILabel 和 UITextView
  • 支持高性能的异步排版和渲染
  • 扩展了 CoreText 的属性以支持更多文字效果
  • 支持 UIImage、UIView、CALayer 作为图文混排元素
  • 支持添加自定义样式的、可点击的文本高亮范围
  • 支持自定义文本解析 (内置简单的 Markdown/表情解析)
  • 支持文本容器路径、内部留空路径的控制
  • 支持文字竖排版,可用于编辑和显示中日韩文本
  • 支持图片和富文本的复制粘贴
  • 文本编辑时,支持富文本占位符
  • 支持自定义键盘视图
  • 撤销和重做次数的控制
  • 富文本的序列化与反序列化支持
  • 支持多语言,支持 VoiceOver
  • 支持 Interface Builder
  • 全部代码都有文档注释
  • 架构

    YYText 和 TextKit 架构对比



    文本属性

    YYText 原生支持的属性

    DemoAttribute NameClass
    TextAttachmentYYTextAttachment
    TextHighlightYYTextHighlight
    TextBindingYYTextBinding
    TextShadow
    TextInnerShadow
    YYTextShadow
    TextBorderYYTextBorder
    TextBackgroundBorderYYTextBorder
    TextBlockBorderYYTextBorder
    TextGlyphTransformNSValue(CGAffineTransform)
    TextUnderlineYYTextDecoration
    TextStrickthroughYYTextDecoration
    TextBackedStringYYTextBackedString

    YYText 支持的 CoreText 属性

    DemoAttribute NameClass
    Font UIFont(CTFontRef)
    Kern NSNumber
    StrokeWidth NSNumber 
    StrokeColor CGColorRef 
    Shadow NSShadow 
    Ligature NSNumber 
    VerticalGlyphForm NSNumber(BOOL) 
    WritingDirection NSArray(NSNumber) 
    RunDelegate CTRunDelegateRef 
    TextAlignment NSParagraphStyle 
    (NSTextAlignment) 
    LineBreakMode NSParagraphStyle 
    (NSLineBreakMode) 
    LineSpacing NSParagraphStyle 
    (CGFloat) 
    ParagraphSpacing 
    ParagraphSpacingBefore 
    NSParagraphStyle 
    (CGFloat) 
    FirstLineHeadIndent NSParagraphStyle 
    (CGFloat) 
    HeadIndent NSParagraphStyle 
    (CGFloat) 
    TailIndent NSParagraphStyle 
    (CGFloat) 
    MinimumLineHeight NSParagraphStyle 
    (CGFloat) 
    MaximumLineHeight NSParagraphStyle 
    (CGFloat) 
    LineHeightMultiple NSParagraphStyle 
    (CGFloat) 
    BaseWritingDirection NSParagraphStyle 
    (NSWritingDirection) 
    DefaultTabInterval 
    TabStops 
    NSParagraphStyle 
    CGFloat/NSArray(NSTextTab)



    用法

    基本用法

    // YYLabel (和 UILabel 用法一致)
    YYLabel *label = [YYLabel new];
    label.frame = ...
    label.font = ...
    label.textColor = ...
    label.textAlignment = ...
    label.lineBreakMode = ...
    label.numberOfLines = ...
    label.text = ...

    // YYTextView (和 UITextView 用法一致)
    YYTextView *textView = [YYTextView new];
    textView.frame = ...
    textView.font = ...
    textView.textColor = ...
    textView.dataDetectorTypes = ...
    textView.placeHolderText = ...
    textView.placeHolderTextColor = ...
    textView.delegate = ...



    属性文本

    // 1. 创建一个属性文本
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."];

    // 2. 为文本设置属性
    text.yy_font = [UIFont boldSystemFontOfSize:30];
    text.yy_color = [UIColor blueColor];
    [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
    text.yy_lineSpacing = 10;

    // 3. 赋值到 YYLabel 或 YYTextView
    YYLabel *label = [YYLabel new];
    label.frame = ...
    label.attributedString = text;

    YYTextView *textView = [YYTextView new];
    textView.frame = ...
    textView.attributedString = text;



    文本高亮

    你可以用一些已经封装好的简便方法来设置文本高亮:

    [text yy_setTextHighlightRange:range
    color:[UIColor blueColor]
    backgroundColor:[UIColor grayColor]
    tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
    NSLog(@"tap text range:...");
    }];

    文本高亮

    你可以用一些已经封装好的简便方法来设置文本高亮:

    [text yy_setTextHighlightRange:range
    color:[UIColor blueColor]
    backgroundColor:[UIColor grayColor]
    tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
    NSLog(@"tap text range:...");
    }];

    或者用更复杂的办法来调节文本高亮的细节:

    // 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性
    YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3];

    YYTextHighlight *highlight = [YYTextHighlight new];
    [highlight setColor:[UIColor whiteColor]];
    [highlight setBackgroundBorder:highlightBorder];
    highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"tap text range:...");
    // 你也可以把事件回调放到 YYLabel 和 YYTextView 来处理。
    };

    // 2. 把"高亮"属性设置到某个文本范围
    [attributedText yy_setTextHighlight:highlight range:highlightRange];

    // 3. 把属性文本设置到 YYLabel 或 YYTextView
    YYLabel *label = ...
    label.attributedText = attributedText

    YYTextView *textView = ...
    textView.attributedText = ...

    // 4. 接受事件回调
    label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"tap text range:...");
    };
    label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"long press text range:...");
    };

    @UITextViewDelegate
    - (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
    NSLog(@"tap text range:...");
    }
    - (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
    NSLog(@"long press text range:...");
    }

    图文混排

    NSMutableAttributedString *text = [NSMutableAttributedString new];
    UIFont *font = [UIFont systemFontOfSize:16];
    NSMutableAttributedString *attachment = nil;

    // 嵌入 UIImage
    UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"];
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];

    // 嵌入 UIView
    UISwitch *switcher = [UISwitch new];
    [switcher sizeToFit];
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];

    // 嵌入 CALayer
    CASharpLayer *layer = [CASharpLayer layer];
    layer.path = ...
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];


    文本布局计算

    NSAttributedString *text = ...
    CGSize size = CGSizeMake(100, CGFLOAT_MAX);
    YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text];

    // 获取文本显示位置和大小
    layout.textBoundingRect; // get bounding rect
    layout.textBoundingSize; // get bounding size

    // 查询文本排版结果
    [layout lineIndexForPoint:CGPointMake(10,10)];
    [layout closestLineIndexForPoint:CGPointMake(10,10)];
    [layout closestPositionToPoint:CGPointMake(10,10)];
    [layout textRangeAtPoint:CGPointMake(10,10)];
    [layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];
    [layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];

    // 显示文本排版结果
    YYLabel *label = [YYLabel new];
    label.size = layout.textBoundingSize;
    label.textLayout = layout;

    部分效果展示






    安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYText'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYText"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYText 文件夹内的所有内容。
    2. 将 YYText 内的源文件添加(拖放)到你的工程。
    3. 链接以下 frameworks:
      • UIKit
      • CoreFoundation
      • CoreText
      • QuartzCore
      • Accelerate
      • MobileCoreServices
    4. 导入 YYText.h


    已知问题

  • YYText 并不能支持所有 CoreText/TextKit 的属性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 YYText 中基本都有对应属性作为替代。详情见上方表格。
  • YYTextView 未实现局部刷新,所以在输入和编辑大量的文本(比如超过大概五千个汉字、或大概一万个英文字符)时会出现较明显的卡顿现象。
  • 竖排版时,添加 exclusionPaths 在少数情况下可能会导致文本显示空白。
  • 当添加了非矩形的 textContainerPath,并且有嵌入大于文本排版方向宽度的 RunDelegate 时,RunDelegate 之后的文字会无法显示。这是 CoreText 的 Bug(或者说是 Feature)。

  • 常见问题及源码下载:点击这里

    demo:YYText.zip


    收起阅读 »

    iOS 数据缓存库

    YYCache高性能 iOS 缓存框架。特性LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。兼容性: API 基本和 NSCache 保...
    继续阅读 »

    YYCache

    高性能 iOS 缓存框架。

    特性

  • LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。
  • 缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。
  • 兼容性: API 基本和 NSCache 保持一致, 所有方法都是线程安全的。
  • 内存缓存
    • 对象释放控制: 对象的释放(release) 可以配置为同步或异步进行,可以配置在主线程或后台线程进行。
    • 自动清空: 当收到内存警告或 App 进入后台时,缓存可以配置为自动清空。
  • 磁盘缓存
    • 可定制性: 磁盘缓存支持自定义的归档解档方法,以支持那些没有实现 NSCoding 协议的对象。
    • 存储类型控制: 磁盘缓存支持对每个对象的存储类型 (SQLite/文件) 进行自动或手动控制,以获得更高的存取性能。

  • 安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYCache'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYCache"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYCache 文件夹内的所有内容。
    2. 将 YYCache 内的源文件添加(拖放)到你的工程。
    3. 链接以下的 frameworks:
      • UIKit
      • CoreFoundation
      • QuartzCore
      • sqlite3
    4. 导入 YYCache.h


    常见问题与源码下载:点击这里

    代码示例:YYCache.zip






    收起阅读 »

    iOS 强大的图片处理库(webP.编解码.gif)等

    YYImage支持以下类型动画图像的播放/编码/解码:    WebP, APNG, GIF。支持以下类型静态图像的显示/编码/解码:    WebP, PNG, GIF, JPE...
    继续阅读 »

    YYImage

  • 支持以下类型动画图像的播放/编码/解码:
        WebP, APNG, GIF。
  • 支持以下类型静态图像的显示/编码/解码:
        WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
  • 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
        PNG, GIF, JPEG, BMP。
  • 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
  • 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
  • 完全兼容 UIImage 和 UIImageView,使用方便。
  • 保留可扩展的接口,以支持自定义动画。
  • 每个类和方法都有完善的文档注释。

  • 安装

    CocoaPods

    1. 将 cocoapods 更新至最新版本.
    2. 在 Podfile 中添加 pod 'YYImage'
    3. 执行 pod install 或 pod update
    4. 导入
    5. 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 pod 'YYImage/WebP'

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYImage"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入
    4. 注意:carthage framework 并没有包含 WebP 组件。如果你需要支持 WebP,可以用 CocoaPods 安装,或者手动安装。

    手动安装

    1. 下载 YYImage 文件夹内的所有内容。
    2. 将 YYImage 内的源文件添加(拖放)到你的工程。
    3. 链接以下 frameworks:
      • UIKit
      • CoreFoundation
      • QuartzCore
      • AssetsLibrary
      • ImageIO
      • Accelerate
      • MobileCoreServices
      • libz
    4. 导入 YYImage.h
    5. 注意:如果你需要支持 WebP,可以将 Vendor/WebP.framework(静态库) 加入你的工程。

    用法

    显示动画类型的图片

    // 文件: ani@3x.gif
    UIImage *image = [YYImage imageNamed:@"ani.gif"];
    UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];

    播放帧动画

    // 文件: frame1.png, frame2.png, frame3.png
    NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
    NSArray *times = @[@0.1, @0.2, @0.1];
    UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
    UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];

    动画播放控制

    YYAnimatedImageView *imageView = ...;
    // 暂停:
    [imageView stopAnimating];
    // 播放:
    [imageView startAnimating];
    // 设置播放进度:
    imageView.currentAnimatedImageIndex = 12;
    // 获取播放状态:
    image.currentIsPlayingAnimation;
    //上面两个属性都支持 KVO。

    图片解码

    // 解码单帧图片:
    NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;

    // 渐进式图片解码 (可用于图片下载显示):
    NSMutableData *data = [NSMutableData new];
    YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
    while(newDataArrived) {
    [data appendData:newData];
    [decoder updateData:data final:NO];
    if (decoder.frameCount > 0) {
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    // progressive display...
    }
    }
    [decoder updateData:data final:YES];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    // final display...


    图片编码

    // 编码静态图 (支持各种常见图片格式):
    YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
    jpegEncoder.quality = 0.9;
    [jpegEncoder addImage:image duration:0];
    NSData jpegData = [jpegEncoder encode];

    // 编码动态图 (支持 GIF/APNG/WebP):
    YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
    webpEncoder.loopCount = 5;
    [webpEncoder addImage:image0 duration:0.1];
    [webpEncoder addImage:image1 duration:0.15];
    [webpEncoder addImage:image2 duration:0.2];
    NSData webpData = [webpEncoder encode];

    图片类型探测

    // 获取图片类型
    YYImageType type = YYImageDetectType(data);
    if (type == YYImageTypePNG) ...

    常见问题及源码:点击这里

    demo下载:YYImage.zip


    收起阅读 »

    iOS JSON转换库

    YYModel特性高性能: 模型转换性能接近手写解析代码。自动类型转换: 对象类型可以自动转换,详情见下方表格。类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。无侵入性: 模型无需继承自其他基类。轻量: 该框架只有 5 个文...
    继续阅读 »

    YYModel

    特性

    • 高性能: 模型转换性能接近手写解析代码。
    • 自动类型转换: 对象类型可以自动转换,详情见下方表格。
    • 类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
    • 无侵入性: 模型无需继承自其他基类。
    • 轻量: 该框架只有 5 个文件 (包括.h文件)。
    • 文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%


    使用方法

    简单的 Model 与 JSON 相互转换

    // JSON:
    {
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
    }

    // Model:
    @interface User : NSObject
    @property UInt64 uid;
    @property NSString *name;
    @property NSDate *created;
    @end
    @implementation User
    @end


    // 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
    User *user = [User yy_modelWithJSON:json];

    // 将 Model 转换为 JSON 对象:
    NSDictionary *json = [user yy_modelToJSONObject];

    当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。


    JSON/DictionaryModel
    NSStringNSNumber,NSURL,SEL,Class
    NSNumberNSString
    NSString/NSNumber基础类型 (BOOL,int,float,NSUInteger,UInt64,...)
    NaN 和 Inf 会被忽略
    NSStringNSDate 以下列格式解析:
    yyyy-MM-dd
    yyyy-MM-dd HH:mm:ss
    yyyy-MM-dd'T'HH:mm:ss
    yyyy-MM-dd'T'HH:mm:ssZ
    EEE MMM dd HH:mm:ss Z yyyy
    NSDateNSString 格式化为 ISO8601:
    "YYYY-MM-dd'T'HH:mm:ssZ"
    NSValuestruct (CGRect,CGSize,...)
    NSNullnil,0
    "no","false",...@(NO),0
    "yes","true",...@(YES),1



    Model 属性名和 JSON 中的 Key 不相同

    // JSON:
    {
    "n":"Harry Pottery",
    "p": 256,
    "ext" : {
    "desc" : "A book written by J.K.Rowing."
    },
    "ID" : 100010
    }

    // Model:
    @interface Book : NSObject
    @property NSString *name;
    @property NSInteger page;
    @property NSString *desc;
    @property NSString *bookID;
    @end
    @implementation Book
    //返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
    + (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
    @"page" : @"p",
    @"desc" : @"ext.desc",
    @"bookID" : @[@"id",@"ID",@"book_id"]};
    }
    @end


    你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。

    在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。

    在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

    Model 包含其他 Model

    // JSON
    {
    "author":{
    "name":"J.K.Rowling",
    "birthday":"1965-07-31T00:00:00+0000"
    },
    "name":"Harry Potter",
    "pages":256
    }

    // Model: 什么都不用做,转换会自动完成
    @interface Author : NSObject
    @property NSString *name;
    @property NSDate *birthday;
    @end
    @implementation Author
    @end

    @interface Book : NSObject
    @property NSString *name;
    @property NSUInteger pages;
    @property Author *author; //Book 包含 Author 属性
    @end
    @implementation Book
    @end


    安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYModel'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYModel"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYModel 文件夹内的所有内容。
    2. 将 YYModel 内的源文件添加(拖放)到你的工程。
    3. 导入 YYModel.h


    常见问题及demo:点击这里

    源码下载:YYModel.zip



    收起阅读 »

    提建议找bug,领京东卡,环信IM SDK等你来找茬啦~

    环信作为中国即时通讯云的开创者一直致力于为开发者提供简单/易用/稳定/完美的产品和服务。我们深知产品和服务没有极限和尽头,为了给开发者提供超出预期的产品和服务,我们诚邀小伙伴们积极参与和帮助。所谓人人为我,我为人人,让我们一起来找bug,一起提建议,一起来打造...
    继续阅读 »

    环信作为中国即时通讯云的开创者一直致力于为开发者提供简单/易用/稳定/完美的产品和服务。我们深知产品和服务没有极限和尽头,为了给开发者提供超出预期的产品和服务,我们诚邀小伙伴们积极参与和帮助。所谓人人为我,我为人人,让我们一起来找bug,一起提建议,一起来打造世界上最好用的即时通讯云sdk叭!

    活动时间

    4月1日-4月30日


    参与方式

    下载最新版环信IM SDK (https://www.easemob.com/download/im) ,依照开发文档进行模拟操作,找出开发文档或SDK或Demo中的bug或建议,即可参与活动!

    集成成功的截图和产品建议请回复本帖,bug等问题发送邮件至:market@easemob.com

     

    奖励规则

     

    参与方式

    描述及示例

    奖励

    1

    成功集成环信IM SDK!

    在本帖回复成功集成SDK的截图,并进官方红包群。

    如:iOS 开发者:截图环信后台AppKey + 环信SDK初始化代码 (截到KEY 和 编译成功) + 项目的Bundle identifier 

    Android开发者截图环信后台APPKey+项目集成的KEY +项目的applicationId

    其他客户端以此类推。

    50%中奖率抽50元京东卡,不定时群红包

    2

    一般功能bug

    EaseUIKit UI

    及兼容性问题

    例如:某功能没有达到预期效果(iOS设置头像圆角不生效)或响应结果与文档描述不符等情况;

    界面(EaseUIKit)样式错乱,或在不同机型上出现的特异性问题;

    50元京东卡

    3

    重大bug发现

    使用环信SDK时出现异常崩溃 崩溃信息指向sdk需提供异常信息log日志

    SDK某个API调用设置不生效/部分监听回调不执行等

    200元京东卡

    4

    用户体验型建议(含Demo)

    页面提示不友好等用户体验问题,对操作和引导不产生影响,属于使用建议。用户体验型问题(包括Demo应用体验)

    IMGeek定制T恤

    5

    对环信IM SDK提出有效的产品建议

    提出具体的产品建议信息,可以是交互形式,产品新功能等

    IMGeek定制T恤

     

    6

    开发文档修正建议

    包括错误的内容、不合理的文档结构等

    例如:参数错误、文档接口过时或文档接口与SDK不符等

    IMGeek定制T恤

    7

    其他回帖内容

    除以上回帖内容外,其他回帖根据内容酌情奖励

     

     

    活动规则

    1、集成成功的截图和产品建议请回复本帖~!经确认符合活动要求,将私信联系你领奖。

    2、发现bug等问题,请提供demo与复现步骤,将问题发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件联系你领取奖励;如不是产品bug,会给予回复或解决方法;

    3、如果有相同的bug提交 ,按收到邮件先后顺序奖励较早的提交者;

    4、反馈多条问题时,定制T恤奖励不叠加,京东卡可叠加至200元上限;

    5、本次活动不限客户端;

     

    成功集成SDK截图举例:

     



     


    活动奖励



    官方活动群



    新集成环信SDK的用户请留意私信,冬冬将拉你进抽奖群。

    PS:如果推荐朋友集成环信IM SDK,推荐人与被推荐人都可进群参与抽奖哦!

     

    相关地址及开发文档获取

    开发者帐号注册地址:https://console.easemob.com/user/register

    登录console地址:https://console.easemob.com/user/login

    IM SDK及Demo下载:https://www.easemob.com/download/im

    安卓端开发文档:http://docs-im.easemob.com/im/android/sdk/import

    iOS端开发文档:http://docs-im.easemob.com/im/ios/sdk/prepare

    WEB端开发文档:http://docs-im.easemob.com/im/web/intro/start

    小程序端开发文档:http://docs-im.easemob.com/im/applet/solution

    桌面端开发文档:http://docs-im.easemob.com/im/pc/intro/integration

    Linux端开发文档:http://docs-im.easemob.com/im/linux/integration

    服务端开发文档:http://docs-im.easemob.com/im/server/ready/intro

    常见开发场景说明:http://docs-im.easemob.com/im/other/integrationcases/live-chatroo

     

    *活动最终解释权归环信IMGeek社区所有。

    收起阅读 »

    iOS 超好用的图表库

    AAChartKit前言AAChartKit 项目,是AAInfographics的 Objective-C 语言版本,是在流行的开源前端图表库Highcharts的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件....
    继续阅读 »

    AAChartKit

    前言

    AAChartKit 项目,是AAInfographics的 Objective-C 语言版本,是在流行的开源前端图表库Highcharts的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件.可能是这个星球上 UI 最精致的第三方 iOS 开源图表库了(✟我以无神论者的名义向上帝起誓🖐,我真的没有在说鬼话✟)

    功能特性

    🎂  环境友好,兼容性强. 适配 iOS 8+, 支持iOS iPad OSmacOS, 支持 Objective-C语言, 同时更有 Swift 语言版本 AAInfographics  Java 语言版本 AAChartCore Kotlin 语言版本 AAChartCore-Kotlin 可供使用, 配置导入工程简单易操作. 支持的所有语言版本及连接,参见此列表.

    🚀  功能强大,类型多样 -. 支持柱状图 条形图 折线图 曲线图 折线填充图 曲线填充图雷达图极地图扇形图气泡图散点图区域范围图柱形范围图面积范围图面积范围均线图直方折线图直方折线填充图箱线图瀑布图热力图桑基图金字塔图漏斗图、等二十几种类型的图形,不可谓之不多.

    📝  现代化声明式语法. 与过往的命令式编程技巧不同, 在 AAChartKit 中绘制任意一款自定义图表, 你完全无需关心挠人的内在实现细节. 描述你所要得到的, 你便得到你所描述的.

    🔬  细致入微的用户自定义功能. 基础的主标题副标题X 轴Y 轴自不必谈, 从纵横的交互准星线、跟手的浮动提示框, 到切割数值的值域分割线值域分割颜色带, 再到细小的线条类型,标记点样式, 各种细微的图形子组件, 应有尽有. 以至于不论是极简、抽象的小清新风格, 还是纷繁复杂的严肃商业派头, 均可完美驾驭.

    🎮  交互式图形动画 . 有着清晰和充满细节的用户交互方式, 与此同时, 图形渲染动画效果细腻精致, 流畅优美. 有三十多种以上渲染动画效果可供选择, 用户可自由设置渲染图形时的动画时间和动画类型, 关于图形渲染动画类型,具体参见 AAChartKit 动画类型.

    🦋  极简主义 . AAChartView + AAChartModel = Chart,在 AAChartKit 图表框架当中,遵循这样一个极简主义公式:图表视图控件 + 图表模型 = 你想要的图表. 同另一款强大而又精美的图表库AAInfographics完全一致.

      链式编程语法 . 支持类 Masonry 链式编程语法, 一行代码即可配置完成 AAChartModel模型对象实例.

    🎈  简洁清晰,轻便易用 . 最少仅仅需要 五行代码 即可完成整个图表的绘制工作(使用链式编程语法配置 AAChartModel 实例对象时, 无论你写多少行代码, 理论上只能算作是一行). 🤪🤪🤪

    🖱  交互事件回调 支持图表的用户点击事件及单指滑动事件, 可在此基础上实现双表联动乃至多表联动,以及其他更多更复杂的自定义用户交互效果.

    👌  支持手势缩放 . 支持各个方向的图表手势缩放和拖动阅览, 手势缩放类型具体参见 AAChartKit 手势缩放类型, 默认禁用手势缩放功能

    效果图


    CocoaPods 安装 (推荐)

    1. 在 Podfile 中添加以下内容
    pod 'AAChartKit', :git => 'https://github.com/AAChartModel/AAChartKit.git'
    1. 执行  pod install 或  pod update

    手动安装
    1. 将项目Demo中的文件夹AAChartKitLib拖入到所需项目中.
    2. 在你的项目的 .pch 全局宏定义文件中添加
    #import "AAGlobalMacro.h"

    使用

    1. 在你的ViewController视图控制器文件中添加
    #import "AAChartKit.h"
    1. 创建视图AAChartView
    CGFloat chartViewWidth  = self.view.frame.size.width;
    CGFloat chartViewHeight = self.view.frame.size.height - 250;
    _aaChartView = [[AAChartView alloc]init];
    _aaChartView.frame = CGRectMake(0, 60, chartViewWidth, chartViewHeight);
    ////禁用 AAChartView 滚动效果(默认不禁用)
    //self.aaChartView.scrollEnabled = NO;
    [self.view addSubview:_aaChartView];
    1. 配置视图模型AAChartModel
    AAChartModel *aaChartModel= AAObject(AAChartModel)
    .chartTypeSet(AAChartTypeArea)//设置图表的类型(这里以设置的为折线面积图为例)
    .titleSet(@"编程语言热度")//设置图表标题
    .subtitleSet(
    @"虚拟数据")//设置图表副标题
    .categoriesSet(@[@"Java",@"Swift",@"Python",@"Ruby", @"PHP",@"Go",@"C",@"C#",@"C++"])//图表横轴的内容
    .yAxisTitleSet(
    @"摄氏度")//设置图表 y 轴的单位
    .seriesSet(@[
    AAObject(AASeriesElement)
    .nameSet(@"2017")
    .
    dataSet(@[@7.0, @6.9, @9.5, @14.5, @18.2, @21.5, @25.2, @26.5, @23.3, @18.3, @13.9, @9.6]),
    AAObject(AASeriesElement)
    .
    nameSet(@"2018")
    .dataSet(@[@0.2, @0.8, @5.7, @11.3, @17.0, @22.0, @24.8, @24.1, @20.1, @14.1, @8.6, @2.5]),
    AAObject(AASeriesElement)
    .nameSet(@"2019")
    .
    dataSet(@[@0.9, @0.6, @3.5, @8.4, @13.5, @17.0, @18.6, @17.9, @14.3, @9.0, @3.9, @1.0]),
    AAObject(AASeriesElement)
    .
    nameSet(@"2020")
    .dataSet(@[@3.9, @4.2, @5.7, @8.5, @11.9, @15.2, @17.0, @16.6, @14.2, @10.3, @6.6, @4.8]),
    ])
    ;
    1. 绘制图形(创建 AAChartView 实例对象后,首次绘制图形调用此方法)
    /*图表视图对象调用图表模型对象,绘制最终图形*/
    [_aaChartView aa_drawChartWithChartModel:aaChartModel];



    当前已支持的图表类型有十种以上,说明如下

    typedef NSString *AAChartType;

    AACHARTKIT_EXTERN AAChartType const AAChartTypeColumn; //柱形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBar; //条形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeArea; //折线区域填充图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeAreaspline; //曲线区域填充图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeLine; //折线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeSpline; //曲线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeScatter; //散点图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePie; //扇形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBubble; //气泡图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePyramid; //金字塔图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeFunnel; //漏斗图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeColumnrange; //柱形范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeArearange; //区域折线范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeAreasplinerange; //区域曲线范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBoxplot; //箱线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeWaterfall; //瀑布图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePolygon; //多边形图

    当前已支持的图表手势缩放类型共有三种,说明如下

    typedef NSString *AAChartZoomType;

    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeNone; //禁用手势缩放功能(默认禁用手势缩放)
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeX; //支持图表 X轴横向缩放
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeY; //支持图表 Y轴纵向缩放
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeXY; //支持图表等比例缩放

    AAChartModel 属性配置列表

    AAPropStatementAndPropSetFuncStatement(copy,   AAChartModel, NSString *, title) //标题文本内容
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, titleFontSize) //标题字体尺寸大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontColor) //标题字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontWeight) //标题字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitle) //副标题文本内容
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, subtitleFontSize) //副标题字体尺寸大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontColor) //副标题字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontWeight) //副标题字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, backgroundColor) //图表背景色(必须为十六进制的颜色色值如红色"#FF0000")
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, colorsTheme) //图表主题颜色数组
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, categories) //x轴坐标每个点对应的名称(注意:这个不是用来设置 X 轴的值,仅仅是用于设置 X 轴文字内容的而已)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, series) //图表的数据列内容

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSubtitleAlignType, subtitleAlign) //图表副标题文本水平对齐方式。可选的值有 “left”,”center“和“right”。 默认是:center.
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartType, chartType) //图表类型
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartStackingType, stacking) //堆积样式
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSymbolType, markerSymbol) //折线曲线连接点的类型:"circle", "square", "diamond", "triangle","triangle-down",默认是"circle"
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartSymbolStyleType, markerSymbolStyle)
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartZoomType, zoomType) //缩放类型 AAChartZoomTypeX 表示可沿着 x 轴进行手势缩放
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartAnimation, animationType) //设置图表的渲染动画类型
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, animationDuration) //设置图表的渲染动画时长(动画单位为毫秒)

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, inverted) //x 轴是否垂直,默认为否
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, gradientColorsThemeEnabled) //是否将常规主题颜色数组 colorsTheme 自动转换为半透明渐变效果的颜色数组(设置后就不用自己再手动去写渐变色字典,相当于是设置渐变色的一个快捷方式,当然了,如果需要细致地自定义渐变色效果,还是需要自己手动配置渐变颜色字典内容,具体方法参见图表示例中的`颜色渐变条形图`示例代码),默认为否
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, polar) //是否极化图形(变为雷达图),默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, dataLabelEnabled) //是否显示数据,默认为否
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontColor) //Datalabel font color
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, dataLabelFontSize) //Datalabel font size
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontWeight) //Datalabel font weight


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisVisible) //x 轴是否可见(默认可见)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisReversed) // x 轴翻转,默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisLabelsEnabled) //x 轴是否显示文字
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisLabelsFontSize) //x 轴文字字体大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisLabelsFontColor) //x 轴文字字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType, xAxisLabelsFontWeight) //x 轴文字字体粗细

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisGridLineWidth) //x 轴网格线的宽度
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisTickInterval) //x轴刻度点间隔数(设置每隔几个点显示一个 X轴的内容)

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisCrosshairWidth) //设置 x 轴准星线的宽度
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisCrosshairColor) //设置 x 轴准星线的颜色
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, xAxisCrosshairDashStyleType) //设置 x 轴准星线的线条样式类型


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisVisible) //y 轴是否可见(默认可见)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisReversed) //y 轴翻转,默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisLabelsEnabled) //y 轴是否显示文字
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLabelsFontSize) //y 轴文字字体大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisLabelsFontColor) //y 轴文字字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType , yAxisLabelsFontWeight) //y 轴文字字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisTitle) //y 轴标题
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLineWidth) //y y-axis line width
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisGridLineWidth) //y轴网格线的宽度
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisAllowDecimals) //是否允许 y 轴显示小数
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisPlotLines) //y 轴基线的配置
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMax) //y 轴最大值
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMin) //y 轴最小值(设置为0就不会有负数)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisTickInterval)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisTickPositions) //自定义 y 轴坐标(如:[@(0), @(25), @(50), @(75) , (100)])

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisCrosshairWidth) //设置 y 轴准星线的宽度
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisCrosshairColor) //设置 y 轴准星线的颜色
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, yAxisCrosshairDashStyleType) //设置 y 轴准星线的线条样式类型


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipEnabled) //是否显示浮动提示框(默认显示)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipShared)//是否多组数据共享一个浮动提示框
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, tooltipValueSuffix) //浮动提示框单位后缀

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, connectNulls) //设置折线是否断点重连(是否连接空值点)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, legendEnabled) //是否显示图例 lengend(图表底部可点按的圆点和文字)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, borderRadius) //柱状图长条图头部圆角半径(可用于设置头部的形状,仅对条形图,柱状图有效)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, markerRadius) //折线连接点的半径长度

    源码下载:AAChartKit-demo.zip

    常见问题及详细说明:点击这里



    收起阅读 »

    iOS 相册选择器推荐

    HXPhotoPicker效果预览特性 - Features 查看、选择GIF图片 照片、视频可同时多选/原图 3DTouch预览照片 长按拖动改变顺序 自定义相机拍照、录制视频 自定义转场动画&nb...
    继续阅读 »

    HXPhotoPicker

    效果预览



    特性 - Features

    •  查看、选择GIF图片
    •  照片、视频可同时多选/原图
    •  3DTouch预览照片
    •  长按拖动改变顺序
    •  自定义相机拍照、录制视频
    •  自定义转场动画
    •  查看、选择LivePhoto iOS9.1以上才有用
    •  浏览网络图片、网络视频
    •  仿微信编辑图片功能
    •  自定义裁剪视频时长
    •  传入本地图片、视频
    •  在线下载iCloud上的资源
    •  两种相册展现方式(列表、弹窗)
    •  支持Cell上添加
    •  支持草稿功能
    •  同一界面多个不同选择器
    •  支持暗黑模式
    •  支持横向布局
    •  支持Xib和Masonry布局
    •  支持自定义item的大小
    •  支持滑动手势选择

    安装 - Installation

    CocoaPods
    # 将以下内容添加到您的Podfile中:
    # 不使用网络图片功能
    pod 'HXPhotoPicker', '~> 3.1.9'

    # 使用SDWebImage加载网络图片
    pod 'HXPhotoPicker/SDWebImage', '~> 3.1.9'

    # 使用YYWebImage加载网络图片
    pod 'HXPhotoPicker/YYWebImage', '~> 3.1.9'

    # 搜索不到库或最新版时请执行
    pod repo update rm ~/Library/Caches/CocoaPods/search_index.json
    Carthage
    # 将以下内容添加到您的Cartfile中:
    github "SilenceLove/HXPhotoPicker"
    手动导入
    手动导入:将项目中的“HXPhotoPicker”文件夹拖入项目中
    使用前导入头文件 "HXPhotoPicker.h"

    要求 - Requirements

    • iOS8及以上系统可使用. ARC环境. - iOS 8 or later. Requires ARC
    • 访问相册和相机需要配置四个info.plist文件
    • Privacy - Photo Library Usage Description 和 Privacy - Camera Usage Description 以及 Privacy - Microphone Usage Description
    • Privacy - Location When In Use Usage Description 使用相机拍照时会获取位置信息
    • 相机拍照功能请使用真机调试

    应用示例 - Examples

    跳转相册选择照片

    // 懒加载 照片管理类
    - (HXPhotoManager *)manager {
    if (!_manager) {
    _manager = [[HXPhotoManager alloc] initWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
    }
    return _manager;
    }

    // 方法一:
    HXWeakSelf
    [self hx_presentSelectPhotoControllerWithManager:self.manager didDone:^(NSArray *allList, NSArray *photoList, NSArray *videoList, BOOL isOriginal, UIViewController *viewController, HXPhotoManager *manager) {
    weakSelf.total.text = [NSString stringWithFormat:@"总数量:%ld ( 照片:%ld 视频:%ld )",allList.count, photoList.count, videoList.count];
    weakSelf.original.text = isOriginal ? @"YES" : @"NO";
    NSSLog(@"block - all - %@",allList);
    NSSLog(@"block - photo - %@",photoList);
    NSSLog(@"block - video - %@",videoList);
    } cancel:^(UIViewController *viewController, HXPhotoManager *manager) {
    NSSLog(@"block - 取消了");
    }];

    // 方法二:
    // 照片选择控制器
    HXCustomNavigationController *nav = [[HXCustomNavigationController alloc] initWithManager:self.manager delegate:self];
    [self presentViewController:nav animated:YES completion:nil];

    // 通过 HXCustomNavigationControllerDelegate 代理返回选择的图片以及视频
    /**
    点击完成按钮

    @param photoNavigationViewController self
    @param allList 已选的所有列表(包含照片、视频)
    @param photoList 已选的照片列表
    @param videoList 已选的视频列表
    @param original 是否原图
    */
    - (void)photoNavigationViewController:(HXCustomNavigationController *)photoNavigationViewController didDoneAllList:(NSArray *)allList photos:(NSArray *)photoList videos:(NSArray *)videoList original:(BOOL)original;

    /**
    点击取消

    @param photoNavigationViewController self
    */
    - (void)photoNavigationViewControllerDidCancel:(HXCustomNavigationController *)photoNavigationViewController;

    单独使用HXPhotoPreviewViewController预览图片

    HXCustomAssetModel *assetModel1 = [HXCustomAssetModel assetWithLocaImageName:@"1" selected:YES];
    // selected 为NO 的会过滤掉
    HXCustomAssetModel *assetModel2 = [HXCustomAssetModel assetWithLocaImageName:@"2" selected:NO];
    HXCustomAssetModel *assetModel3 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/1466408576222.jpg"] selected:YES];
    // selected 为NO 的会过滤掉
    HXCustomAssetModel *assetModel4 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/0034821a-6815-4d64-b0f2-09103d62630d.jpg"] selected:NO];
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"QQ空间视频_20180301091047" withExtension:@"mp4"];
    HXCustomAssetModel *assetModel5 = [HXCustomAssetModel assetWithLocalVideoURL:url selected:YES];

    HXPhotoManager *photoManager = [HXPhotoManager managerWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
    photoManager.configuration.saveSystemAblum = YES;
    photoManager.configuration.photoMaxNum = 0;
    photoManager.configuration.videoMaxNum = 0;
    photoManager.configuration.maxNum = 10;
    photoManager.configuration.selectTogether = YES;
    photoManager.configuration.photoCanEdit = NO;
    photoManager.configuration.videoCanEdit = NO;

    HXWeakSelf
    // 长按事件
    photoManager.configuration.previewRespondsToLongPress = ^(UILongPressGestureRecognizer *longPress,
    HXPhotoModel *photoModel,
    HXPhotoManager *manager,
    HXPhotoPreviewViewController *previewViewController) {
    hx_showAlert(previewViewController, @"提示", @"长按事件", @"确定", nil, nil, nil);
    };
    // 跳转预览界面时动画起始的view
    photoManager.configuration.customPreviewFromView = ^UIView *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell;
    };
    // 跳转预览界面时展现动画的image
    photoManager.configuration.customPreviewFromImage = ^UIImage *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell.imageView.image;
    };
    // 退出预览界面时终点view
    photoManager.configuration.customPreviewToView = ^UIView *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell;
    };
    [photoManager addCustomAssetModel:@[assetModel1, assetModel2, assetModel3, assetModel4, assetModel5]];

    [self hx_presentPreviewPhotoControllerWithManager:photoManager
    previewStyle:HXPhotoViewPreViewShowStyleDark
    currentIndex:0
    photoView:nil];


    UIViewController+HXExtension.h
    /// 跳转预览照片界面
    /// @param manager 照片管理者
    /// @param previewStyle 预览样式
    /// @param currentIndex 当前预览的下标
    /// @param photoView 照片展示视图 - 没有就不传
    - (void)hx_presentPreviewPhotoControllerWithManager:(HXPhotoManager *)manager
    previewStyle:(HXPhotoViewPreViewShowStyle)previewStyle
    currentIndex:(NSUInteger)currentIndex
    photoView:(HXPhotoView * _Nullable)photoView;


    使用如何保存草稿

    通过 HXPhotoManager 对象进行存储
    /// 获取保存在本地文件的模型数组
    - (NSArray *)getLocalModelsInFile;

    /// 将模型数组保存到本地文件
    - (BOOL)saveLocalModelsToFile;

    /// 将保存在本地文件的模型数组删除
    - (BOOL)deleteLocalModelsInFile;

    /// 将本地获取的模型数组添加到manager的数据中
    /// @param models 在本地获取的模型数组
    - (void)addLocalModels:(NSArray *)models;

    /// 将本地获取的模型数组添加到manager的数据中
    - (void)addLocalModels;


    demo:HXPhotoPicker.zip

    常见问题及源码地址:点击这里


    收起阅读 »

    iOS 网络图片加载库

    SDWebImage  一款超级好用的网络图片加载库集成方式pod 'SDWebImage', '~> 5.0'使用方式#import [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址...
    继续阅读 »

    SDWebImage  一款超级好用的网络图片加载库

    集成方式

    pod 'SDWebImage', '~> 5.0'

    使用方式

    #import <SDWebImage/SDWebImage.h>
    [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"]
    placeholderImage:[UIImage imageNamed:@"占位图名字"]];

        加载gif

    SDAnimatedImageView *imageView = [SDAnimatedImageView new];
    SDAnimatedImage *animatedImage = [SDAnimatedImage imageNamed:@"image.gif"];
    imageView.image = animatedImage;

       使用Blocks,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否

    [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"] placeholderImage:[UIImage imageNamed:@"占位图"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { //... completion code here ... }];

       取图片的缓存大小
    [SDImageCache sharedImageCache] getSize];
       清理内存,磁盘缓存
    [[SDImageCache sharedImageCache] clearMemory];


    常见问题及demo地址:点击这里


    收起阅读 »

    iOS 提示框

    推荐一个好用的iOS 提示框库MBProgressHUD集成方式pod 'MBProgressHUD', '~> 1.2.0'或者直接将附件拖入项目内导入#import "MBProgressHUD.h" 效果图:在这里顺便分享一下使用小技巧我们可以将hud定...
    继续阅读 »

    推荐一个好用的iOS 提示框库

    MBProgressHUD

    集成方式

    pod 'MBProgressHUD', '~> 1.2.0'

    或者直接将附件拖入项目内

    导入#import "MBProgressHUD.h"


    效果图:





    在这里顺便分享一下使用小技巧

    我们可以将hud定义成宏 .

    #pragma mark - hud 提示

    /**

     默认请求开始的hud

     */

    #define HudShow MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.label.text = NSLocalizedString(@"请求中...", @"HUD loading title");


    /**

     自定义title的请求开始hud

     */

    #define HudShowStr(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.label.text = NSLocalizedString(str, @"HUD loading title");


    /**

     移除hud

     */

    #define HudHidden         [hud hideAnimated:YES];


    /**

     自定义title 的提示hud

     */ 

    #define HudMessageStr(str) hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\


    /**

     提示的hud .title来自json

     */

    #define HudMessage hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"根据需求传值例如取服务器json的某个值", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\



    /**

     请求错误时的hud

     */

    #define HudError   hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); \

    [hud hideAnimated:YES afterDelay:1.f];




    /**

     alert效果的hud

     */

    #define alertHudShow(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\





    /**

     请求时错误才提示的hud

     */

    #define RequestErrorHud MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\


    源码下载: Hud.zip

    常见问题及demo:点击这里

    收起阅读 »

    ImmersionBar -- android 4.4以上沉浸式实现

    ImmersionBar -- android 4.4以上沉浸式实现使用android studio// 基础依赖包,必须要依赖 implementation 'com.gyf.immersionbar:immersionbar:3.0.0' // fragm...
    继续阅读 »

    ImmersionBar -- android 4.4以上沉浸式实现

    使用

    android studio

    // 基础依赖包,必须要依赖
    implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
    // fragment快速实现(可选)
    implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0'
    // kotlin扩展(可选)
    implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0'

    关于使用AndroidX支持库

    • 如果你的项目中使用了AndroidX支持库,请在你的gradle.properties加入如下配置,如果已经配置了,请忽略
         android.useAndroidX=true
      android.enableJetifier=true

    关于全面屏与刘海

    关于全面屏

    在manifest加入如下配置,四选其一,或者都写

    ① 在manifest的Application节点下加入

       <meta-data 
    android:name="android.max_aspect"
    android:value="2.4" />

    ② 在manifest的Application节点中加入

       android:resizeableActivity="true"

    ③ 在manifest的Application节点中加入

       android:maxAspectRatio="2.4"

    ④ 升级targetSdkVersion为25以上版本

    关于刘海屏

    在manifest的Application节点下加入,vivo和oppo没有找到相关配置信息

       
    <meta-data
    android:name="android.notch_support"
    android:value="true"/>

    <meta-data
    android:name="notch.config"
    android:value="portrait|landscape" />


    Api详解

    • 基础用法

      ImmersionBar.with(this).init();
    • 高级用法(每个参数的意义)

       ImmersionBar.with(this)
      .transparentStatusBar() //透明状态栏,不写默认透明色
      .transparentNavigationBar() //透明导航栏,不写默认黑色(设置此方法,fullScreen()方法自动为true)
      .transparentBar() //透明状态栏和导航栏,不写默认状态栏为透明色,导航栏为黑色(设置此方法,fullScreen()方法自动为true)
      .statusBarColor(R.color.colorPrimary) //状态栏颜色,不写默认透明色
      .navigationBarColor(R.color.colorPrimary) //导航栏颜色,不写默认黑色
      .barColor(R.color.colorPrimary) //同时自定义状态栏和导航栏颜色,不写默认状态栏为透明色,导航栏为黑色
      .statusBarAlpha(0.3f) //状态栏透明度,不写默认0.0f
      .navigationBarAlpha(0.4f) //导航栏透明度,不写默认0.0F
      .barAlpha(0.3f) //状态栏和导航栏透明度,不写默认0.0f
      .statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色
      .navigationBarDarkIcon(true) //导航栏图标是深色,不写默认为亮色
      .autoDarkModeEnable(true) //自动状态栏字体和导航栏图标变色,必须指定状态栏颜色和导航栏颜色才可以自动变色哦
      .autoStatusBarDarkModeEnable(true,0.2f) //自动状态栏字体变色,必须指定状态栏颜色才可以自动变色哦
      .autoNavigationBarDarkModeEnable(true,0.2f) //自动导航栏图标变色,必须指定导航栏颜色才可以自动变色哦
      .flymeOSStatusBarFontColor(R.color.btn3) //修改flyme OS状态栏字体颜色
      .fullScreen(true) //有导航栏的情况下,activity全屏显示,也就是activity最下面被导航栏覆盖,不写默认非全屏
      .hideBar(BarHide.FLAG_HIDE_BAR) //隐藏状态栏或导航栏或两者,不写默认不隐藏
      .addViewSupportTransformColor(toolbar) //设置支持view变色,可以添加多个view,不指定颜色,默认和状态栏同色,还有两个重载方法
      .titleBar(view) //解决状态栏和布局重叠问题,任选其一
      .titleBarMarginTop(view) //解决状态栏和布局重叠问题,任选其一
      .statusBarView(view) //解决状态栏和布局重叠问题,任选其一
      .fitsSystemWindows(true) //解决状态栏和布局重叠问题,任选其一,默认为false,当为true时一定要指定statusBarColor(),不然状态栏为透明色,还有一些重载方法
      .supportActionBar(true) //支持ActionBar使用
      .statusBarColorTransform(R.color.orange) //状态栏变色后的颜色
      .navigationBarColorTransform(R.color.orange) //导航栏变色后的颜色
      .barColorTransform(R.color.orange) //状态栏和导航栏变色后的颜色
      .removeSupportView(toolbar) //移除指定view支持
      .removeSupportAllView() //移除全部view支持
      .navigationBarEnable(true) //是否可以修改导航栏颜色,默认为true
      .navigationBarWithKitkatEnable(true) //是否可以修改安卓4.4和emui3.x手机导航栏颜色,默认为true
      .navigationBarWithEMUI3Enable(true) //是否可以修改emui3.x手机导航栏颜色,默认为true
      .keyboardEnable(true) //解决软键盘与底部输入框冲突问题,默认为false,还有一个重载方法,可以指定软键盘mode
      .keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //单独指定软键盘模式
      .setOnKeyboardListener(new OnKeyboardListener() { //软键盘监听回调,keyboardEnable为true才会回调此方法
      @Override
      public void onKeyboardChange(boolean isPopup, int keyboardHeight) {
      LogUtils.e(isPopup); //isPopup为true,软键盘弹出,为false,软键盘关闭
      }
      })
      .setOnNavigationBarListener(onNavigationBarListener) //导航栏显示隐藏监听,目前只支持华为和小米手机
      .setOnBarListener(OnBarListener) //第一次调用和横竖屏切换都会触发,可以用来做刘海屏遮挡布局控件的问题
      .addTag("tag") //给以上设置的参数打标记
      .getTag("tag") //根据tag获得沉浸式参数
      .reset() //重置所以沉浸式参数
      .init(); //必须调用方可应用以上所配置的参数

    在Activity中实现沉浸式

    • java用法

       ImmersionBar.with(this).init();
    • kotlin用法

       immersionBar {
      statusBarColor(R.color.colorPrimary)
      navigationBarColor(R.color.colorPrimary)
      }

    在Fragment中实现沉浸式

    在Fragment使用ImmersionBar


    在Dialog中实现沉浸式,具体实现参考demo

    • ①结合dialogFragment使用,可以参考demo中的BaseDialogFragment这个类

          ImmersionBar.with(this).init();
    • ②其他dialog,关闭dialog的时候必须调用销毁方法

          ImmersionBar.with(this, dialog).init();

      销毁方法:

      java中

          ImmersionBar.destroy(this, dialog);

      kotlin中

          destroyImmersionBar(dialog)


    在PopupWindow中实现沉浸式,具体实现参考demo

    重点是调用以下方法,但是此方法会导致有导航栏的手机底部布局会被导航栏覆盖,还有底部输入框无法根据软键盘弹出而弹出,具体适配请参考demo。

        popupWindow.setClippingEnabled(false);

    状态栏与布局顶部重叠解决方案,六种方案根据不同需求任选其一

    • ① 使用dimen自定义状态栏高度,不建议使用,因为设备状态栏高度并不是固定的

      在values-v19/dimens.xml文件下

          <dimen name="status_bar_height">25dpdimen>

      在values/dimens.xml文件下

          <dimen name="status_bar_height">0dpdimen>

      然后在布局界面添加view标签,高度指定为status_bar_height

         <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="@color/darker_gray"
      android:orientation="vertical">

      <View
      android:layout_width="match_parent"
      android:layout_height="@dimen/status_bar_height"
      android:background="@color/colorPrimary" />

      <android.support.v7.widget.Toolbar
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@color/colorPrimary"
      app:title="方法一"
      app:titleTextColor="@android:color/white" />
      LinearLayout>
    • ② 使用系统的fitsSystemWindows属性,使用该属性不会导致输入框与软键盘冲突问题,不要再Fragment使用该属性,只适合纯色状态栏

          <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      android:fitsSystemWindows="true">
      LinearLayout>

      然后使用ImmersionBar时候必须指定状态栏颜色

          ImmersionBar.with(this)
      .statusBarColor(R.color.colorPrimary)
      .init();
      • 注意:ImmersionBar一定要在设置完布局以后使用,
    • ③ 使用ImmersionBar的fitsSystemWindows(boolean fits)方法,只适合纯色状态栏

          ImmersionBar.with(this)
      .fitsSystemWindows(true) //使用该属性,必须指定状态栏颜色
      .statusBarColor(R.color.colorPrimary)
      .init();
    • ④ 使用ImmersionBar的statusBarView(View view)方法,可以用来适配渐变色状态栏、侧滑返回

      在标题栏的上方增加View标签,高度指定为0dp

          <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="@color/darker_gray"
      android:orientation="vertical">

      <View
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:background="@color/colorPrimary" />

      <android.support.v7.widget.Toolbar
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@color/colorPrimary"
      app:title="方法四"
      app:titleTextColor="@android:color/white" />
      LinearLayout>

      然后使用ImmersionBar的statusBarView方法,指定view就可以啦

           ImmersionBar.with(this)
      .statusBarView(view)
      .init();
      //或者
      //ImmersionBar.setStatusBarView(this,view);
    • ⑤ 使用ImmersionBar的titleBar(View view)方法,原理是设置paddingTop,可以用来适配渐变色状态栏、侧滑返回

               ImmersionBar.with(this)
      .titleBar(view) //可以为任意view,如果是自定义xml实现标题栏的话,标题栏根节点不能为RelativeLayout或者ConstraintLayout,以及其子类
      .init();
      //或者
      //ImmersionBar.setTitleBar(this, view);
    • ⑥ 使用ImmersionBar的titleBarMarginTop(View view)方法,原理是设置marginTop,只适合纯色状态栏

               ImmersionBar.with(this)
      .titleBarMarginTop(view) //可以为任意view
      .statusBarColor(R.color.colorPrimary) //指定状态栏颜色,根据情况是否设置
      .init();
      //或者使用静态方法设置
      //ImmersionBar.setTitleBarMarginTop(this,view);

    解决EditText和软键盘的问题

    • 第一种方案
          ImmersionBar.with(this)
      .keyboardEnable(true) //解决软键盘与底部输入框冲突问题
      // .keyboardEnable(true, WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
      // | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //软键盘自动弹出
      .init();
    • 第二种方案 不使用keyboardEnable方法,只需要在布局的根节点(最外层节点)加上android:fitsSystemWindows="true"属性即可,只适合纯色状态栏

    当白色背景状态栏遇到不能改变状态栏字体为深色的设备时,解决方案

          ImmersionBar.with(this)
    .statusBarDarkFont(true, 0.2f) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度
    .init();


    状态栏和导航栏其它方法

    • public static boolean hasNavigationBar(Activity activity)

      判断是否存在导航栏

    • public static int getNavigationBarHeight(Activity activity)

      获得导航栏的高度

    • public static int getNavigationBarWidth(Activity activity)

      获得导航栏的宽度

    • public static boolean isNavigationAtBottom(Activity activity)

      判断导航栏是否在底部

    • public static int getStatusBarHeight(Activity activity)

      获得状态栏的高度

    • public static int getActionBarHeight(Activity activity)

      获得ActionBar的高度

    • public static boolean hasNotchScreen(Activity activity)

      是否是刘海屏

    • public static boolean getNotchHeight(Activity activity)

      获得刘海屏高度

    • public static boolean isSupportStatusBarDarkFont()

      判断当前设备支不支持状态栏字体设置为黑色

    • public static boolean isSupportNavigationIconDark()

      判断当前设备支不支持导航栏图标设置为黑色

    • public static void hideStatusBar(Window window)

      隐藏状态栏

    混淆规则(proguard-rules.pro)

     -keep class com.gyf.immersionbar.* {*;} 
    -dontwarn com.gyf.immersionbar.**



    代码下载 :ImmersionBar-master.zip

    原文链接:https://github.com/gyf-dev/ImmersionBar


    收起阅读 »

    material风格-DialogUtil

    DialogUtilmaterial风格(v7支持包中的),ios风格,自动获取顶层activity,可在任意界面弹出,可在任意线程弹出注意点在activity已经resume后再调用,不要在onstart里用,否则可能会不显示. 如果非要在onstart里,...
    继续阅读 »

    DialogUtil

    material风格(v7支持包中的),ios风格,自动获取顶层activity,可在任意界面弹出,可在任意线程弹出

    注意点

    • 在activity已经resume后再调用,不要在onstart里用,否则可能会不显示. 
    • 如果非要在onstart里,就记得调用setActivity()
    • 如果有的国产机不显示,就调用setActivity()
    • 不要滥用loadingdialog:

    注意使用的场景:

     第一此进入页面,用layout内部的loadingview,有很多statelayout框架,
    再次刷新,用刷新头显示刷新状态
    局部刷新或点击某按钮访问网络,用loading dialog,不影响页面本身状态,类似web中的ajax请求.

    特性

    • **自动获取顶层activity,**无需传入activity也可弹出dialog.如果传入,则指定在此activity弹出.
    • 安全,任意线程均可调用.
    • 类型丰富,包括常用的ios风格dialog和material design风格的dialog,且按钮和文字样式可便捷地修改
    • 自定义view:可以传入自定义的view,定义好事件,本工具负责安全地显示
    • 也可以保留iso样式或material 样式的底部按钮和上方title(可隐藏),中间的view可以完全自定义
    • 考虑了显示内容超多时的滑动和与屏幕的间隙.
    • 也可以设置宽高百分比来自定义宽高
    • 可以关闭默认的阴影背景,从而能使用xml中自定义的背景(弹出自定义view的dialog时常用)
    • 支持国际化
    • 智能弹出和隐藏软键盘.自定义view的dialog只要设置setNeedSoftKeyboard为true,即可自动处理软键盘的弹出和隐藏
    • ios样式和material 样式的均可以在三种状态下显示: 普通dialog,TYPE_TOAST,作为activity.(原生ProgressDialog和Design包下的BottomSheetDialog除外,其在TYPE_TOAST或activity显示有异样)
    • 支持带x的广告样式的动画

    useage

    gradle

    Step 1. Add the JitPack repository to your build file

    Add it in your root build.gradle at the end of repositories:

    	allprojects {
    repositories {
    ...
    maven { url "https://jitpack.io" }
    }
    }

    Step 2. Add the dependency

    	dependencies {
           compile ('com.github.hss01248:DialogUtil:lastest release'){
    exclude group: 'com.android.support'
           }
            compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:recyclerview-v7:26.1.0'
    compile 'com.android.support:design:26.1.0'
    //将26.1.0: 改为自己项目中一致的版本
    }

    lastest release: https://github.com/hss01248/DialogUtil/releases

    初始化

    //在Application的oncreate方法里:
    传入context
    StyledDialog.init(this);

    activity生命周期callback中拿到顶层activity引用:
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    ActivityStackManager.getInstance().addActivity(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {
    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    ActivityStackManager.getInstance().removeActivity(activity);
    }
    });

    示例代码(MainActivity里)

            //使用默认样式时,无须.setxxx:
    StyledDialog.buildLoading().show();

    //自定义部分样式时:
    StyledDialog.buildMdAlert("title", msg, new MyDialogListener() {
    @Override
    public void onFirst() {
    showToast("onFirst");
    }

    @Override
    public void onSecond() {
    showToast("onSecond");
    }

    @Override
    public void onThird() {
    showToast("onThird");
    }


    })
    .setBtnSize(20)
    .setBtnText("i","b","3")
    .show();

    相关回调

    MyDialogListener

    	public abstract void onFirst();//md-确定,ios-第一个
    public abstract void onSecond();//md-取消,ios-第二个
    public void onThird(){}//md-netural,ios-第三个

    public void onCancle(){}

    /**
    * 提供给Input的回调
    * @param input1
    * @param input2
    */

    public void onGetInput(CharSequence input1,CharSequence input2){

    }

    /**
    * 提供给MdSingleChoose的回调
    * @param chosen
    * @param chosenTxt
    */

    public void onGetChoose(int chosen,CharSequence chosenTxt){

    }

    /**
    * 提供给MdMultiChoose的回调
    * @param states
    */

    public void onChoosen( List selectedIndex, List selectedStrs,boolean[] states){

    }

    MyItemDialogListener

     /**
    * IosSingleChoose,BottomItemDialog的点击条目回调
    * @param text
    * @param position
    */

    public abstract void onItemClick(CharSequence text, int position);


    /**
    * BottomItemDialog的底部按钮(经常是取消)的点击回调
    */

    public void onBottomBtnClick(){}


    最后必须调用show(),返回dialog对象

    对话框的消失

    StyledDialog.dismiss(DialogInterface... dialogs);

    两个loading对话框不需要对象就可以直接dismisss:

    StyledDialog.dismissLoading();

    progress dialog 的进度更新

    /**
    * 可以在任何线程调用
    * @param dialog 传入show方法返回的对象
    * @param progress
    * @param max
    * @param msg 如果是转圈圈,会将msg变成msg:78%的形式.如果是水平,msg不起作用
    * @param isHorizontal 是水平线状,还是转圈圈
    */

    public static void updateProgress( Dialog dialog, int progress, int max, CharSequence msg, boolean isHorizontal)


    代码下载: DialogUtil-master.zip

    原文链接:https://github.com/hss01248/DialogUtil

    收起阅读 »