Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

(热更新技术)高效率Hybird移动应用开发过程解决方案

$
0
0

这里写图片描述

前言

作为一名移动应用开发者而言快速高效进行版本测试,是至关重要的,所以一直在探索一个解决方案,可以随时更新我们的逻辑代码,今天我们就来看一下,我是如何在项目中进行应用的。

热更新

这个名词很早就听说过,只不过一直都没有一个明确的定义,也没有过多的机会去尝试,但是最近遇到过一个需求,尝试了一下, 所谓热更新就是在不需要重新安装的情况,升级和测试我们的app, 这个理念多被应用在混合和驱动原生型的应用当中。

实践项目技术背景

现有的项目采用cordova+H5进行项目架构,所以所谓的热更新一定要基于Cordova才可以生效, Cordova也是当前最为热门的混合移动应用的解决方案,性能上相比之前版本已经有了很大提升,但是所耗费的内存还是较为突出,整体评价还是非常不错的。

解决方案

此方案应用了一个开源的项目,这是一款非常不错的混合应用插件,项目地址,分享给大家,可以自行folk查看,也非常感谢这位作者,给我们开发提供这款工具,这里大概说一下原理:

这里写图片描述

会在本地www文件搭建一个测试服务,负责实时更新www下的代码到实际设备中,这里应用到了ngrok等代理服务器。

多的不说看看实施流程:

创建一个空的cordova项目

cordova create HotUpdate com.delawareconsulting.hot-update HotUpdate

//添加相应的测试平台
cordova platform add android
cordova platform add ios

安装插件

cordova plugin add cordova-hot-code-push-plugin

安装本地push文件的插件

cordova plugin add cordova-hot-code-push-local-dev-addon

安装热更新本地客户端Module

npm install -g cordova-hot-code-push-cli

启动本地server服务

cordova-hcp server

会看到如下内容:

这里写图片描述

运行真实设备

说到底我们主要是为了实时测试真机的效果,所以我们需要启动我们的模拟设备

一种已启动方式是通过命令:

cordova run ios

或者通过xcode和android studio 运行。

个人测试了一下,这个过程等待大概4秒左右才会刷新。 效果还是还是可以的,总比我们在运行次要快很多。

尝试一下吧!

作者:jiangbo_phd 发表于2016/9/26 16:54:15 原文链接
阅读:40 评论:0 查看评论

驱动原生型移动应用的跨平台分析与见解(个人观点)

$
0
0

这里写图片描述

前言

做移动互联网的这些年,从事过Android, IOS,混合应用开发,应用过Wex5, Cordova等平台框架,对于这些平台还都有一定了解,但是驱动原生型移动应用开发理念还是第一次听说, 通过MDCC 2016大会,对这个一概念有了更深一层次的认识,了解了Hybird App和驱动原生型应用的区别所在,为此也在这里讲述一下我再驱动型开发的实践经验和个人见解!

驱动原生型应用概念

混合应用

1.主要业务逻辑和UI采用html和javascript的形式编写
2.页面渲染主要应用webkit来做,我们看到的页面和浏览器中的是一个形态。
3.项目中主要用原生的webview做容器进行打包编译,不需要转换成二进制文件。

驱动原生

1.它不是Hybird, 因为并不使用webkit做UI渲染,但却使用webkit的api.
2.它并不是原生应用,因为它不会把JS,html代码转换成二进制文件
3.这是一种实时于原生系统交互,让操作系统提供原生的UI组件的一种开发技术
4.伴随React-Native等一系列框架的发布,这已经成为一种热门。

最近微信刚刚更新了一个微信小应用,已经刷爆朋友圈,着同样也是这个原理,等正式版本发布,我们在做一个尝试,目前较为看好的平台有:

  1. Weex移动开发平台(阿里巴巴)

  2. React Native开发平台(Facebook)

个人分析

目前来看,跨平台的开发模式,已经越来越被人们所关注,说到这里是不是有人认为原生开发者就没有活路了呢?

我任何原生开发自然有好处,其良好的用户体验,在特定的项目背景下非常有必要,比如游戏,比如频繁使用传感器,大数据量的处理,等等。

但是对于大多数业务场景来说,混合占据的优势更大些。

未来的发展不是谁死谁活的问题,而是一个相互融合的过程,驱动原生就是这个趋势最完美的解释。

作者:jiangbo_phd 发表于2016/9/26 17:42:41 原文链接
阅读:19 评论:0 查看评论

OC-布局Layout、CoreAnimation层面的动画-CALayer&CAAnimation

$
0
0

CoreAnimation层面的动画

CALayer层(显示的基础)

  • UIView核心显示功能就是依靠CALayer实现

UIView和CALayer的关系

  • 1.UIView显示能力是依赖底层的CALayer实现的,每一个UIView都包换一个CALayer对对象,修改CALayer,会影响表现出来的UIView的外观
  • 2.UIView与CALayer最大的不同在于,layer是不能够响应用户事件,UIView可以响应用户事件

如何获取UIView底层的那个CALayer对象

  • 通过.layer属性获取

可以使用CALayer做哪些操作?

常用属性

  • borderColor边框颜色
  • borderWidth边框宽度
  • cornerRadius圆角半径
  • shadowOpacity阴影透明度
  • shadowColor阴影颜色
  • shadowRadius阴影半径
  • shadowOffset阴影偏移量
  • masksToBounds是否按layer遮罩

与尺寸和位置相关的三个重要属性

  • bounds大小
  • position位置
  • anchorPosition锚点自身的参考点
- (void)viewDidLoad {
    [super viewDidLoad];
    //获取 view 中的 layer 对象
    CALayer *layer = self.redView.layer;
    //背景颜色
    layer.backgroundColor = [UIColor greenColor].CGColor;
    //设置边框宽度
    layer.borderWidth = 4;
    //设置边框颜色
    layer.borderColor = [UIColor redColor].CGColor;
    //设置圆角的 半径
//    layer.cornerRadius = self.redView.bounds.size.width * 0.5;
    //设置阴影 一定要先设置 阴影为不透明  默认是透明 0
    layer.shadowOpacity = 1;
    //设置阴影颜色
    layer.shadowColor = [UIColor blackColor].CGColor;
    //设置阴影的圆角
    layer.shadowRadius = 10;
    //设置阴影的 便宜量
    layer.shadowOffset = CGSizeMake(20, 20);


    //设置imageView的layer
    CALayer *imageViewLayer = self.imageView.layer;
    imageViewLayer.cornerRadius = self.imageView.bounds.size.height * 0.5;
    imageViewLayer.borderWidth = 5;
    imageViewLayer.borderColor = [UIColor lightGrayColor].CGColor;
    //要按照层的边缘进行遮罩
    imageViewLayer.masksToBounds = YES;


    //CALyer具有容器的特性  可以相互嵌套
    //创建CALayer对象
    CALayer *myLayer = [CALayer layer];
    myLayer.backgroundColor = [UIColor blueColor].CGColor;
//    myLayer.frame = CGRectMake(0, 0, 100, 100);

    //设置大小
    myLayer.bounds = CGRectMake(0, 0, 100, 100);
    //设置位置  当前layer 参考点(中心点)  在父layer坐标系中 的位置  默认是 0 0 点
//    myLayer.position = CGPointMake(50, 50);
    //设置当前 layer 参考点的位置  (锚点  默认 0.5 0.5)
//    myLayer.anchorPoint = CGPointZero;
    //设置 层的 旋转  (变形 是锚点操作的)
    myLayer.transform = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);


    //将自己创建的layer 添加到 self.redView的layer中
    [self.redView.layer addSublayer:myLayer];

}

创建新的Layer使用layer

  • 图片CALayer
    • 给contents属性赋值
  • 文字层CATextLayer
  • 图形层CAShapeLayer

图片Layer

-(CADisplayLink *)link {
    if (!_link) {
        //每刷新一次 调用一次 定时器的事件方法
        _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotation:)];
        //手动将定时器 加入到 事件循环队列中
        [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        _link.paused = YES;
    }
    return _link;
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.link.paused = !self.link.paused;
}

-(void)rotation:(CADisplayLink*)sender {
    // 5秒钟 转一圈
//    360 / 5; 求出1秒钟多少度  72
//    1秒钟调用 60次  转72度
//    每次调用 转  72/60.0
    //不能用角度 因为参数 需要的是 弧度
    self.imageView.layer.transform = CATransform3DRotate(self.imageView.layer.transform, angleToRadian(72/60.0), 1, 1, 1);
}


- (void)viewDidLoad {
    [super viewDidLoad];

    self.imageView.layer.cornerRadius = self.imageView.bounds.size.width * 0.5;
    self.imageView.layer.borderWidth = 3;
    self.imageView.layer.borderColor = [UIColor redColor].CGColor;
    self.imageView.layer.masksToBounds = YES;
    self.imageView.layer.anchorPoint = CGPointMake(0.2, 0.2);

    for (NSInteger i = 0; i < 8; i++) {
        CALayer *layer = [CALayer layer];
        //为层添加图片内容
        layer.contents = (id)[UIImage imageNamed:@"60"].CGImage;
        layer.bounds = CGRectMake(0, 0, 20, self.imageView.bounds.size.height * 0.5);
        layer.position = CGPointMake(self.imageView.bounds.size.width * 0.5, self.imageView.bounds.size.height * 0.5);
        layer.anchorPoint = CGPointMake(0.5, 1);
        layer.transform = CATransform3DMakeRotation(M_PI_4 * i, 0, 0, 1);
        [self.imageView.layer addSublayer:layer];
    }

}

文字及图形层

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建文字类型的图层
    CATextLayer *tLayer = [CATextLayer layer];
    tLayer.string = @"这是一个文字";
    tLayer.fontSize = 20;
    tLayer.foregroundColor = [UIColor whiteColor].CGColor;
    tLayer.backgroundColor = [UIColor blackColor].CGColor;

    tLayer.bounds = CGRectMake(0, 0, 200, 40);
    tLayer.position = CGPointMake(100, 100);
    tLayer.anchorPoint = CGPointZero;

    //将文字层 添加到 self.view.layer 上
    [self.view.layer addSublayer:tLayer];


    //图形类型的图层
    CAShapeLayer *sLayer = [CAShapeLayer layer];
    self.sLayer = sLayer;
    sLayer.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 10, 100, 50) cornerRadius:10].CGPath;
    sLayer.strokeColor = [UIColor redColor].CGColor;
    sLayer.fillColor = [UIColor whiteColor].CGColor;

    sLayer.backgroundColor = [UIColor blackColor].CGColor;

    sLayer.bounds = CGRectMake(0, 0, 200, 200);
    sLayer.position = CGPointMake(50, 250);
    sLayer.anchorPoint = CGPointZero;

    //添加边框
    sLayer.borderColor = [UIColor blueColor].CGColor;
    sLayer.borderWidth = 5;
    sLayer.cornerRadius = 10;

    //将图形层 添加到 self.view.Layer 上
    [self.view.layer addSublayer:sLayer];

}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    arc4random_uniform 取出 0 ~ x-1
    self.sLayer.borderWidth = arc4random_uniform(25);
    self.sLayer.cornerRadius = arc4random_uniform(30);

}

CALayer的很多属性都有隐式动画,在修改该属性时,会自动出现动画效果,可以通过查看头文件中,属性上面出现animatable这样的说明时,意味可以有隐式动画

CAAnimation动画

  • CA的动画,只能施加在CALayer上
  • CA动画与UIView动画最大的一个区别:
    • CA动画是假的,视图看着好像位置改变了,但其实没有变
    • UIView动画中,由于明确的设定了动画结束时视图的状态,所以视图的数据会随着动画的结束而真的改变

CAAnimation的子类之一

  • CABasicAnimation基础动画

CAAnimation的子类之二

  • CAAnimationGroup动画组

CAAnimation的子类之三

  • CAKeyFrameAnimation关键帧动画

CAAnimation

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//    [self position];
//    [self transform];
//    [self groupAnimation];
    [self keyFrameAnimation];
}

-(void)position {//位移动画
    CABasicAnimation *basicAnim = [CABasicAnimation animation];
    //位移position  缩放scale  旋转 rotation
    //使用KVC的方式为对象赋值,说明要改的属性名是什么
    basicAnim.keyPath = @"position";
    basicAnim.fromValue =  [NSValue valueWithCGPoint:CGPointMake(50, 50)];
    //toValue  是 到 50 300的位置上
    basicAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(50, 300)];
    //byValue  在自身基础上 移动 50 300
//    basicAnim.byValue = [NSValue valueWithCGPoint:CGPointMake(50, 300)];

    //动画结束时 不把动画 从视图上 移除  如果需要固定动画结束时视图的位置 必须 再配合 fillMode 该属性一起使用
    basicAnim.removedOnCompletion = NO;

    /*
     kCAFillModeForwards  当动画结束后,layer会一致保持着动画最后的状态
     kCAFillModeBackwards 在动画开始前,只需要将动画加入一个Layer,layer便立即进入动画的初始状态并等待运行动画
     kCAFillModeBoth 上面两种效果叠加在一起
     kCAFillModeRemoved (默认) 当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
     */
    basicAnim.fillMode = kCAFillModeBoth;
    //设置动画 开始前等待时间
    basicAnim.beginTime = CACurrentMediaTime() + 3;


    //设置动画的 相关属性
    //动画持续时间
    basicAnim.duration = 2;
    //动画重复次数   一直重复只是给以非常大的值
//    basicAnim.repeatCount = MAXFLOAT;
    basicAnim.repeatCount = 1;

    //运行动画 注意:CA动画只能使用 CALayer对象运行
    [self.imageView.layer addAnimation:basicAnim forKey:nil];
}

-(void)transform {//变形动画

    CABasicAnimation *transformAnim = [CABasicAnimation animation];

//    transformAnim.keyPath = @"transform";
//    transformAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.2, 1)];

    //动画只能执行 最后设置的 keyPath  ,如果需要同时执行只能依赖 CAAnimation 的另外一个子类 CAAnimationGrop
    transformAnim.keyPath = @"transform.scale";
    transformAnim.toValue = @1.2;

    transformAnim.keyPath = @"transform.rotation";
    transformAnim.toValue = @(M_PI * 2);

    transformAnim.duration = 1;
    transformAnim.repeatCount = MAXFLOAT;
    [self.imageView.layer addAnimation:transformAnim forKey:nil];
}

-(void)groupAnimation {

    CABasicAnimation *positionAnim = [CABasicAnimation animationWithKeyPath:@"position.y"];
    positionAnim.toValue = @(self.imageView.center.y + 200);

    CABasicAnimation *transformAnim = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
    transformAnim.toValue = @(M_PI);


    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[positionAnim, transformAnim];
    group.duration = 3;
    group.removedOnCompletion = NO;
    group.fillMode = kCAFillModeForwards;

    [self.imageView.layer addAnimation:group forKey:nil];
}

-(void)keyFrameAnimation {//关键帧动画
    CAKeyframeAnimation *keyFrameAnim = [CAKeyframeAnimation animation];
    keyFrameAnim.keyPath = @"position";
//    //设置中间行进的路线的关键值
//    keyFrameAnim.values = @[
//            [NSValue valueWithCGPoint:CGPointMake(50, 50)],
//            [NSValue valueWithCGPoint:CGPointMake(200, 200)],
//            [NSValue valueWithCGPoint:CGPointMake(60, 300)],
//            [NSValue valueWithCGPoint:CGPointMake(150, 80)]];


    keyFrameAnim.path = [UIBezierPath bezierPathWithRect:CGRectMake(80, 80, 200, 300)].CGPath;


    keyFrameAnim.duration = 3;
    keyFrameAnim.removedOnCompletion = NO;
    keyFrameAnim.fillMode = kCAFillModeForwards;

    [self.imageView.layer addAnimation:keyFrameAnim forKey:nil];
}

2.布局Layout

什么是布局

  • 是指在一个视图中,如何摆放它的子视图(设置子视图的位置和大小)

可能导致屏幕尺寸大小发生变化的原因

  • a.屏幕方向(横竖屏)
  • b.设备不同(3.5寸,4寸,4.7寸,5.5寸)
  • c.状态栏
    • 隐藏
    • 特殊的状态栏
      – 来电时绿色的状态栏
      – 开启个人热点蓝色的状态栏
      – 录音时红色的状态栏
  • d.各种bar
    • NaviationBar:竖屏时64点高横屏时52个点高
    • ToolBar:44/32个点
    • TabBar:49个点
  • e.键盘弹起和收回

如何布局?

  • 方法一:纯代码布局(古老的方法)
  • 理念:当屏幕发生变化时,自动执行一段代码,在代码中根据新的屏幕大小重新计算各个视图的frame,从而达到重新定位的目的
  • 特点:功能强大,非常繁琐
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *myView1;
@property (weak, nonatomic) IBOutlet UIView *myView2;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIView *greenView1;
@property (weak, nonatomic) IBOutlet UIView *greenView2;
@property (weak, nonatomic) IBOutlet UIView *greenView3;

@end

@implementation ViewController

//该方法在view加载完会调用一次, 在屏幕发生旋转也会自动调用
-(void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    CGFloat space20 = 20;
    CGFloat space10 = 10;
    CGFloat viewWidth = (self.view.bounds.size.width - space20 * 2 - space10) * 0.5;
    CGFloat viewHeight = 40;
    CGFloat viewX = space20;
    CGFloat viewY = space20;

    CGFloat greenViewWidth = 20;
    CGFloat greenViewHeight = 20;

    CGRect frame = CGRectMake(viewX, viewY, viewWidth, viewHeight);
    self.myView1.frame = frame;

    //设置 第二个view的frame
    frame.origin.x += viewWidth + space10;
    self.myView2.frame = frame;

    //设置imageview的frame
    frame.origin.x = space20;
    frame.origin.y += viewHeight + space10;
    frame.size.width = self.view.bounds.size.width - space20 * 2;
    frame.size.height = self.view.bounds.size.height - space20 * 2 - space10 * 2 - greenViewHeight - viewHeight;
    self.imageView.frame = frame;

    //设置小绿view1的frame
    frame.origin.x = self.view.bounds.size.width - (space20 + greenViewWidth);
    frame.origin.y = self.view.bounds.size.height - space20 - greenViewHeight;
    frame.size.width = greenViewWidth;
    frame.size.height = greenViewHeight;
    self.greenView1.frame = frame;
    //设置小绿view2的frame
    frame.origin.x -= greenViewWidth + space10;
    self.greenView2.frame = frame;
    //设置小绿view3的frame
    frame.origin.x -= greenViewWidth + space10;
    self.greenView3.frame = frame;


}
作者:shuan9999 发表于2016/9/26 19:44:35 原文链接
阅读:206 评论:0 查看评论

Android之浅析回调

$
0
0

      初次用到回调是在Fragment和Activity之间进行通信的时候,当时感觉很难理解,但又觉得这个方法使用起来很方便,所以对它进行仔细的研究。发现回调不仅仅是实现功能那么简单,它还可以把几个相似的功能用简单的几句代码来实现。所以在编程中使用回调可增加码农们的效率。

我自己总结出了回调的大致四个步骤:

1.在需要调用的文件里写一个接口和一个接口方法。注意,这里的权限都是公共的。

2.在被调用的文件里实现这个接口和这个接口的方法。在这个方法里写我们的需求,也就是所需要的功能。

3.在需要调用的文件里写静态方法和一个接口类型的静态变量,在方法里使被调用的文件转化为此接口类型的变量。

4.在被调用的文件里调用第3步的静态方法,使两文件关联起来。

5.调用接口方法(静态变量.接口方法)。

简而言之:回调就是使用实现接口的接口方法来操作本类文件。

下面这是一个简单的示例:

我们需要在A画面实现文本框文字的改变,所以我们可以在另一个B画面里创建接口,操作A画面的文本文字。

A画面:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. <span style="font-size:18px;">public class CallBack_AActivity extends AppCompatActivity  
  2.      implements CallBack_BActivity.callBack{  
  3.     private TextView tv;  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_call_back__a);  
  8.   
  9.         CallBack_BActivity.setCallBack(this);  
  10.   
  11.         tv = (TextView) findViewById(R.id.tv);  
  12.         tv.setClickable(true);  
  13.         tv.setOnClickListener(new View.OnClickListener() {  
  14.             @Override  
  15.             public void onClick(View view) {  
  16.                 Intent intent = new Intent(getBaseContext(),CallBack_BActivity.class);  
  17.                 startActivity(intent);  
  18.             }  
  19.         });  
  20.     }  
  21.   
  22.     @Override  
  23.     public void changeText(String result) {  
  24.         tv.setText(result);  
  25.     }  
  26.   
  27. }</span>  

B画面:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class CallBack_BActivity extends AppCompatActivity {  
  2.     private static callBack back;  
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_call_back__b);  
  7.     }  
  8.     public void click(View view){  
  9.         back.changeText("这是一个回调");  
  10.         finish();  
  11.     }  
  12.     public interface callBack{  
  13.         public void changeText(String result);  
  14.     }  
  15.     public static void setCallBack(Context context){  
  16.         back = (callBack) context;  
  17.     }  
  18. }  
  19. /* 
  20. * 1.在本界面定义接口 
  21. * 2.在另一个界面实现此接口 
  22. * 3.本界面实现方法 
  23. * 4.另一个界面关联。 
  24. * */  

截图:

1.第一次进入A画面:


2.点击文本框进入B画面


3.点击按钮返回A画面



作者:woainijinying 发表于2016/9/26 22:32:01 原文链接
阅读:193 评论:1 查看评论

Android之视频播放以及亮度音量变换

$
0
0

程序实现功能:
播放视频,右侧上下滑动改变亮度,左侧上下滑动改变音量。
现在我们来记住几句重要的属性
清单文件,强制横屏
android:screenOrientation=”landscape”
不会重新执行Oncreat
android:configChanges=”orientation|keyboard”
android:configChanges=”orientation|keyboard|screenSize
它会走onConfigurationChanged
VideoView中:android:foregroundGravity=”center”
下面是代码啦

public class VideoActivity extends Activity {
private VideoView vv;
    private int k;
    //手势
    private GestureDetector gestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_video);
        vv = (VideoView) findViewById(R.id.vv);
        MediaController mediaController=new MediaController(this);
        vv.setMediaController(mediaController);
        mediaController.setMediaPlayer(vv);
        Intent intent =getIntent();
        String url=intent.getStringExtra("playurl");
        vv.setVideoPath(url);
        vv.requestFocus();
        vv.start();
        //返回上一页
        vv.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                finish();
            }
        });
        //据说并不好用,需要和onTouchEvent配合使用
        setGestureDetector();
    }
//触摸(触摸事件)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
//        //手指抬起
//        if(event.getAction()==MotionEvent.ACTION_UP){
//            Log.d("==g==","up");
//        }
//        //手指按下
//        if(event.getAction() == MotionEvent.ACTION_DOWN){
//            Log.d("==g==","down");
//            Log.d("==g==", "down" + event.getX());
//            Log.d("==g==", "down" + event.getY());
//
//        }
//        //手指移动
//        if(event.getAction() == MotionEvent.ACTION_MOVE){
//            Log.d("==g==","move");
//        }
//类的监听,完成解析
        return gestureDetector.onTouchEvent(event);
    }
    //手势方法
    public void setGestureDetector(){
        gestureDetector =new GestureDetector(
                new GestureDetector.OnGestureListener() {
                    @Override
                    public boolean onDown(MotionEvent e) {
                        return false;
                    }

                    @Override
                    public void onShowPress(MotionEvent e) {

                    }

                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                        return false;
                    }
//滚动MotionEvent  e1 1.手势起点事件
// MotionEvent e2 2.手势终点
//distanceX 3.x轴方向上移动的速度/每秒
// distanceY 4.Y轴方向上移动的速度/每秒

                    @Override
                    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                        if(e1.getY()-e2.getY()>0.5&&Math.abs(distanceY)>0.5){
                           if(e1.getX()>500){
                               setLightness(30);
                           } else{
                               setAudio(1);
                           }
                        }
                        if(e1.getY()-e2.getY()<0.5&&Math.abs(distanceY)>0.5){
                            Log.d("==g==", e1.getY() + "");
                            if(e1.getX()>500){
                                setLightness(-30);
                            }else{
                                setAudio(-1);
                            }
                        }
                        return true;
                    }

                    @Override
                    public void onLongPress(MotionEvent e) {

                    }
//抛的动作
                    @Override
                    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                        return false;
                    }
                }
        );
    }
//物理键按下
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return super.onKeyDown(keyCode, event);
    }
//物理键抬起
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return super.onKeyUp(keyCode, event);
    }

    //轨迹球
    @Override
    public boolean onTrackballEvent(MotionEvent event) {
        return super.onTrackballEvent(event);
    }


    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        Log.d("==d==", ""+newConfig.orientation);
    }
//改变屏幕亮度
    public void setLightness(float lightness){
        WindowManager.LayoutParams layoutParams =getWindow().getAttributes();
        //屏幕的亮度,最大是255
        layoutParams.screenBrightness =layoutParams.screenBrightness+lightness/255f;
        if(layoutParams.screenBrightness>1){
            layoutParams.screenBrightness=1;
        }else if(layoutParams.screenBrightness<0.2){
            layoutParams.screenBrightness=0.2f;
        }
        getWindow().setAttributes(layoutParams);
    }
//加减音量
    public void setAudio(int volume){
        AudioManager audioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
        //当前音量
       k = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        //最大音量
        int max =audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        Log.d("==d==", "" + max);
        Log.d("==d==", "" + k);
        k=k+volume;
        if(k>=0&&k<=max){
            audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,k,AudioManager.FLAG_PLAY_SOUND);
        }else {
            return;
        }


        //audioManager.adjustVolume(i+volume,AudioManager.FLAG_PLAY_SOUND);

    }
}

下面是布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.edu.jereh.musicapp.VideoActivity"
    android:gravity="center"
    android:background="#000">
    <VideoView
        android:id="@+id/vv"
        android:foregroundGravity="center"
        android:keepScreenOn="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
</RelativeLayout>

视频截图

这里写图片描述

作者:woainijinying 发表于2016/9/26 22:57:12 原文链接
阅读:178 评论:1 查看评论

Cocos2dx-3.x 中CCCamera相机类详解及源码分析

$
0
0

Cocos2d-x 3.3版本中加入了相机这个类,该类在3D游戏中是必不可少的,在3D立体游戏中,往往需要视野角度的变化,通过相机的变换才能观察和体验整个游戏世界。

CCCamera类基本使用

在游戏中一般有两种类型的相机:一种是透视相机,它在3D游戏中十分常见;另一种是正交相机,它没有透视相机的近大远小的效果而是相机内任何位置的物体大小比例都是一样的。
透视相机原理图
上图是透视相机的原理图,一般来说,我们通过以下代码创建:

_camera = Camera::createPerspective(60, (GLfloat)s.width/s.height, 1, 1000);

上面的代码就是创建了一个透视投影的相机,下面我来说明下参数的意义:第一个参数是FOV,即视场角(field of view),它可以理解为你的视线左右能看多宽(以角度计)第二个就是上述所有的宽高比,最后两个是相机的近裁面和远裁面,这个也很好理解,距离相机比近裁面还要近的,比远裁面还要远的,都不会被渲染到。

正交相机原理图
上图是正交相机的原理图,一般来说,我们通过以下代码创建:

_camera = Camera::createOrthographic(s.width, s.height, 1,1000);

上面的代码就是创建了一个正交投影的相机,下面我来说明下参数的意义:第一个参数是相机的宽度,第二个就是相机的高度,最后两个是相机的近裁面和远裁面,这个也很好理解,距离相机比近裁面还要近的,比远裁面还要远的,都不会被渲染到。这个和透视相机是一样的。

接下来,我们需要对相机设置一个标记位(FLAG),这样可以让相机与其他的相机区分开来–在一些游戏的应用中,通常不仅仅只有一个相机,如果有多个相机的话,那么我们要标记一个物体,到底是要被哪一个相机所”看到”,这时候,我们就需要设置它的CameraMask来与相机的Flag对应:

_layer3D->setCameraMask(2);
_camera->setCameraFlag(CameraFlag::USER1);

这样_layer3D就能被_camera看到。
注意到Camera中有个_cameraFlag属性,为枚举类型,定义如下:

enum class CameraFlag
{
    DEFAULT = 1,
    USER1 = 1 << 1,
    USER2 = 1 << 2,
    USER3 = 1 << 3,
    USER4 = 1 << 4,
    USER5 = 1 << 5,
    USER6 = 1 << 6,
    USER7 = 1 << 7,
    USER8 = 1 << 8,
};

Node中有个_cameraMask的属性,当相机的_cameraFlag & _cameraMask为true时,该Node可以被该相机看到。所以在上述相机的创建代码中,camera的CameraFlag设置为CameraFlag::USER1,并且该layer的CameraMask为2,则表示该layer只能被CameraFlag::USER1相机看到。如果你设置的精灵的cameraMask是3的话,它也是能被cameraFlag为CameraFlag::USER1和CameraFlag::USER2的相机看到的。我们还要注意如果你的精灵是在layer设置cameraMask之后添加的,它是不会被看到的,还需要手动再设置精灵的cameraMask。不要以为这样就可以了,最后我们还要把相机添加到场景中,不然我们还是看不到效果的,一定要记住呀,下图就是把相机加到场景中的代码:

_layer3D->addChild(_camera);

这样,通过设置相机的位置,角度等参数就能实现不同视角观察游戏世界。

CCCamera类源码分析

下面是CCCamera.h文件内容:

#ifndef _CCCAMERA_H__
#define _CCCAMERA_H__

#include "2d/CCNode.h"
#include "3d/CCFrustum.h"
#include "renderer/CCQuadCommand.h"
#include "renderer/CCCustomCommand.h"
#include "renderer/CCFrameBuffer.h"

NS_CC_BEGIN

class Scene;
class CameraBackgroundBrush;

/**
相机标识,每个Node中有个_cameraMask的属性,当相机的_cameraFlag & _cameraMask为true时,该Node可以被该相机看到。
*/

enum class CameraFlag
{
    DEFAULT = 1,
    USER1 = 1 << 1,
    USER2 = 1 << 2,
    USER3 = 1 << 3,
    USER4 = 1 << 4,
    USER5 = 1 << 5,
    USER6 = 1 << 6,
    USER7 = 1 << 7,
    USER8 = 1 << 8,
};
/**
定义一个相机类,该类继承于Node。
*/
class CC_DLL Camera :public Node
{
    /**
    友元类有场景类,导演类以及事件分发类。
    */
    friend class Scene;
    friend class Director;
    friend class EventDispatcher;
public:
    ;/**
    枚举类标记:透视相机和正交相机。
    */
    enum class Type
    {
        PERSPECTIVE = 1,
        ORTHOGRAPHIC = 2
    };
public:

    ;/**
    创建一个透视相机。
    参数:
    fieldOfView 透视相机的可视角度 (一般是在40-60度之间).
    aspectRatio 相机的长宽比(通常会使用视窗的宽度除以视窗的高度)。
    nearPlane   近平面的距离。
    farPlane    远平面的距离。  
    */
    static Camera* createPerspective(float fieldOfView, float aspectRatio, float nearPlane, float farPlane);
    /**
    创建一个正交相机。
    参数:
    zoomX   沿x轴的正交投影的缩放因子(正交投影的宽度)。
    zoomY   沿y轴的正交投影的缩放因子(正交投影的高度)。
    nearPlane   近平面的距离。
    farPlane    远平面的距离。
    */
    static Camera* createOrthographic(float zoomX, float zoomY, float nearPlane, float farPlane);

    /** 
    创建默认的相机,相机的类型取决于Director::getProjection,默认的相机深度是0  
    */
    static Camera* create();

    /**
    获取相机类型。
    */
    Camera::Type getType() const { return _type; }

    /**
    获取和设置相机标识。类型为枚举类和无符号短整型。
    */
    CameraFlag getCameraFlag() const { return (CameraFlag)_cameraFlag; }
    void setCameraFlag(CameraFlag flag) { _cameraFlag = (unsigned short)flag; }

    /**
    使相机看着目标
    参数:
    target  目标的位置
    up  相机向上的向量,通常这是Y轴 
    */
    virtual void lookAt(const Vec3& target, const Vec3& up = Vec3::UNIT_Y);

    /**
    获取相机的投影矩阵。
    返回:
    相机投影矩阵。 
    */
    const Mat4& getProjectionMatrix() const;
    /**
    获取相机的视图矩阵。
    返回:
    相机视图矩阵。 
    */
    const Mat4& getViewMatrix() const;

    /**
    得到视图投影矩阵。
    */
    const Mat4& getViewProjectionMatrix() const;

    /*
    把指定坐标点从世界坐标转换为屏幕坐标。 原点在GL屏幕坐标系的左下角。
    参数:
    src 世界的位置。
    返回:
    屏幕的位置。 
     */
    Vec2 project(const Vec3& src) const;

    /*
    把指定坐标点从3D世界坐标转换为屏幕坐标。 原点在GL屏幕坐标系的左下角。
    参数:
    src 3D世界的位置。
    返回:
    GL屏幕空间的位置。 
     */
    Vec2 projectGL(const Vec3& src) const;

    /**
    把指定坐标点从屏幕坐标转换为世界坐标。 原点在GL屏幕坐标系的左下角。
    参数:
    src 屏幕的位置。
    返回:
    世界的位置。 
     */
    Vec3 unproject(const Vec3& src) const;

    /**
    把指定坐标点从屏幕坐标转换为3D世界坐标。 原点在GL屏幕坐标系的左下角。
    参数
    src GL屏幕空间的位置。
    返回
    3D世界的位置。 
     */
    Vec3 unprojectGL(const Vec3& src) const;

    /**
    把指定坐标点从屏幕坐标转换为世界坐标。 原点在GL屏幕坐标系的左下角。
    参数
    size    使用的视窗大小。
    src 屏幕的位置。
    dst 世界的位置。 
     */
    void unproject(const Size& size, const Vec3* src, Vec3* dst) const;

    /**
    把指定坐标点从屏幕坐标转换为3D世界坐标。 原点在GL屏幕坐标系的左下角。
    参数:
    size    使用的窗口大小。
    src GL屏幕空间的位置。
    dst 3D世界的位置。
     */
    void unprojectGL(const Size& size, const Vec3* src, Vec3* dst) const;

    /**
     aabb在视椎体内是否可见
     */
    bool isVisibleInFrustum(const AABB* aabb) const;

    /**
     获取朝向相机的物体深度。
     */
    float getDepthInView(const Mat4& transform) const;

    /**
    设置深度,相比深度小的,深度较大的相机会绘制在顶端,标识是CameraFlag::DEFAULT的相机深度是0,用户定义的相机深度默认为-1 
     */
    void setDepth(int8_t depth);

    /**
    获取深度,相比深度小的,深度较大的相机会绘制在顶端,标识是CameraFlag::DEFAULT的相机深度是0,用户定义的相机深度默认为-1 
     */
    int8_t getDepth() const { return _depth; }

    /**
    获取渲染顺序。
     */
    int getRenderOrder() const;

    /**
    获取视椎体远平面。
     */
    float getFarPlane() const { return _farPlane; }

    /**
    获取视椎体近平面。
     */
    float getNearPlane() const { return _nearPlane; }

    //复写
    virtual void onEnter() override;
    virtual void onExit() override;

    /**
    获取绘制的相机,绘制的相机会在Scene::render中设置。 
     */
    static const Camera* getVisitingCamera() { return _visitingCamera; }

    /**
     获取到当前运行场景的默认相机。
     */
    static Camera* getDefaultCamera();
    /**
    在相机渲染所属的场景前,需要对背景进行清除。它以默认的深度值清除缓存,可以通过setBackgroundBrush 函数获取深度值。
     */
    void clearBackground();
    /**
     应用帧缓冲,渲染目标和视图。
     */
    void apply();
    /**
    设置帧缓冲,从中可以获取到一些需要渲染的目标。
    */
    void setFrameBufferObject(experimental::FrameBuffer* fbo);
    /**
     设置相机视口。
     */
    void setViewport(const experimental::Viewport& vp) { _viewport = vp; }

    /**
    视图矩阵是否在上一帧被更新。
     */
    bool isViewProjectionUpdated() const {return _viewProjectionUpdated;}

    /**
    设置背景刷,通过CameraBackgroundBrush 查看更多详情。
     */
    void setBackgroundBrush(CameraBackgroundBrush* clearBrush);

    /**
    获取背景刷。
    */
    CameraBackgroundBrush* getBackgroundBrush() const { return _clearBrush; }
    /**
    遍历所有子节点,并且循环递归得发送它们的渲染指令。
    参数:
    renderer    指定一个渲染器
    parentTransform 父节点放射变换矩阵
    parentFlags 渲染器标签
    重载 Node .
    */
    virtual void visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags) override;

CC_CONSTRUCTOR_ACCESS:
    Camera();
    ~Camera();

    /**
    设置场景,这个方法不应该手动调用 .
     */
    void setScene(Scene* scene);

    /**
    对投影矩阵设置额外的矩阵,在WP8平台使用时,调用的时候它会乘以投影矩阵
    */
    void setAdditionalProjection(const Mat4& mat);

    /**
    初始化相机。包括透视相机和正交相机。
    */
    bool initDefault();
    bool initPerspective(float fieldOfView, float aspectRatio, float nearPlane, float farPlane);
    bool initOrthographic(float zoomX, float zoomY, float nearPlane, float farPlane);
    void applyFrameBufferObject();
    void applyViewport();
protected:

    Scene* _scene; //相机所属的场景。
    Mat4 _projection;
    mutable Mat4 _view;
    mutable Mat4 _viewInv;
    mutable Mat4 _viewProjection;
    Vec3 _up;
    Camera::Type _type;
    float _fieldOfView;
    float _zoom[2];
    float _aspectRatio;
    float _nearPlane;
    float _farPlane;
    mutable bool  _viewProjectionDirty;
    bool _viewProjectionUpdated; //视图矩阵是否在上一帧中被更新。
    unsigned short _cameraFlag; // 相机标识
    mutable Frustum _frustum;   // 相机投影平面
    mutable bool _frustumDirty;
    int8_t  _depth;                 //相机深度
    static Camera* _visitingCamera;

    CameraBackgroundBrush* _clearBrush; 

    experimental::Viewport _viewport;

    experimental::FrameBuffer* _fbo;
protected:
    static experimental::Viewport _defaultViewport;
public:
    static const experimental::Viewport& getDefaultViewport() { return _defaultViewport; }
    static void setDefaultViewport(const experimental::Viewport& vp) { _defaultViewport = vp; }
};

NS_CC_END

#endif// __CCCAMERA_H_

好的,我们大致了解了相机类中有哪些方法,接下来我们看具体实现。

下面是CCCamera.cpp的内容:

#include "2d/CCCamera.h"
#include "2d/CCCameraBackgroundBrush.h"
#include "base/CCDirector.h"
#include "platform/CCGLView.h"
#include "2d/CCScene.h"
#include "renderer/CCRenderer.h"
#include "renderer/CCQuadCommand.h"
#include "renderer/CCGLProgramCache.h"
#include "renderer/ccGLStateCache.h"
#include "renderer/CCFrameBuffer.h"
#include "renderer/CCRenderState.h"

NS_CC_BEGIN


Camera* Camera::_visitingCamera = nullptr;
experimental::Viewport Camera::_defaultViewport;

Camera* Camera::getDefaultCamera()
{
    //获取当前场景,并返回默认相机。
    auto scene = Director::getInstance()->getRunningScene();
    if(scene)
    {
        return scene->getDefaultCamera();
    }

    return nullptr;
}

Camera* Camera::create()
{
    //创建一个相机,根据投影类型初始化相机为透视相机或者正交相机。
    Camera* camera = new (std::nothrow) Camera();
    camera->initDefault();
    //设置回收释放及相机深度。
    camera->autorelease();
    camera->setDepth(0.f);

    return camera;
}

Camera* Camera::createPerspective(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
{
    auto ret = new (std::nothrow) Camera();
    if (ret)
    {
        //初始化透视相机。
        ret->initPerspective(fieldOfView, aspectRatio, nearPlane, farPlane);
        ret->autorelease();
        return ret;
    }
    CC_SAFE_DELETE(ret);
    return nullptr;
}

Camera* Camera::createOrthographic(float zoomX, float zoomY, float nearPlane, float farPlane)
{
    auto ret = new (std::nothrow) Camera();
    if (ret)
    {
        //初始化正交相机。
        ret->initOrthographic(zoomX, zoomY, nearPlane, farPlane);
        ret->autorelease();
        return ret;
    }
    CC_SAFE_DELETE(ret);
    return nullptr;
}
//构造函数,初始化参数列表。
Camera::Camera()
: _scene(nullptr)
, _viewProjectionDirty(true)
, _cameraFlag(1)
, _frustumDirty(true)
, _depth(-1)
, _fbo(nullptr)
{
    _frustum.setClipZ(true);
    _clearBrush = CameraBackgroundBrush::createDepthBrush(1.f);
    _clearBrush->retain();
}

Camera::~Camera()
{
    //释放帧缓存和背景刷。
    CC_SAFE_RELEASE_NULL(_fbo);
    CC_SAFE_RELEASE(_clearBrush);
}

const Mat4& Camera::getProjectionMatrix() const
{
    return _projection;
}
const Mat4& Camera::getViewMatrix() const
{
    Mat4 viewInv(getNodeToWorldTransform());
    static int count = sizeof(float) * 16;
    if (memcmp(viewInv.m, _viewInv.m, count) != 0)
    {
        _viewProjectionDirty = true;
        _frustumDirty = true;
        _viewInv = viewInv;
        _view = viewInv.getInversed();
    }
    return _view;
}
void Camera::lookAt(const Vec3& lookAtPos, const Vec3& up)
{
    //camera->lookAt必须在camera->setPostion3D之后,因为其在运行过程中调用了getPosition3D()

    //定义y方向的归一化向量。
    Vec3 upv = up;
    upv.normalize();
    //计算x、y、z、方向上的向量。
    Vec3 zaxis;
    Vec3::subtract(this->getPosition3D(), lookAtPos, &zaxis);
    zaxis.normalize();

    Vec3 xaxis;
    Vec3::cross(upv, zaxis, &xaxis);
    xaxis.normalize();

    Vec3 yaxis;
    Vec3::cross(zaxis, xaxis, &yaxis);
    yaxis.normalize();
    //将上面计算的向量值构造旋转矩阵。
    Mat4  rotation;
    rotation.m[0] = xaxis.x;
    rotation.m[1] = xaxis.y;
    rotation.m[2] = xaxis.z;
    rotation.m[3] = 0;

    rotation.m[4] = yaxis.x;
    rotation.m[5] = yaxis.y;
    rotation.m[6] = yaxis.z;
    rotation.m[7] = 0;
    rotation.m[8] = zaxis.x;
    rotation.m[9] = zaxis.y;
    rotation.m[10] = zaxis.z;
    rotation.m[11] = 0;
    /*
    定义四元数,将旋转矩阵转换为四元数。
    通过四元数来设置3D空间中的旋转角度。要保证四元数是经过归一化的。
    */
    Quaternion  quaternion;
    Quaternion::createFromRotationMatrix(rotation,&quaternion);
    quaternion.normalize();
    setRotationQuat(quaternion);
}

const Mat4& Camera::getViewProjectionMatrix() const
{
    //获取视图矩阵
    getViewMatrix();
    if (_viewProjectionDirty)
    {
    //设置标记视图投影矩阵已更新
        _viewProjectionDirty = false;
        //投影矩阵和视图矩阵相乘得到视图投影矩阵。
        Mat4::multiply(_projection, _view, &_viewProjection);
    }

    return _viewProjection;
}

void Camera::setAdditionalProjection(const Mat4& mat)
{
    //可以在原基础投影上做投影
    _projection = mat * _projection;
    getViewProjectionMatrix();
}

bool Camera::initDefault()
{
    auto size = Director::getInstance()->getWinSize();
    //默认相机的初始化根据投影类型创建。
    auto projection = Director::getInstance()->getProjection();
    switch (projection)
    {
        case Director::Projection::_2D:
        {
            initOrthographic(size.width, size.height, -1024, 1024);
            setPosition3D(Vec3(0.0f, 0.0f, 0.0f));
            setRotation3D(Vec3(0.f, 0.f, 0.f));
            break;
        }
        case Director::Projection::_3D:
        {
            /*
            zeye的大小:_winSizeInPoints.height(设计分辨率的高) / 1.1566f
            1.1566约等于2/1.732,这个值正是使相机60度角视角正好覆盖屏幕分辨率高的值。
            */
            float zeye = Director::getInstance()->getZEye();
            initPerspective(60, (GLfloat)size.width / size.height, 10, zeye + size.height / 2.0f);
            Vec3 eye(size.width/2, size.height/2.0f, zeye), center(size.width/2, size.height/2, 0.0f), up(0.0f, 1.0f, 0.0f);
            setPosition3D(eye);
            lookAt(center, up);
            break;
        }
        default:
            CCLOG("unrecognized projection");
            break;
    }
    return true;
}

bool Camera::initPerspective(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
{
    _fieldOfView = fieldOfView;
    _aspectRatio = aspectRatio;
    _nearPlane = nearPlane;
    _farPlane = farPlane;
    //新建透视相机投影矩阵
    Mat4::createPerspective(_fieldOfView, _aspectRatio, _nearPlane, _farPlane, &_projection);
    _viewProjectionDirty = true;
    _frustumDirty = true;

    return true;
}

bool Camera::initOrthographic(float zoomX, float zoomY, float nearPlane, float farPlane)
{
    _zoom[0] = zoomX;
    _zoom[1] = zoomY;
    _nearPlane = nearPlane;
    _farPlane = farPlane;
    //新建正交相机投影矩阵
    Mat4::createOrthographicOffCenter(0, _zoom[0], 0, _zoom[1], _nearPlane, _farPlane, &_projection);
    _viewProjectionDirty = true;
    _frustumDirty = true;

    return true;
}

Vec2 Camera::project(const Vec3& src) const
{
    Vec2 screenPos;

    auto viewport = Director::getInstance()->getWinSize();
    Vec4 clipPos;
    getViewProjectionMatrix().transformVector(Vec4(src.x, src.y, src.z, 1.0f), &clipPos);

    CCASSERT(clipPos.w != 0.0f, "clipPos.w can't be 0.0f!");
    float ndcX = clipPos.x / clipPos.w;
    float ndcY = clipPos.y / clipPos.w;

    screenPos.x = (ndcX + 1.0f) * 0.5f * viewport.width;
    screenPos.y = (1.0f - (ndcY + 1.0f) * 0.5f) * viewport.height;
    return screenPos;
}

Vec2 Camera::projectGL(const Vec3& src) const
{
    Vec2 screenPos;

    auto viewport = Director::getInstance()->getWinSize();
    Vec4 clipPos;
    getViewProjectionMatrix().transformVector(Vec4(src.x, src.y, src.z, 1.0f), &clipPos);

    CCASSERT(clipPos.w != 0.0f, "clipPos.w can't be 0.0f!");
    float ndcX = clipPos.x / clipPos.w;
    float ndcY = clipPos.y / clipPos.w;

    screenPos.x = (ndcX + 1.0f) * 0.5f * viewport.width;
    screenPos.y = (ndcY + 1.0f) * 0.5f * viewport.height;
    return screenPos;
}

Vec3 Camera::unproject(const Vec3& src) const
{
    Vec3 dst;
    unproject(Director::getInstance()->getWinSize(), &src, &dst);
    return dst;
}

Vec3 Camera::unprojectGL(const Vec3& src) const
{
    Vec3 dst;
    unprojectGL(Director::getInstance()->getWinSize(), &src, &dst);
    return dst;
}

void Camera::unproject(const Size& viewport, const Vec3* src, Vec3* dst) const
{
    CCASSERT(src && dst, "vec3 can not be null");

    Vec4 screen(src->x / viewport.width, ((viewport.height - src->y)) / viewport.height, src->z, 1.0f);
    screen.x = screen.x * 2.0f - 1.0f;
    screen.y = screen.y * 2.0f - 1.0f;
    screen.z = screen.z * 2.0f - 1.0f;

    getViewProjectionMatrix().getInversed().transformVector(screen, &screen);
    if (screen.w != 0.0f)
    {
        screen.x /= screen.w;
        screen.y /= screen.w;
        screen.z /= screen.w;
    }

    dst->set(screen.x, screen.y, screen.z);
}

void Camera::unprojectGL(const Size& viewport, const Vec3* src, Vec3* dst) const
{
    CCASSERT(src && dst, "vec3 can not be null");

    Vec4 screen(src->x / viewport.width, src->y / viewport.height, src->z, 1.0f);
    screen.x = screen.x * 2.0f - 1.0f;
    screen.y = screen.y * 2.0f - 1.0f;
    screen.z = screen.z * 2.0f - 1.0f;

    getViewProjectionMatrix().getInversed().transformVector(screen, &screen);
    if (screen.w != 0.0f)
    {
        screen.x /= screen.w;
        screen.y /= screen.w;
        screen.z /= screen.w;
    }

    dst->set(screen.x, screen.y, screen.z);
}

bool Camera::isVisibleInFrustum(const AABB* aabb) const
{
    //每次判断视锥是否需要重新初始化
    if (_frustumDirty)
    {
        _frustum.initFrustum(this);
        _frustumDirty = false;
    }
    //判断aabb是否在视锥内
    return !_frustum.isOutOfFrustum(*aabb);
}

float Camera::getDepthInView(const Mat4& transform) const
{
    Mat4 camWorldMat = getNodeToWorldTransform();
    const Mat4 &viewMat = camWorldMat.getInversed();
    float depth = -(viewMat.m[2] * transform.m[12] + viewMat.m[6] * transform.m[13] + viewMat.m[10] * transform.m[14] + viewMat.m[14]);
    return depth;
}

void Camera::setDepth(int8_t depth)
{
    if (_depth != depth)
    {
        _depth = depth;
        if (_scene)
        {
            //更改了相机层级后需要重新更新渲染顺序
            _scene->setCameraOrderDirty();
        }
    }
}

void Camera::onEnter()
{
    if (_scene == nullptr)
    {
        auto scene = getScene();
        if (scene)
        {
            setScene(scene);
        }
    }
    Node::onEnter();
}

void Camera::onExit()
{
    // remove this camera from scene
    setScene(nullptr);
    Node::onExit();
}

void Camera::setScene(Scene* scene)
{
    //设置当前相机所属场景。
    if (_scene != scene)
    {
        /*
    移除相机所属场景的相机列表中的该相机。
    相机列表是一个数组容器,我们需要从中找到相机地址并移除。
    */
        if (_scene)
        {
            auto& cameras = _scene->_cameras;
            auto it = std::find(cameras.begin(), cameras.end(), this);
            if (it != cameras.end())
                cameras.erase(it);
            _scene = nullptr;
        }
        /*
    添加相机到场景,若场景的相机列表已经包含该相机则将相机添加到末尾,否则不做处理。
    */
        if (scene)
        {
            _scene = scene;
            auto& cameras = _scene->_cameras;
            auto it = std::find(cameras.begin(), cameras.end(), this);
            if (it == cameras.end())
            {
                _scene->_cameras.push_back(this);
                //notify scene that the camera order is dirty
                _scene->setCameraOrderDirty();
            }
        }
    }
}

void Camera::clearBackground()
{
    if (_clearBrush)
    {
        _clearBrush->drawBackground(this);
    }
}

void Camera::setFrameBufferObject(experimental::FrameBuffer *fbo)
{
    CC_SAFE_RETAIN(fbo);
    CC_SAFE_RELEASE_NULL(_fbo);
    _fbo = fbo;
    if(_scene)
    {
        _scene->setCameraOrderDirty();
    }
}

void Camera::applyFrameBufferObject()
{
    if(nullptr == _fbo)
    {
        experimental::FrameBuffer::applyDefaultFBO();
    }
    else
    {
        _fbo->applyFBO();
    }
}

void Camera::apply()
{
    applyFrameBufferObject();
    applyViewport();
}

void Camera::applyViewport()
{
    if(nullptr == _fbo)
    {
        glViewport(getDefaultViewport()._left, getDefaultViewport()._bottom, getDefaultViewport()._width, getDefaultViewport()._height);
    }
    else
    {
        glViewport(_viewport._left * _fbo->getWidth(), _viewport._bottom * _fbo->getHeight(),
                   _viewport._width * _fbo->getWidth(), _viewport._height * _fbo->getHeight());
    }

}

int Camera::getRenderOrder() const
{
    /*根据是否有帧缓存返回渲染顺序
    若有帧缓存,返回帧缓存的FID左移8位的结果
    若无帧缓存,返回127左移8位后与相机深度的和
    */
    int result(0);
    if(_fbo)
    {
        result = _fbo->getFID()<<8;
    }
    else
    {
        result = 127 <<8;
    }
    result += _depth;
    return result;
}

void Camera::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
    //渲染器循环递归遍历渲染子节点
    _viewProjectionUpdated = _transformUpdated;
    return Node::visit(renderer, parentTransform, parentFlags);
}

void Camera::setBackgroundBrush(CameraBackgroundBrush* clearBrush)
{
    CC_SAFE_RETAIN(clearBrush);
    CC_SAFE_RELEASE(_clearBrush);
    _clearBrush = clearBrush;
}

NS_CC_END

CCCamera使用

一般来说,相机分为三种:自由相机,第一视角相机,第三视角相机。
如下定义:

enum class CameraType
{
    Free = 0,
    FirstPerson = 1,
    ThirdPerson = 2,
};

对于非第一视角的相机拉近拉远:

void Camera3DTestDemo::scaleCameraCallback(Ref* sender,float value)
{
    if(_camera&& _cameraType!=CameraType::FirstPerson)
    {
        //获取相机位置,相机向朝向的方向移动归一化指向向量的value倍距离
        Vec3 cameraPos=  _camera->getPosition3D();
        cameraPos+= cameraPos.getNormalized()*value;
        _camera->setPosition3D(cameraPos);
    }
}

对于第一视角的方向变化:

void Camera3DTestDemo::rotateCameraCallback(Ref* sender,float value)
{
    if(_cameraType==CameraType::Free || _cameraType==CameraType::FirstPerson)
    {
        //获取当前相机角度,改变y方向上的值来改变第一人称移动平面的视角。
        Vec3  rotation3D= _camera->getRotation3D();
        rotation3D.y+= value;
        _camera->setRotation3D(rotation3D);
    }
}

对于自由视角和第三视角的视角移动:

void Camera3DTestDemo::onTouchesMoved(const std::vector<Touch*>& touches, cocos2d::Event  *event)
{
    //判断是否为单点触摸。
    if(touches.size()==1)
    {
        auto touch = touches[0];
        auto location = touch->getLocation();
        Point newPos = touch->getPreviousLocation()-location;
        if(_cameraType==CameraType::Free || _cameraType==CameraType::FirstPerson)
        {
            Vec3 cameraDir;
            Vec3 cameraRightDir;
        //getForwardVector,得到向前的向量,相当于变换Vec3(0,0,-1)。
        _camera->getNodeToWorldTransform().getForwardVector(&cameraDir);
            cameraDir.normalize();
            cameraDir.y=0;
            //getRightVector,得到向右的向量,相当于变换Vec3(1,0,0)。
            _camera->getNodeToWorldTransform().getRightVector(&cameraRightDir);
            cameraRightDir.normalize();
            cameraRightDir.y=0;
            //手指滑动操作是相对于x-z平面的,所以获取向前和向右方向的归一化向量值大小,从而设置相机位置。
            Vec3 cameraPos=  _camera->getPosition3D();
            cameraPos+=cameraDir*newPos.y*0.1f;
            cameraPos+=cameraRightDir*newPos.x*0.1f;
            _camera->setPosition3D(cameraPos);
            if(_sprite3D &&  _cameraType==CameraType::FirstPerson)
            {
            //若是第一视角,得为作为第一视角的精灵设置位置。
        _sprite3D->setPosition3D(Vec3(_camera->getPositionX(),0,_camera->getPositionZ()));
            _targetPos=_sprite3D->getPosition3D();
            }
        }
    }
}

源码中这段代码看了好久还是有点抽象:

void Camera3DTestDemo::onTouchesEnded(const std::vector<Touch*>& touches, cocos2d::Event  *event)
{
    //遍历触摸事件点
    for ( auto &item: touches )
    {
        auto touch = item;
        //获取屏幕坐标
        auto location = touch->getLocationInView();
        if(_camera)
        {
            if(_sprite3D && _cameraType==CameraType::ThirdPerson && _bZoomOut == false && _bZoomIn == false && _bRotateLeft == false && _bRotateRight == false)
            {
            //定义了两个远近点坐标,其实就是相机屏幕触摸点前后一个像素的两个点。
                Vec3 nearP(location.x, location.y, -1.0f), farP(location.x, location.y, 1.0f);

                auto size = Director::getInstance()->getWinSize();
                //把指定坐标点从屏幕坐标转换为世界坐标。
                nearP = _camera->unproject(nearP);
                farP = _camera->unproject(farP);
                //定义一个远近两个点的方向向量。
                Vec3 dir(farP - nearP);
                float dist=0.0f;
                //取y方向上的差值,个人感觉取x方向上的差值效果一样。都是为了之后算比例。
                float ndd = Vec3::dot(Vec3(0,1,0),dir);
                if(ndd == 0)
                    dist=0.0f;
                float ndo = Vec3::dot(Vec3(0,1,0),nearP);
                /*
                因为
        dir.y = farP.y - nearP.y
        ndo.y = nearP.y
        ndd = dir.y
        所以,dist = (0 - ndo) / ndd = -nearP.y/(farP.y - nearP.y)
        */
                dist= (0 - ndo) / ndd;
                //计算比例,通过这种方式可以降低误差。
                Vec3 p =   nearP + dist *  dir;
                //接下来可以判断投影的点的位置是否在某区域内。
                if( p.x > 100)
                    p.x = 100;
                if( p.x < -100)
                    p.x = -100;
                if( p.z > 100)
                    p.z = 100;
                if( p.z < -100)
                    p.z = -100;

                _targetPos=p;
            }
        }
    }
} 

CCCamera小结

每个场景都有一个相机,通过相机能投影一个游戏世界。
相机类就介绍到这里,感觉需要补一补坐标系的知识。

作者:u012611878 发表于2016/9/27 1:04:28 原文链接
阅读:151 评论:0 查看评论

RecyclerView自定义LayoutManager,打造不规则布局

$
0
0

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。

RecyclerView的时代

自从google推出了RecyclerView这个控件, 铺天盖地的一顿叫好, 开发者们也都逐渐从ListView,GridView等控件上转移到了RecyclerView上, 那为什么RecyclerView这么受开发者的青睐呢? 一个主要的原因它的高灵活性, 我们可以自定义点击事件, 随意切换显示方式, 自定义item动画, 甚至连它的布局方式我们都可以自定义.

吐吐嘈

夸完了RecyclerView, 我们再来吐槽一下大家在工作中各种奇葩需求, 大家在日常工作中肯定会遇到各种各种的奇葩需求, 这里没就包括奇形怪状的需求的UI. 站在我们开发者的角度, 看到这些奇葩的UI, 心中无数只草泥马呼啸崩腾而过, 在愤愤不平的同时还不得不老老实实的去找解决方案… 好吧, 吐槽这么多, 其实大家都没有错, 站在开发者的角度, 这样的需求无疑增加了我们很多工作量, 不加班怎么能完成? 但是站在老板的角度, 他也是希望将产品做好, 所以才会不断的思考改需求.

效果展示

开始进入正题, 今天我们的主要目的还是来自定义一个LayoutManager, 实现一个奇葩的UI, 这样的一个布局我也是从我的一个同学的需求那看到的, 我们先来看看效果.

当然了, 效果不是很优雅, 主要是配色问题, 配色都是随机的, 所以肯定没有UI上好看. 原始需求是一个死的布局, 当然用自定义View的形式可以完成, 但是我认为那样不利于扩展, 例如效果图上的从每组3个变成每组9个, 还有一点很重要, 就是用RecyclerView我们还得轻松的利用View的复用机制. 好了, UI我们就先介绍到这, 下面我们开始一步步的实现这个效果.

自定义LayoutManager

前面说了, 我们这个效果是利用自定义RecyclerViewLayoutManager实现的, 所以, 首先我们要准备一个类让它继承RecyclerView.LayoutManager.

public class CardLayoutManager extends RecyclerView.LayoutManager {}

定义完成后, android studio会提醒我们去实现一下RecyclerView.LayoutManager里的一个抽象方法,

public class CardLayoutManager extends RecyclerView.LayoutManager {
  @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
      return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
              ViewGroup.LayoutParams.WRAP_CONTENT);
  }
}

这样, 其实一个最简单的LayoutManager我们就完成了, 不过现在在界面上是什么也没有的, 因为我们还没有对item view进行布局. 在开始布局之前, 还有几个参数需要我们从构造传递, 一个是每组需要显示几个, 一个当每组的总宽度小于RecyclerView总宽度的时候是否要居中显示, 来重写一下构造方法.

public class CardLayoutManager extends RecyclerView.LayoutManager {
    public static final int DEFAULT_GROUP_SIZE = 5;
    // ...
    public CardLayoutManager(boolean center) {
        this(DEFAULT_GROUP_SIZE, true);
    }

    public CardLayoutManager(int groupSize, boolean center) {
        mGroupSize = groupSize;
        isGravityCenter = center;
        mItemFrames = new Pool<>(new Pool.New<Rect>() {
            @Override
            public Rect get() { return new Rect();}
        });
    }
    // ...
}

ok, 在完成准备工作后, 我们就开始着手准备进行item的布局操作了, 在RecyclerView.LayoutManager中布局的入口是一个叫onLayoutChildren的方法. 我们来重写这个方法.

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0 || state.isPreLayout()) { return;}

    detachAndScrapAttachedViews(recycler);
    View first = recycler.getViewForPosition(0);
    measureChildWithMargins(first, 0, 0);
    int itemWidth = getDecoratedMeasuredWidth(first);
    int itemHeight = getDecoratedMeasuredHeight(first);

    int firstLineSize = mGroupSize / 2 + 1;
    int secondLineSize = firstLineSize + mGroupSize / 2;
    if (isGravityCenter && firstLineSize * itemWidth < getHorizontalSpace()) {
        mGravityOffset = (getHorizontalSpace() - firstLineSize * itemWidth) / 2;
    } else {
        mGravityOffset = 0;
    }

    for (int i = 0; i < getItemCount(); i++) {
        Rect item = mItemFrames.get(i);
        float coefficient = isFirstGroup(i) ? 1.5f : 1.f;
        int offsetHeight = (int) ((i / mGroupSize) * itemHeight * coefficient);

        // 每一组的第一行
        if (isItemInFirstLine(i)) {
            int offsetInLine = i < firstLineSize ? i : i % mGroupSize;
            item.set(mGravityOffset + offsetInLine * itemWidth, offsetHeight, mGravityOffset + offsetInLine * itemWidth + itemWidth,
                    itemHeight + offsetHeight);
        }else {
            int lineOffset = itemHeight / 2;
            int offsetInLine = (i < secondLineSize ? i : i % mGroupSize) - firstLineSize;
            item.set(mGravityOffset + offsetInLine * itemWidth + itemWidth / 2,
                    offsetHeight + lineOffset, mGravityOffset + offsetInLine * itemWidth + itemWidth  + itemWidth / 2,
                    itemHeight + offsetHeight + lineOffset);
        }
    }

    mTotalWidth = Math.max(firstLineSize * itemWidth, getHorizontalSpace());
    int totalHeight = getGroupSize() * itemHeight;
    if (!isItemInFirstLine(getItemCount() - 1)) { totalHeight += itemHeight / 2;}
    mTotalHeight = Math.max(totalHeight, getVerticalSpace());
    fill(recycler, state);
}

这里的代码很长, 我们一点点的来分析, 首先一个detachAndScrapAttachedViews方法, 这个方法是RecyclerView.LayoutManager的, 它的作用是将界面上的所有item都detach掉, 并缓存在scrap中,以便下次直接拿出来显示.
接下来我们通过一下代码来获取第一个item view并测量它.

View first = recycler.getViewForPosition(0);
measureChildWithMargins(first, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(first);
int itemHeight = getDecoratedMeasuredHeight(first);

为什么只测量第一个view呢? 这里是因为在我们的这个效果中所有的item大小都是一样的, 所以我们只要获取第一个的大小, 就知道所有的item的大小了. 另外还有个方法getDecoratedMeasuredWidth, 这个方法是什么意思? 其实类似的还有很多, 例如getDecoratedMeasuredHeight, getDecoratedLeft… 这个getDecoratedXXX的作用就是获取该view以及他的decoration的值, 大家都知道RecyclerView是可以设置decoration的.

继续代码

int firstLineSize = mGroupSize / 2 + 1;
int secondLineSize = firstLineSize + mGroupSize / 2;

这两句主要是来获取每一组中第一行和第二行中item的个数.

if (isGravityCenter && firstLineSize * itemWidth < getHorizontalSpace()) {
    mGravityOffset = (getHorizontalSpace() - firstLineSize * itemWidth) / 2;
} else {
    mGravityOffset = 0;
}

这几行代码的作用是当设置了isGravityCenter为true, 并且每组的宽度小于recyclerView的宽度时居中显示.
接下来的一个if...else...在if中的是判断当前item是否在它所在组的第一行. 为什么要加这个判断? 大家看效果就知道了, 因为第二行的view的起始会有一个二分之一的item宽度的偏移, 而且相对于第一行, 第二行的高度是偏移了二分之一的item高度. 至于这里面具体的逻辑大家可以对照着效果图去看代码, 这里就不一一解释了.
再往下, 我们记录了item的总宽度和总高度, 并且调用了fill方法, 其实在这个onLayoutChildren方法中我们仅仅记录了所有的item view所在的位置, 并没有真正的去layout它, 那真正的layout肯定是在这个fill方法中了,

private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getItemCount() <= 0 || state.isPreLayout()) { return;}
    Rect displayRect = new Rect(mHorizontalOffset, mVerticalOffset,
            getHorizontalSpace() + mHorizontalOffset,
            getVerticalSpace() + mVerticalOffset);

    Rect rect = new Rect();
    for (int i = 0; i < getChildCount(); i++) {
        View item = getChildAt(i);
        rect.left = getDecoratedLeft(item);
        rect.top = getDecoratedTop(item);
        rect.right = getDecoratedRight(item);
        rect.bottom = getDecoratedBottom(item);
        if (!Rect.intersects(displayRect, rect)) {
            removeAndRecycleView(item, recycler);
        }
    }

    for (int i = 0; i < getItemCount(); i++) {
        Rect frame = mItemFrames.get(i);
        if (Rect.intersects(displayRect, frame)) {
            View scrap = recycler.getViewForPosition(i);
            addView(scrap);
            measureChildWithMargins(scrap, 0, 0);
            layoutDecorated(scrap, frame.left - mHorizontalOffset, frame.top - mVerticalOffset,
                    frame.right - mHorizontalOffset, frame.bottom - mVerticalOffset);
        }
    }
}

在这里面, 我们首先定义了一个displayRect, 他的作用就是标记当前显示的区域, 因为RecyclerView是可滑动的, 所以这个区域不能简单的是0~高度/宽度这么一个值, 我们还要加上当前滑动的偏移量.
接下来, 我们通过getChildCount获取RecyclerView中的所有子view, 并且依次判断这些view是否在当前显示范围内, 如果不再, 我们就通过removeAndRecycleView将它移除并回收掉, recycle的作用是回收一个view, 并等待下次使用, 这里可能会改变它的属性(例如显示的值). 而scrap的作用是缓存一个view, 并等待下次显示, 这里的view会被重新绑定新的数据.

ok, 继续代码, 又一个for循环, 这里是循环的getItemCount, 也就是所有的item个数, 这里我们依然判断它是不是在显示区域, 如果在, 则我们通过recycler.getViewForPosition(i)拿到这个view, 并且通过addView添加到RecyclerView中, 添加进去了还没完, 我们还需要调用measureChildWithMargins方法对这个view进行测量. 最后的最后我们调用layoutDecorated对item view进行layout操作.

好了, 我们来回顾一下这个fill方法都是干了什么工作, 首先是回收操作, 这保证了RecyclerView的子view仅仅保留可显示范围内的那几个, 然后就是将这几个view进行布局.

现在我们来到MainActivity中,

mRecyclerView = (RecyclerView) findViewById(R.id.list);
mRecyclerView.setLayoutManager(new CardLayoutManager(mGroupSize, true));
mRecyclerView.setAdapter(mAdapter);

然后大家就可以看到上面的效果了, 高兴ing… 不过手指在屏幕上滑动的一瞬间, 高兴就会变成纳闷了. 纳尼? 怎么不能滑动呢? 好吧, 是因为我们的LayoutManager没有处理滑动操作, 是的, 滑动操作需要我们自己来处理…

让RecyclerView动起来

要想让RecyclerView能滑动, 我们需要重写几个方法.

public boolean canScrollVertically() {}
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {}

同样的, 因为我们的LayoutManager还支持横向滑动, 所以还有

public boolean canScrollHorizontally() {}
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {}

我们先来看看竖直方向上的滑动处理.

public boolean canScrollVertically() {
    return true;
}

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    detachAndScrapAttachedViews(recycler);
    if (mVerticalOffset + dy < 0) {
        dy = -mVerticalOffset;
    } else if (mVerticalOffset + dy > mTotalHeight - getVerticalSpace()) {
        dy = mTotalHeight - getVerticalSpace() - mVerticalOffset;
    }

    offsetChildrenVertical(-dy);
    fill(recycler, state);
    mVerticalOffset += dy;
    return dy;
}

第一个方法返回true代表着可以在这个方法进行滑动, 我们主要是来看第二个方法.

首先我们还是先调用detachAndScrapAttachedViews将所有的子view缓存起来, 然后一个if...else...判断是做边界检测, 接着我们调用offsetChildrenVertical来做偏移, 主要代码中这里的参数, 是对scrollVerticallyBy取反, 因为在scrollVerticallyBy参数中这个dy在我们手指往左滑动的时候是正值, 可能是google感觉这个做更加直观吧. 接着我们还是调用fill方法来做新的子view的布局, 最后我们记录偏移量并返回.

这里面的逻辑还算简单, 横向滑动的处理逻辑也相同, 下面给出代码, 就不再赘述了.

public boolean canScrollHorizontally() {
    return true;
}

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    detachAndScrapAttachedViews(recycler);
    if (mHorizontalOffset + dx < 0) {
        dx = -mHorizontalOffset;
    } else if (mHorizontalOffset + dx > mTotalWidth - getHorizontalSpace()) {
        dx = mTotalWidth - getHorizontalSpace() - mHorizontalOffset;
    }

    offsetChildrenHorizontal(-dx);
    fill(recycler, state);
    mHorizontalOffset += dx;
    return dx;
}

ok, 现在我们再次运行程序, 发现RecyclerView真的可以滑动了. 到现在位置我们的自定义LayoutManager已经实现了. 不过那个菱形咋办呢? 算了, 直接搞一张图片上去就行了. 其实刚开始我也是这么想的, 不过仔细想想, 一个普通的图片是有问题的. 我们还是要通过自定义view的方式去实现.

来搞一搞那个菱形

上面提到了, 那个菱形用图片是有问题的, 问题出在哪呢? 先来说答案吧: 点击事件. 说到这可能有些同学已经明白了, 也有一部分还在纳闷中… 我们来具体分析一下. 首先来张图.

大家看黄色框部分, 其实第三个view的布局是在黄色框里面的, 那如果我们点击第一个view的黄色框里面的区域是不是就点击到第三个view上了? 而我们的感觉确是点击在了第一个上, 所以一个普通的view在这里是不适用的. 根据这个问题, 我们再来想想自定义这个view的思路, 是不是只要我们在dispatchTouchEvent方法中来判断点击的位置是不是在那个菱形中, 如果不在就返回false, 让事件可以继续在RecyclerView往下分发就可以了?

下面我们根据这个思路来实现这么个view.

public class CardItemView extends View {

    private int mSize;
    private Paint mPaint;
    private Path mDrawPath;
    private Region mRegion;

    public CardItemView(Context context) {
        this(context, null, 0);
    }

    public CardItemView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CardItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Card, defStyleAttr, 0);
        mSize = ta.getDimensionPixelSize(R.styleable.Card_size, 10);
        mPaint.setColor(ta.getColor(R.styleable.Card_bgColor, 0));
        ta.recycle();

        mRegion = new Region();
        mDrawPath = new Path();

        mDrawPath.moveTo(0, mSize / 2);
        mDrawPath.lineTo(mSize / 2, 0);
        mDrawPath.lineTo(mSize, mSize / 2);
        mDrawPath.lineTo(mSize / 2, mSize);
        mDrawPath.close();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mSize, mSize);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            if (!isEventInPath(event)) { return false;}
        }

        return super.dispatchTouchEvent(event);
    }

    private boolean isEventInPath(MotionEvent event) {
        RectF bounds = new RectF();
        mDrawPath.computeBounds(bounds, true);
        mRegion.setPath(mDrawPath, new Region((int)bounds.left,
                (int)bounds.top, (int)bounds.right, (int)bounds.bottom));
        return mRegion.contains((int) event.getX(), (int) event.getY());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT);
        canvas.drawPath(mDrawPath, mPaint);
    }

    public void setCardColor(int color) {
        mPaint.setColor(color);
        invalidate();
    }
}

代码并不长, 首先我们通过Path来规划好我们要绘制的菱形的路径, 然后在onDraw方法中将这个Path绘制出来, 这样, 那个菱形就出来了.
我们还是重点来关注一下dispatchTouchEvent方法, 这个方法中我们通过一个isEventInPath来判断是不是DOWN事件发生在了菱形内, 如果不是则直接返回false, 不处理事件.

通过上面的分析, 我们发现其实重点是在isEventInPath中, 这个方法咋写的呢?

private boolean isEventInPath(MotionEvent event) {
    RectF bounds = new RectF();
    mDrawPath.computeBounds(bounds, true);
    mRegion.setPath(mDrawPath, new Region((int)bounds.left,
            (int)bounds.top, (int)bounds.right, (int)bounds.bottom));
    return mRegion.contains((int) event.getX(), (int) event.getY());
}

判断点是不是在某一个区域内, 我们是通过Region来实现的, 首先我们通过Path.computeBounds方法来获取到这个path的边界, 然后通过Region.contains来判断这个点是不是在该区域内.

到现在为止, 整体的效果我们已经实现完成了, 而且点击事件我们处理的也非常棒, 如果大家有这种需求, 可以直接copy该代码使用, 如果没有就当让大家来熟悉一下如何自定义LayoutManager了.

参考链接: https://github.com/hehonghui/android-tech-frontier/

最后给出github地址: https://github.com/qibin0506/CardLayoutManager

作者:qibin0506 发表于2016/9/27 1:19:06 原文链接
阅读:274 评论:0 查看评论

让你的「微信小程序」运行在Chrome浏览器上,让我们使用WebStorm

$
0
0

「微信小程序」的开发框架体验起来,还不错——自带了UI框架。但是问题是他的IDE,表现起来相当的糟糕——其实主要是因为,我当时买WebStorm License买了好多年。所以,我觉得他的IDE真不如我这个付费好用。

而且,作为一个拥护自由和开源的 「GitHub 中国区首席Markdown程序员」。微信在「微信小程序」引导着Web开向封闭,我们再也不能愉快地分享我们的代码了。

如果我们放任下去,未来的Web世界令人堪忧。

好了,废话说完了:

文章太长不想看,可以直接看Demo哈哈:

GitHub: https://github.com/phodal/weapp-webdemo
预览:http://weapp.phodal.com/

真实世界下的MINA三基本元素

「微信小程序」的背后运行的是一个名为MINA框架。在之前的几篇文章里,我们介绍得差不多了。现在让我们来作介绍pipeline:

Transform wxml和wxss

当我们修改完WXML、WXSS的时候,我们需要重新编译项目才能在浏览器上看到效果。这时候后台就会执行一些transform动作:

  1. wcc来转换wxml为一个genrateFun,执行这个方法将会得到一个virtual dom
  2. wxss就会转换wxss为css——这一点有待商榷。

wcc和wxss,可以从vendor目录下获取到,在“微信web开发者工具”下敲入help,你就会得到下面的东东:

这里写图片描述

运行openVendor(),你就会得到上面的wcss、wxss、WAService.js、WAWebview.js四个文件了。

Transform js文件

对于js文件来说,则是一个拼装的过程,如下是我们的app.js文件:

App({
onLaunch: function () { }
})

它在转换后会变成:

define("app.js", function(require, module){var window={Math:Math}/*兼容babel*/,location,document,navigator,self,localStorage,history,Caches;
        App({
            onLaunch: function () {

            }
        })
});
require("app.js");

我假装你已经知道这是什么了,反正我也不想、也不会解释了~~。同理于:

define("pages/index/index.js", function(require, module){var window={Math:Math}/*兼容babel*/,location,document,navigator,self,localStorage,history,Caches;
        Page({
            data: {
                text: initData
            }
        });
    require("pages/index/index.js");

至于它是如何replace或者apend到html中,我就不作解释了。

MINA如何运行?

为了运行一个Page,我们需要有一个virtual dom,即用wcc转换后的函数,如:

 /*v0.7cc_20160919*/
        var $gwxc
        var $gaic={}
        $gwx=function(path,global){
            function _(a,b){b&&a.children.push(b);}
            function _n(tag){$gwxc++;if($gwxc>=16000){throw 'enough, dom limit exceeded, you don\'t do stupid things, do you?'};return {tag:tag.substr(0,3)=='wx-'?tag:'wx-'+tag,attr:{},children:[]}}
            function _s(scope,env,key){return typeof(scope[key])!='undefined'?scope[key]:env[key]}
            function _wl(tname){console.warn('template `' + tname + '` is being call recursively, will be stop.')}
            function _ai(i,p,e,me){var x=_grp(p,e,me);if(x)i.push(x);else{console.warn('path `'+p+'` not found from `'+me+'`')}}
            function _grp(p,e,me){if(p[0]!='/'){var mepart=me.split('/');mepart.pop();var ppart=p.split('/');for(var i=0;i<ppart.length;i++){if( ppart[i]=='..')mepart.pop();else if(!ppart[i])continue;else mepart.push(ppart[i]);}p=mepart.join('/');}if(me[0]=='.'&&p[0]=='/')p='.'+p;if(e[p])return p;if(e[p+'.wxml'])return p+'.wxml';}
//以下省略好多字。

然后在我们的html中加一个script,如

document.dispatchEvent(new CustomEvent("generateFuncReady", {
        detail: {
            generateFunc: $gwx('index.wxml')
        }
    }))

就会凑发这个事件了。我简单的拆分了WXWebview.js得到了几个功能组件:

  • define.js,这里就是定义AMD模块化的地方
  • exparser.js,用于转换WXML标签到HTML标签
  • exparser-behvaior.js,定义不同标签的一些行为
  • mobile.js,应该是一个事件库,好像我并不关心。
  • page.js,核心代码,即Page、App的定义所在。
  • report.js,你所说的一切都能够用作为你的呈堂证供
  • virtual_dom.js,一个virtual dom实现结合wcc使用,里面应该还有component.css,也可能是叫weui
  • wa-wx.js,定义微信各种API以及WebView和Native的地方,和下面的WX有冲突。
  • wx.js,同上,但是略有不同。
  • wxJSBridge.js,Weixin JS Bridge

于是,我就用上面的组件来定义不同的位置好了。当我们触发自定义的generateFuncReady事件时,将由virtual_dom.js来接管这次Render:

document.addEventListener("generateFuncReady", function (e) {
    var generateFunc = e.detail.generateFunc;
    wx.onAppDataChange && generateFunc && wx.onAppDataChange(function (e) {
        var i = generateFunc((0, d.getData)());
        if (i.tag = "body", e.options && e.options.firstRender){
            e.ext && ("undefined" != typeof e.ext.webviewId && (window.__webviewId__ = e.ext.webviewId), "undefined" != typeof e.ext.downloadDomain && (window.__downloadDomain__ = e.ext.downloadDomain)), v = f(i, !0), b = v.render(), b.replaceDocumentElement(document.body), setTimeout(function () {
                wx.publishPageEvent(p, {}), r("firstRenderTime", n, Date.now()), wx.initReady && wx.initReady()
            }, 0);
        } else {
            var o = f(i, !1), a = v.diff(o);
            a.apply(b), v = o, document.dispatchEvent(new CustomEvent("pageReRender", {}));
        }
    })
})

因此,这里就是负责DOM初始化的地方了,这里得到的Dom结果是这样的:

<wx-view class="btn-area">
    <wx-view class="body-view">
        <wx-text><span style="display:none;"></span><span></span></wx-text>
        <wx-button>add line</wx-button>
        <wx-button>remove line</wx-button>
    </wx-view>
</wx-view>

而我们写的wxml是这样的:

<view class="btn-area">
  <view class="body-view">
    <text>{{text}}</text>
    <button bindtap="add">add line</button>
    <button bindtap="remove">remove line</button>
  </view>
</view>

很明显view会被转换为wx-view,text会被转换为wx-text等等,以此类推。这个转换是在virtual dom.js中调用的,调用的方法就是exparser。

遗憾的是我现在困在 data初始化上面了~~,这里面有两套不同的事件系统,有一些困扰。其中有一个是:WeixinJSBridge、还有一个是app engine中的事件系统,两个好像不能互调。。。

使用WebStorm开发

在浏览器上运行之前,我们需要简单的mock一些方法,如:

  • window.webkit.messageHandlers.invokeHandler.postMessage
  • window.webkit.messageHandlers.publishHandler.postMessage
  • WeixinJSCore.publishHandler
  • WeixinJSCore..invokeHandler

然后把 config.json中的一些内容变成__wxConfig,如:

__wxConfig = {
    "debug": true,
    "pages": ["index"],
    "window": {
        "backgroundTextStyle": "light",
        "navigationBarBackgroundColor": "#fff",
        "navigationBarTitleText": "WeChat",
        "navigationBarTextStyle": "black"
    },
    "projectConfig": {

    },
    "appserviceConfig": {

    },
    "appname": "fdfafafafafafafa",
    "appid": "touristappid",
    "apphash": 2107567080,
    "isTourist": true,
    "userInfo": {}
}

如这里我们的appname是哈哈哈哈哈哈哈——我家在福建。

然后在我们的html中引入各个js文件,啦啦。

我们还需要一个自动化的glup脚本来watch wxml和wxss的修改,然后编译,如:

exec('./vendor/wcc -d ' + inputPath + ' > ' + outputFileName, function(err, stdout, stderr) {
            console.log(stdout);
            console.log(stderr);
});

说了这么多,你还不如去看代码好了:

GitHub: https://github.com/phodal/weapp-webdemo
预览:http://weapp.phodal.com/

作者:gmszone 发表于2016/9/27 7:34:12 原文链接
阅读:46 评论:0 查看评论

Android官方开发文档Training系列课程中文版:电池续航时间优化之按需开启广播接收器

$
0
0

原文地址:http://android.xsoftlab.net/training/monitoring-device-state/manifest-receivers.html

监测设备状态变化最简单的实现方式就是为每种状态都创建一个广播接收器,然后只需在相应的广播接收器内依据当前的设备状态重新执行各自的任务即可。

这种方式的不好之处就在于每次广播接收器被触发后,APP都会唤醒设备。

一种比较好的解决方案就是在运行时关闭或开启广播接收器。这样也可以使在清单文件中声明的广播接收器按需触发。

动态开启广播接收器

我们可以通过PackageManager将清单文件中声明过的任何组件切换到开启\关闭状态,其中也包括你将要开启或者关闭的广播接收器:

ComponentName receiver = new ComponentName(context, myReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
        PackageManager.DONT_KILL_APP)

通过使用这种方法,如果发现网络连接已经断开,那么就可以关闭所有的相关广播接收器,除了监听连接变化的广播接收器之外。反之,一旦连接到网络,那么则应当停止网络变化的监听:只需要在执行网络任务之前,检查一下是否有网络连接即可。

你也可以使用这种方式推迟那种需要超大带宽的网络任务。只需要监听一下网络连接的变化即可,一旦连接到Wi-Fi,那则可以开始进行网络下载。

作者:u011064099 发表于2016/9/27 8:55:26 原文链接
阅读:61 评论:0 查看评论

MeasureSpec解析

$
0
0

MeasureSpec测量规格

/**
 * A MeasureSpec encapsulates the layout requirements passed from parent to child.
 * MeasureSpec是父控件传给子控件的布局条件
 * Each MeasureSpec represents a requirement for either the width or the height.
 * 每个MeasureSpec标识着宽度或高度的限制
 * A MeasureSpec is comprised of a size and a mode. There are three possible
 * modes:
 * MeasureSpec由size和mode组成
 * <dl>
 * <dt>UNSPECIFIED</dt>未指定
 * <dd>
 * The parent has not imposed any constraint on the child. It can be whatever size
 * it wants.
 * </dd>
 *
 * <dt>EXACTLY</dt>精确地
 * <dd>
 * The parent has determined an exact size for the child. The child is going to be
 * given those bounds regardless of how big it wants to be.
 * </dd>
 *
 * <dt>AT_MOST</dt>至多
 * <dd>
 * The child can be as large as it wants up to the specified size.
 * </dd>
 * </dl>
 *
 * MeasureSpecs are implemented as ints to reduce object allocation.
 *
 * This class is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
 */
public class MeasureSpec {
    ......
}

标记位

一个整型是32比特位,MeasureSpec用前2位表示SpecMode,后30位表示SpecSize,它们的操作涉及到的知识点:移位运算、与或非操作参考我另外两篇博客,
java二进制、八进制、十六进制间转换详细
java位运算示例

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//11000000 00000000 00000000 00000000

public static final int UNSPECIFIED = 0 << MODE_SHIFT;//00000000 00000000 00000000 00000000

public static final int EXACTLY     = 1 << MODE_SHIFT;//01000000 00000000 00000000 00000000

public static final int AT_MOST     = 2 << MODE_SHIFT;//10000000 00000000 00000000 00000000

SpaceMode

  • UNSPECIFIED
    父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示测量的状态
  • EXACTLY
    父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。他对应于LayoutParams中的match_parent和具体的数值这两种模式
  • AT_MOST
    父容器制定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content

相关代码

1.MeasureSpec被实例化之后,通过adjust将SpacMode、SpecSize打包成int值

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);
}

方法:makeMeasureSpec

/**
 * Creates a measure specification based on the supplied size and mode.
 * 创建一个基于size和mode的测量规范
 * The mode must always be one of the following:
 * <ul>
 *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
 *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
 *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
 * </ul>
 *
 * @param size the size of the measure specification
 * @param mode the mode of the measure specification
 * @return the measure specification based on size and mode
 */
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

也可以外部调用,通过makeSafeMeasureSpec方法打包int值

/**
 * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
 * will automatically get a size of 0. Older apps expect(期望) this.
 * 当spec的mode为UNSPECIFIED时,size的值自动设置为0
 * @hide internal use only for compatibility(兼容) with system widgets and older apps
 */
public static int makeSafeMeasureSpec(int size, int mode) {
    if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
        return 0;
    }
    return makeMeasureSpec(size, mode);
}

2.可以通过MeasureSpec的getMode、getSize获取相关信息

方法:getMode

/**
 * Extracts the mode from the supplied measure specification.
 * 从measureSpec中抽取mode
 * @param measureSpec the measure specification to extract the mode from
 * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
 *         {@link android.view.View.MeasureSpec#AT_MOST} or
 *         {@link android.view.View.MeasureSpec#EXACTLY}
 */
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

方法:getSize

/**
 * Extracts the size from the supplied measure specification.
 *
 * @param measureSpec the measure specification to extract the size from
 * @return the size in pixels defined in the supplied measure specification
 */
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}
作者:u010137760 发表于2016/9/27 9:38:54 原文链接
阅读:1 评论:0 查看评论

Android绘制一条宽度逐渐变大的路径

$
0
0

1.介绍

  • 先看一下效果图

用过电脑版印象笔记演示功能的应该知道这个效果,现在想在手机上实现一下,当然这个是最终的目标,这篇只是介绍实现过程中的一步 —— 绘制一条宽度逐渐变大的路径

2.实现

2.1完成图

这里写图片描述

2.2思路

把一条路径分成很多段,每段path首尾相连,然后依次设置每一段path的宽度,把这些path存储在一个集合中,最后依次绘制出来。

这种方法实现的问题主要是如果截取的段太少,在拐角处显示不圆滑,锯齿严重,所以定义了DEFAULT_SEGMENT_LENGTH来控制截取长度,长度越小,锯齿度越小,拐角处越圆滑。对于这个问题,如有更好的思路,请告诉我。

2.3把一条path分成多段path的方法

这里要使用到PathMeasure,PathMeasure的getSegment方法就是获取一段path,

public boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)

startD 表示从path某个长度位置开始
stopD 表示某个长度位置结束
dst 表示截取后的path
startWithMoveTo 表示这个截取的path开始位置是否移动到截取的开始位置,false的话,path开始点位置是(0,0),这里需要设置为true,不然达不到首尾相连的效果

对于path长度的获取,PathMeasure有相应的方法getLength();

2.4截取路径的方法实现

/**
     * 截取path
     * @param path
     */
    private void getPaths(Path path){
        PathMeasure pm = new PathMeasure(path, false);
        float length = pm.getLength();
        int segmentSize = (int) Math.ceil(length / DEFAULT_SEGMENT_LENGTH);
        Path ps = null;
        PathSegment pe = null;
        int nowSize = pathSegments.size();//集合中已经有的
        if(nowSize == 0){
            ps = new Path();
            pm.getSegment(0, length, ps, true);
            pe = new PathSegment(ps);
            pe.setAlpha(255);
            pe.setWidth(DEFAULT_WIDTH);
            pathSegments.add(pe);
        } else{
            for (int i = nowSize; i < segmentSize; i++) {
                ps = new Path();
                pm.getSegment((i - 1) * DEFAULT_SEGMENT_LENGTH - 0.4f, Math.min(i * DEFAULT_SEGMENT_LENGTH, length), ps,  true);
                pe = new PathSegment(ps);
                pe.setAlpha(255);
                pe.setWidth((float) Math.min(MAX_WIDTH, i * 0.3 + DEFAULT_WIDTH));
                pathSegments.add(pe);
            }
        }
    }

3.完整代码

public class TailView2 extends View{
    private Paint paint;
    private Path mFingerPath;
    private float mOriginX;
    private float mOriginY;

    private List<PathSegment> pathSegments;

    private class PathSegment{
        Path path;
        float width;
        int alpha;

        public PathSegment(Path path) {
            this.path = path;
        }

        public Path getPath() {
            return path;
        }

        public void setPath(Path path) {
            this.path = path;
        }

        public float getWidth() {
            return width;
        }

        public void setWidth(float width) {
            this.width = width;
        }

        public int getAlpha() {
            return alpha;
        }

        public void setAlpha(int alpha) {
            this.alpha = alpha;
        }

    }

    public TailView2(Context context) {
        this(context, null, 0);
    }

    public TailView2(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TailView2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);

        //-------------------------------------------------
        mFingerPath = new Path();
        pathSegments = new ArrayList<>();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (PathSegment p: pathSegments) {
            paint.setAlpha(p.getAlpha());
            paint.setStrokeWidth(p.getWidth());
            canvas.drawPath(p.getPath(), paint);
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                pathSegments.clear();
                mOriginX = x;
                mOriginY = y;
                mFingerPath.reset();
                mFingerPath.moveTo(mOriginX, mOriginY);
                break;

            case MotionEvent.ACTION_MOVE:
                getPaths(mFingerPath);
                mFingerPath.lineTo(x, y);
                break;

            case MotionEvent.ACTION_UP:
                break;
        }
        invalidate();
        return true;
    }


    /**
     * 越小,线条锯齿度越小
     */
    private static final float DEFAULT_SEGMENT_LENGTH = 10F;
    private static final float DEFAULT_WIDTH = 3F;
    private static final float MAX_WIDTH = 45F;

    /**
     * 截取path
     * @param path
     */
    private void getPaths(Path path){
        PathMeasure pm = new PathMeasure(path, false);
        float length = pm.getLength();
        int segmentSize = (int) Math.ceil(length / DEFAULT_SEGMENT_LENGTH);
        Path ps = null;
        PathSegment pe = null;
        int nowSize = pathSegments.size();//集合中已经有的
        if(nowSize == 0){
            ps = new Path();
            pm.getSegment(0, length, ps, true);
            pe = new PathSegment(ps);
            pe.setAlpha(255);
            pe.setWidth(DEFAULT_WIDTH);
            pathSegments.add(pe);
        } else{
            for (int i = nowSize; i < segmentSize; i++) {
                ps = new Path();
                pm.getSegment((i - 1) * DEFAULT_SEGMENT_LENGTH - 0.4f, Math.min(i * DEFAULT_SEGMENT_LENGTH, length), ps,  true);
                pe = new PathSegment(ps);
                pe.setAlpha(255);
                pe.setWidth((float) Math.min(MAX_WIDTH, i * 0.3 + DEFAULT_WIDTH));
                pathSegments.add(pe);
            }
        }
    }
}
作者:u011102153 发表于2016/9/27 13:04:55 原文链接
阅读:112 评论:0 查看评论

[Android编译(二)] 从谷歌官网下载android 6.0源码、编译并刷入nexus 6p手机

$
0
0

1 前言

经过一周的奋战,终于从谷歌官网上下载最新的android 6.0.1_r62源码,编译成功,并成功的刷入nexus6p,接着root完毕,现写下这篇博客记录一下实践过程。

2 简介

自己下载android系统源码,修改定制,然后编译刷入安卓手机,想想还有点小激动呢。简单点说一句话——定制我们自己的MIUI,这就是android的魅力,这篇博客博主就来教大家实现自己的定制系统。
首先,要明白下面的基础知识:

(1) 什么是aosp?
aosp就是android open source project(安卓开源项目),也就是android源码的简称,使用git管理,我们要下载的就是aosp源码,它托管在谷歌的服务器上,直接编译aosp后得到的android系统就叫原生android。

(2) 如何下载aosp?
aosp需要专门的下载工具repo下载,当然必须科学上网。

(3) 如何编译aosp?
aosp必须在Linux系统上编译,官方推荐的是Ubuntu系统。目前官网上只有Ubuntu 14.04的编译教程,但博主用的是Ubuntu 16.04,并且编译成功了。

(3) 什么手机可以刷入aosp?
并不是所有安卓手机都可以刷入安卓源码,虽然aosp是开源的,但是aosp本身界面很简陋,也不带有驱动,所以厂商拿到aosp后都会进行定制,这些定制系统就是MIUI们,它们并不开源,所以你无法拿到MIUI的源码,自然也无法编译它了。现在大部分手机厂商为了自家定制系统的市场占有率,都给手机的bios(bootloader)加了锁,所以魅族的手机你是无法刷入小米的系统的。所以能够刷入原生android的机型很少,博主只知道谷歌的nexus系列和一加手机可以。这里推荐谷歌nexus系列手机,也就是人称的太子,android的标杆机,当然,nexus手机不在大陆发售,你需要在万能的淘宝上购买。

(4) android开放但不开源
android并不是真正的开源操作系统,因为android的驱动是不开源的,所以它从Linux中除名了,linux才是真正的开源系统,你可以拿到所有源码,包括驱动,但是安卓不行,所以安卓是开放的系统。android把驱动放在了HAL层,这个是抽象层,具体实现交给了各个厂商,而且为了规避GPL协议的传染性,android巧妙的将HAL层放在了用户态,并且把关键的内核库全部重写了,从而保证从android的framework层开始不必开源了,不得不说谷歌在开源和商业化之间做了巧妙的结合,这也是安卓能够迅速发展的原因之一。所以,我们编译的系统要刷入手机中去最难的地方就是没有驱动,那么有没有手机厂商放出了驱动呢?有的,这就是谷歌的nexus系列手机,最新的是nexus 5x和nexus 6p,作为android开发者必备手机。

(5) 什么是CM?
前面提到了,将AOSP刷入手机最困难的地方是没有驱动,因为驱动是商业机密,是闭源的,那么有没有人专门处理不同手机的驱动问题呢,屏蔽掉驱动的差异呢?答案是有的,这就是大名鼎鼎的Cyanogenmod,简称CM,目前全球最有名的第三方编译团队。换句话说,如果你的手机不是nexus系列手机,又想编译源码刷入手机中,那么你可以下载你手机型号对应的CM的源码。当然,CM是怎么解决驱动问题的呢,据说是从官方ROM中提取的二进制代码。

3 环境

  • Ubuntu 16.04
  • nexus 6p
  • 科学上网
  • Git
  • Gmail 邮箱
  • Twrp recovery
  • Open Gapps
  • SuperSU

4 准备工作

(1) 安装ubuntu 16.04
首先要安装Ubuntu 16.04,博主直接安装在实体机器上,你也可以在vmware虚拟机中安装,但是强烈建议在实体机器上直接安装Ubuntu,因为运行流畅、编译速度快,节约时间。Ubuntu和windows的兼容做的很好,博主安装的就是win7和Ubuntu双系统。
至于如何安装Ubuntu百度一下一大堆,这里博主就带过来了,只是顺便提醒一点,如果在实体机上安装Ubuntu,一定要给Ubuntu的/home分区至少250G空间,因为源码解压、编译后真的很大,如果你想把源码压缩保存起来,那么至少300G空间,博主就在压缩的过程中遇到了空间不足的情况,只好切换到win7用分区助手压缩分出空闲分区,然后再用Gparted在Linux下面把空闲分区分给/home,整整浪费一天时间,其中涉及到windows和linux重新分区,Gparted用法,linux启动修复等高级技术,所以提醒大家多分点空间给Linux.

(2) 科学上网
要下载Google上的Android源码必须学会科学上网,博主使用的是搬瓦工vps+shadowsocks的方式科学上网,为了下载Android源码,博主专门写了一篇如何科学上网的博客,具体请看这里:

Ubuntu 16.04 使用ShadowSocks + Privoxy 科学上网

科学上网一般都是要花钱的,免费的不稳定。

(3) Gmail邮箱
在下载源码的过程中,谷歌为了防止匿名连接过多,对同一个ip的访问次数进行了限制,这就导致我们下载过程中很可能会失败,因此我们需要用一个谷歌账号(也就是Gmail邮箱)来进行验证。博主在下载过程中就遇到了匿名下载失败这个问题,后来加了验证之后才下载成功。有了Gmail邮箱就可以使用Google的所有服务了,可以体验优秀的Google App了,如果你还没有Gmail账号,那就赶紧去申请吧,当然,前提是能科学上网。

(4) nexus 6p手机
作为Android标杆机,nexus系列是Android开发者必备,你可以自己修改源码,编译后push到手机中看看效果,这样学习过程就不会枯燥了,想想是不是还有点小激动呢。如果nexus手机上某个app有问题,那么一定是app写的不好,而不是手机的问题,这就是标杆机。目前最新的nexus系列是华为代工的nexus 6p,颜值高,大屏幕,双喇叭,瞬间指纹解锁,高端大气,上一张nexus 6p的图片:

这里写图片描述

当然原生的AOSP不带谷歌全家桶(GApps),用nexus不用谷歌全家桶就好比喝拿铁不加糖一样—苦不堪言。所以,刷机完毕,博主会教大家如何刷入Google Apps。
最后补充一点,nexus不翻&墙也是可以用的,但是,逼格呢?

5 下载源码

(1) 安装Git
Git安装命令如下:

sudo apt-get install git 

安装完毕,配置Git账号,这里用户名可以随便写,但是邮箱请务必使用你的Gmail邮箱,因为涉及到下面的访问验证,命令如下:

git config --global user.name "Your Name"   
git config --global user.email "Your Gmail@gmail.com"

(2) Google账号验证
对于匿名访问,为了防止连接过多,谷歌对同一个ip的访问次数进行了限制,所以我们需要添加一个下载验证,打开下面的链接:

https://android.googlesource.com/new-password

登录谷歌账号,会看到下面的界面:

这里写图片描述

翻译一下就是:通过复制下面的脚本代码到shell中,为Git配置cookie,用于访问谷歌服务。
如果你是在windows下用Git for windows来下载源码,那么复制上面框框里面代码到cmd命令行中去执行,一行一行的执行。
由于我们使用的是Ubuntu,所以我们打开shell窗口,将下面框框的代码一行一行的复制到shell中执行,复制一行执行一行就可以了。

(3) 安装repo
android源码包括很多模块,每个模块都是一个个工程,不同版本依赖不同模块,总不能一个一个去下载这些工程吧,所以为了管理这些工程,谷歌开发了一个管理Git仓库的工具叫repo,官网的安装教程是这样的:
首先创建bin文件夹:

$ mkdir ~/bin
$ PATH=~/bin:$PATH

下载repo工具:

$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo

(4) 选择合适的源码版本
android源码除了大版本外还有很多小版本,例如:android-6.0.1_r62,6.0.1就是大版本号,r62就是小版本号,因此我们需要先确定好需要下载的版本号,具体有哪些版本请看下面的链接:

https://source.android.com/source/build-numbers.html#source-code-tags-and-builds

第一列是编译的代号,第二列是版本号,第三列是版本名称,最后一列是该版本支持的手机型号,请下载与你手机对应的版本,由于博主用的是nexus 6p,所以我下载的是最新的android-6.0.1_r62:

这里写图片描述

(5) 开始下载
确定了版本号之后,首先创建文件夹,这里以版本号命名:

$ mkdir android-6.0.1_r62
$ cd android-6.0.1_r62

初始化repo,注意把 -b 后面改为你要下载的版本号:

$ repo init -u https://android.googlesource.com/platform/manifest -b android-6.0.1_r62

使用下面的命令就可以开始下载了:

$ repo sync 

但是,请先不要着急下载,由于下载过程中还会遇到各种各样的问题,所以我们需要创建一个自动下载脚本,确保出错了之后会自动执行repo sync,在android-6.0.1_r62下创建一个脚本down.sh,代码如下:

#!/bin/bash
repo sync  -j16
while [ $? = 1 ]; do  
        echo “======sync failed, re-sync again======”  
        sleep 3  
        repo sync  -j16
done 

在shell中执行下面命令:

chmod a+x down.sh
./down.sh

好了,开始下载源码了,整个源码大概有40G,建议在晚上下载。在没有下载完毕之前,源码的文件夹中只有一个.repo文件夹,这是正常的,等到所有源码下载完毕,其余文件夹就会出现,不要着急。下载完毕后如下图:

这里写图片描述

全部下载完后android-6.0.1_r62中文件如下:

这里写图片描述

(6) 备份源码
源码下载完毕后,先不要着急着编译,我们先备份一下源码,万一玩坏了还可以推倒重来,否则又要辛辛苦苦下载一遍。博主一上来就直接压缩打包android-6.0.1_r62这个源码文件夹,结果发现有50多G,直接把磁盘爆掉了,于是又重新从windows下面分出空闲分区,然后调整Linux的分区,折腾了整整一天,后来解压的时候解压了半个小时还没有解压完,又把硬盘给爆掉了。仔细一想,不对啊,别人上传到网盘的源码只有7G左右啊,我的源码怎么这么大呢,后来发现都是.repo这个文件夹太大了,百度一下,发现这个文件夹只是与代码同步有关,并不影响编译,于是果断删掉之后,压缩打包源码,这次只有7G左右,而且删掉之后编译成功。下面我们来备份源码。
首先删掉 .repo 文件夹,然后使用下面的命令:

cd ~
tar -zcvf android-6.0.1_r62.tar.gz android-6.0.1_r62/

压缩完毕后android-6.0.1_r62.tar.gz只有不到7G,如果你已经编译完了才备份代码,那么备份前请使用make clean来清除掉编译出来的文件,以便减小压缩后的体积。

下载完源码请务必删掉 .repo 文件夹,以便备份源码,同时防止编译时磁盘空间不够用

6 编译

关于android 6.0 的编译,请看我之前写的一篇博客,非常详细,这里不再赘述,博客地址如下,只补充两点:

[Android 编译(一)] Ubuntu 16.04 LTS 成功编译 Android 6.0 源码教程

(1) 下面三个库如果你安装失败了,不用管他,照样可以编译成功:

libncurses5-dev:i386
libreadline6-dev:i386
lib32ncurses5-dev

(2) 在lunch的时候选择17,如下图所示:

这里写图片描述

aosp_angler-userdebug ,解释一下,angler是nexus 6p的代号(code name),每个设备对应的代号如下:

这里写图片描述

userdebug 是编译类型,含义如下:

这里写图片描述

user 用于正式产品
userdebug 和user类似,但是有root权限,并且可以调试,主要用于调试
eng 开发用的选项,配有额外的调试工具。

如果编译后只在模拟器上运行,则lunch时选择:1
这里我们的目标是要编译后刷入nexus 6p,所以 lunch 时选择:17

如果你不知道lunch该选哪一个,请参考下面的链接,根据第三列选择编译选项:

https://source.android.com/source/running.html

上一张编译成功后的图:

这里写图片描述

7 驱动概述

如果只在模拟器中运行,到这里就结束了,但我们要把编译好的镜像刷入nexus 6p,首先得去官网下载工厂镜像(factory image)。

(1) 为何还要下载工厂镜像呢?
前面我们提到了,只有nexus系列手机放出了驱动,所以我们需要去谷歌官网获取驱动。

(2) 驱动又是以什么形式放出的呢?
在nexus 6p(5x)之前,例如nexus 5,驱动的二进制代码(当然不可能有源码啦)是压缩包的形式,下载后在aosp源码根目录下面解压,然后执行里面的sh脚本文件,接着编译全部源码就能编译出驱动镜像,谷歌一下都是这种方法,然而关于nexus 6p编译AOSP后刷机时驱动问题的资料就很少了,博主在这里卡了很长时间才搞清楚了nexus 6p刷入AOSP时如何刷入驱动的方法。

(3) 如何获取nexus 6p的驱动?
从nexus 6p(5x)起,驱动都放在了专门的vender分区中,谷歌官网上只有工厂镜像,没有二进制文件的压缩包了。我们需要下载工厂镜像,然后提取出里面的vender.img镜像,刷入nexus 6p中就可以了。当然,经过实践发现,驱动问题可没这么简单就解决了哦。

8 刷机

(1) 首先到谷歌官网下载工厂镜像,下载地址如下:

https://developers.google.com/android/nexus/images

根据前面下载源码时候的编译代号(Build)来选择工厂镜像:

这里写图片描述

根据源码下载那一节,我们选择的android-6.0.1_r62版本对应的build是MTC20F,所以我们下载MTC20F的工厂镜像,博主将下载的文件保存在~/nexus6p目录中。解压后如下图所示:

这里写图片描述

(2) 配置adb命令
刷机需要使用adb和fastboot两个命令,先下载Android SDK,下载地址如下:

Android SDK下载地址

打开~/.bashrc文件,在末尾追加下面的代码,注意替换成你的sdk路径:

export PATH=$PATH:/home/fuchao/Android/Sdk/tools
export PATH=$PATH:/home/fuchao/Android/Sdk/platform-tools

重新打开shell,输入adb命令,看看配置是否生效。

(3) 解锁bootloader
bootloader可以简单理解为手机的bios,解锁bootloader之后才可以用fastboot命令刷机(线刷),相当于U盘重装系统,虽然不解锁也可以通过进入recovery模式的方法刷机(卡刷),相当于PE重装系统,但是强烈建议你解锁,因为解锁之后就不怕变砖了,万一玩坏了,最后还可以通过fastboot救回来,当然,解锁后就不能保修了,鱼和熊掌不可兼得嘛。刷机是个long long story,相关知识请看后面章节,我们先来操作。
谷歌在nexus 6p的bootloader前面加了一个oem锁,所以先得解oem锁,才能再解bootloader锁。
解oem锁很简单,开机进入设置、关于手机、连续点版本号6次,进入开发者模式,将OEM解锁选项打开,如下图所示:

这里写图片描述

建议一直打开OEM解锁,这样你就可以随时进入bootloader了,能够进入bootloader就不怕变砖了。
接着解bootloader锁,在shell中输入下面命令进入bootloader:

adb reboot bootloader

bootloader界面如图所示:

这里写图片描述

接着输入下面的代码解锁bootloader:

fastboot flashing unlock

然后会看到下面的界面:

这里写图片描述

用音量+选择yes,电源键确认,解锁完成。
解锁完毕后,开机会显示一个警告,请无视之,如图:

这里写图片描述

至此解锁完毕。

(4) 备份数据
只需要备份联系人和短信就可以了,应用不需要备份,因为还原的时候太慢了,博主用的是360手机助手备份的。值得一提的是,如果你想备份Google App然后刷完机再还原,这是不行的,这不是谷歌全家桶的正确安装姿势。

(5) 刷机ing
万事俱备,终于进入到了激动人心的刷机阶段了,首先进入解压后的工厂镜像文件夹中,博主的是~/nexus6p/angler-mtc20f:

cd ~/nexus6p/angler-mtc20f

重启进入bootloader,接着执行flash-base.sh脚本:

adb reboot bootloader
./flash-base.sh

接着刷入驱动,解压 image-angler-mtc20f.zip 文件后进入image-angler-mtc20f文件夹:

unzip image-angler-mtc20f.zip
cd image-angler-mtc20f

刷入vernder.img:

fastboot flash vendor vendor.img

驱动刷入完毕,接着就要刷入我们编译的其他镜像了,进入源码编译后生成的镜像目录android-6.0.1_r62/out/target/product/angler:

cd android-6.0.1_r62/out/target/product/angler

依次执行下面的命令,分别刷入镜像:

fastboot flash boot boot.img
fastboot flash recovery recovery.img
fastboot flash system system.img
fastboot flash userdata userdata.img
fastboot flash cache cache.img

好了,刷完之后重启就可以了:

fastboot reboot

到这里就成功的刷入了我们编译的aosp啦,不过别高兴的太早,问题还在后头呢。

(6) 一次批量刷入
到这里你应该明白了,其实刷机就是将我们编译出来的系统镜像和工厂镜像里面的vernder.img用fastboot命令写入手机中,那么我们可以将imge-angler-mtc20f中的镜像除vender.img以外全部替换成我们自己的,然后打包,用flash-all.sh脚本一次性刷入,省时省力,下面来操作。
将imge-angler-mtc20f下的文件除vender.img外全部替换成我们的,如下图所示:

这里写图片描述

在imge-angler-mtc20f下将所有文件压缩打成包imge-angler-mtc20f.zip:

cd ~/nexus6p/angler-mtc20f/image-angler-mtc20f
zip image-angler-mtc20f ./*

这个时候在 imge-angler-mtc20f 文件下会产生一个 imge-angler-mtc20f .zip的文件,接着替换掉上级文件夹angler-mtc20f中的同名文件,如果你想保留官方镜像文件,只需要重命名一下官方zip包就可以了。接着执行flash-all.sh脚本:

cd ~/nexus6p/angler-mtc20f
./flash-all.sh

上一张一次刷入的图片:

这里写图片描述

(7) 原生AOSP
刷机完毕,开机,原生android就呈现在你面前,是不是有点小激动外稍微又有点小失望呢,没有Google全家桶的AOSP丑的不是一点点啊,尤其是那个首页的Google搜索栏还停留在android 2.3的样式,相机和相册也是分分钟不想用啊,默认的声音简直是满大街山寨机即视感。作为谷歌粉的博主当然不能忍啦,但是别急,后面我们再来刷入Gapps,首先我们得解决一些”小”问题,你有没有发现原生aosp刷入nexus 6p后不能识别sim卡和蓝牙呢?也就是说,电话都打不了,每次开机都报个错误,说系统遇到一个问题,请联系设备制造商。下面我们就来解决这两个问题。

9 解决sim卡不识别问题

博主刷完aosp后遇到这个问题,sim卡怎么都识别不了,驱动明明已经刷好了呀,于是顺手百度了一下,结果差点吓尿,看到机锋论坛有人说是因为刷机前没有备份,把基带搞丢了,要返厂修理,于是赶紧看了下关于手机里面,基带一栏果然是空白的,吓的午觉都没睡好,这才刚买的手机呢不会就玩坏了吧?不管了,先喝口82年的雪碧冷静一下,然后开始谷歌大法,经过一天奋斗,终于解决了这个问题,擦,百度这个坑货。
博主在这里找到了解决方法:

https://github.com/anestisb/android-prepare-vendor

出现这个问题的原因是,先看英文:

if vendor.img is not generated when system.img is prepared for build, a few bits are broken that also require manual fixing (various symbolic links between two partitions, bytecode product packages, vendor shared libs dependencies, etc.).

解释一下,vender.img的生成需要以system.img为前提,其中涉及到一些符号、变量、分区、vernder依赖的包等都需要system.img中的信息。也就是说,谷歌的工厂镜像中的system.img和我们自己编的当然不同啦,所以直接刷入工厂镜像的vender.img就会出现问题,需要我们自己生成vender.img。搞清楚了原因,下面我们就来解决它。
首先下载这个工程:

git clone git@github.com:anestisb/android-prepare-vendor.git

接着进入这个工程,执行下面的命令:

./execute-all.sh -d angler -b MTC20F -o $(pwd) -i ~/nexus6p/angler-mtc20f-factory-4355fe06.zip -k

解释一下参数:

-d 后面是设备代号,nexus6p是angler
-b build id,分支编译时的代号
-o 输出目录
-i 下载好的谷歌工厂镜像路径
-k (不懂,好像是保持所有工厂镜像,优化数据)

耐心等待程序执行完毕,如图所示,中间的那块是API,自己看英文:

这里写图片描述

执行完毕,会在我们的设备代号文件下,生成一个vender文件夹,例如博主的nexus6p生成的路径就是 angler/mtc20f/vender,接着我们要将这个vender文件夹复制到我们的android源码目录下面,注意:不要用GUI来操作,必须用命令,因为其中有些文件你没有权限

//复制所有文件到android源码下面
sudo cp ~/android-prepare-vendor/angler/mtc20f/vendor ~/android-6.0.1_r62/
//进入android源码目录
cd ~/android-6.0.1_r62
//改变vernder所有者为自己,防止编译的时候没有权限
sudo chown -R fuchao:fuchao ./vender

接着开始编译喽:

source build/envsetup.sh 
lunch           
make clean      
make -j8    

编译完毕,在android-6.0.1_r62/out/target/product/angler 下面就可以看到我们自己遍出来的vender.img啦,赶紧刷入试试吧。上一张博主编译好的图:

这里写图片描述

10 解决开机报错的问题

刷完AOSP每次开机都会报个错:

There’s an internal problem with your device. Contact your manufacturer for details

中文则是:

您的设备内部出现了问题,请联系您的设备制造商了解详情。

如下图所示:

这里写图片描述

报这个错的原因是,同样先看英文:

The problem occurs because of a check that google implemented in Android 5.1 which compares /system/build.prop with the values found in /vendor/build.prop. If they differ you get that error message. All one has to do to get rid of the error is to change the 3 values in /vendor/build.prop according to the values in /system/build.prop.

翻译一下:从android 5.1开始谷歌有个检查,这个检查会比较/system/build.prop和/vendor/build.prop这两个文件中的3个变量值,如果不一致则导致这个错误,解决方法是手动修改,让两个文件中的这几个变量保持一致。

听起来很复杂,其实解决方法很简单啦,将下面的压缩包放在sd卡上,然后进recovery安装就可以了:

vendor-build-prop-fix-signed.zip

博主是通过刷入twrp recovery后来安装这个压缩包的,如果你还不懂recovery是怎么回事没关系,看完后面的刷入Gapps章节后你就懂了。
问题解决。

最后附上博主搜到的解决问题的xda上的帖子:

[FIX] build.prop variety fix (aka contact manufacturer problem)

作者说手动修改这些参数是很烦人的,于是他制作了这个压缩包,看他原话:

Doing this manually after every flash is cumbersome so I created an update-zip that corrects the values in /vendor/build.prop in a generic way (it should work no matter what rom you use). It mounts /system and /vendor, extracts the relevant information from /system/build.prop and creates a new /vendor/build.prop. I successfully tested it on my nexus 9 so I decided to release it to all of you.

当然每次刷入新的rom都要重复上面的操作:

Just install it via your recovery and you are done! This has to be done every time you flash a new rom

xda,你值得拥有。

11 刷入Gapps

好了,解决了前面两个问题,下面我们就来刷入谷歌全家桶喽。先科普一下,谷歌全家桶,指的是Google开发的一系列App和Google服务,包括: Google Play,Google Now,Google Service,Google Play,Google Driver等。由于终所周知的原因,Google Service不能像普通app那样安装,反正博主是没有安装成功,没有Google Service其余的Google App即使安装了也无法运行,所以必须使用Recovery的形式刷进去,所谓Recovery,你可以简单的理解为windows pe系统,这里博主推荐twrp recovery。去哪儿找哪些Google app呢?放心,早有第三方把谷歌一套收集打包好了,只等你刷进去了,这些包,行话叫Gapps,这里博主推荐OpenGapps。下面我们来刷入OpenGapps。

首先,到这里选择你的设备对应的twrp recovery版本去下载:

https://twrp.me/Devices

博主使用nexus6p下载地址:

Download twrp-3.0.2-2-angler.img

注意:一定要下载你的设备对应的twrp版本
接着刷入twrp,先用adb reboot bootloader命令进入bootloader,接着用下面的命令刷入twrp:

fastboot flash recovery twrp-3.0.2-2-angler.img

刷入twrp成功之后,接着我们去下载OpenGapps。OpenGapps的官网如下:

http://opengapps.org

平台选择Arm64。OpenGapps有很多版本,各版本介绍如下:

  • stock :最为贴近 Nexus 机型体验的 GApps 版本,包含了 Nexus 机型所预装的所有 Google 服务和 Google 应用。需要注意的是,这个版本会用 Chrome 、 Google Now Launcher 、 Google Keybord 等 Google 应用替换掉 CM 系 ROM 中那些基于 AOSP 代码的相关应用。
  • full :与 stock 版所包含的内容相同,但不会替换 AOSP 应用。
  • mini :包含了完整的 Google 服务框架和主流 Google 应用,去掉了 Google Docs 等文档处理应用
  • micro :包含了完整的 Google 服务框架和少数 Google 应用,如 Gmail 、 Google Calender 、 Google Now Launcher 。
  • nano :包含完整的 Google 服务框架但不包含多余的 Google 应用。
  • pico:包含了最基础的 Google 服务框架,体积最小,一些依赖完整 Google 框架的应用(如 Google Camera )将无法运行。

这里我们选择stock就行了,保持和官方预装的App一致。如下图:

这里写图片描述

然而,博主下载了很多次始终下载失败,这个OpenGapps实在很难下载成功,于是博主从下面的帖子中的百度网盘下载了一个:

http://tieba.baidu.com/p/4646253729

博主下载的文件是open_gapps-arm64-6.0-stock-20160701.zip,接着将这个文件放进手机的sdcard上,然后进入bootloader,按两下音量减键,看到recovery,电源键确认进入,此时屏幕下方会有一个滑动条,向右拉即可:

这里写图片描述

点击安装,选择我们放在sdcard上的Gapps,然后把滑动条拉到右边即可开始安装,

这里写图片描述

安装完毕,重启系统就可以了,第一次开机可能比较慢。如果开机后不停的弹框报错,请把菜单拉下来,然后点击设置进入应用,给Google Service打开所有权限就行了。

特别注意:刷完aosp后必须马上刷入Gapps,中途不能开机,否则会卡在开机Logo

12 root

root的原理就是把所有者为root的su文件放入系统文件/system/bin、/sbin等目录下,放入之后就拥有了root权限,但是这个放入过程需要root权限,所以这就是矛盾的地方,怎么想办法放进去呢?一般思路是利用系统的漏洞,例如具有root权限的进程对栈溢出,或者adbd提权漏洞等。但是漏洞越来越难找了,所以就有了下面recovery的方式,nexus6p的root方法很简单,用twrp将下面的SuperSu刷入就可以了,前面我们已经用twrp刷入了OpenGapps,这里OpenGapps换成SuperSu刷入就可以了,不再多说。
SuperSu的下载地址如下:

SuperSU-v2.78 下载地址

13 实践记录

下面记录博主在实践过程中遇到的问题,以及怎么解决这些问题的心路历程。博主的体会就是:能用谷歌坚决不用百度这个渣渣。每当爆出一个error,如果全是英文的,博主就很happy,因为Google一下一定能搜到,如果是china特色的问题就很头大,例如:QQ,以及GFW。

(1) 源码下载失败问题
博主在下载源码工程中遇到一个错误:

A TLS packet with unexpected length was received

如图:

这里写图片描述

导致这个错误的原因是,谷歌对同一个IP的匿名访问次数有限制,解决方法是在下载源码前,配置谷歌的访问验证,也就是设置Git的cookie,具体参见上文。

(2) fastboot 卡在 waiting for device
在使用fastboot命令刷入镜像的时候总是卡在waiting for devices

$ fastboot flash boot boot.img
   < waiting for device >

看看devices:

$ fastboot devices
   no permissions  fastboot

原来是fastboot没有权限,那就给fastboot增加权限:

$ whereis fastboot             //先看fastboot在哪儿
$ sudo chown root:root /bin/fastboot       //改为root用户所有
$ sudo chmod +s /bin/fastboot          //执行的时候就会自动拥有root权限
$ fastboot devices
   7D89BCD6        fastboot

注意替换你的fastboot位置。

(3) nexus 6p的驱动问题
编译完AOSP后,怎么处理驱动问题,博主在这里卡了很长时间,最开始,在谷歌的官网上看到了放出nexus二进制驱动的地方:

https://developers.google.com/android/nexus/drivers

但是,始终没有找到nexus 6p的二进制驱动,于是又看了很多编译Android 6.0的源码的博客,要么就是编译完了之后启动模拟器就结束了,要么就是刷入nexus 5的教程,直接在官网下载一个驱动包,解压后执行一个shell脚本,再编译就出现驱动了。
后来,在这里看到一篇博客,知道了nexus 6p的驱动是在工厂镜像中:

http://www.jianshu.com/p/99ba74eda9c4

不过作者说的太简单了,又没有操作步骤。然后自己摸索,将vernder.img刷入nexus 6p,结果开机发现sim卡无法识别,百度一下,发现基带没了,尼玛,吓了一跳。赶紧谷歌,后来在这里发现了别人在谷歌论坛上提的问题:

With a custom AOSP build on the Nexus 5X (bullhead), the SIM Card fails to work

慢慢看,居然有人解决了,卧槽,赶紧试试,看中间那个叫 Jared Tsai 的说:

I found a solution in the github.
https://github.com/anestisb/android-prepare-vendor

The script will download a Google factory image, extract necessary files, and create a vendor folder for aosp.
Move the created vendor folder to aosp root and build the images again, the vendor.img will be in the out folder.
Just flash all the images into device, the radio works as normal.

然后博主打开那个github后,就一脸懵逼了,尼玛,全是英文,不知所云,怎么用的也不写清楚,还好下面有个example,博主clone之后,执行了一下脚本,擦,各种参数说明都出来了,再对比一下example之后,嗯,so easy。
然后再看上面 Jared Tsai 说的,把生成的vender文件夹复制到源码文件夹下,开编。擦,过一会儿报个错,说那个vender文件找不到target,无法编译,根据博主多年经验,这很明显是makefile文件无法识别嘛,于是又谷歌,在谷歌论坛上发现别人也有同样的问题,原因是复制的时候vender文件夹的文件丢失了,没有复制全,于是博主又在命令行下面用sudo来复制,然后修改所有文件的所有者为自己,这样再编译就ok了,终于编出来了,费了好大劲才解决驱动问题。

(4) Open Gapps下载、刷入的问题
博主开始安装Google Service时,打算用360手机助手,直接把刷机前备份的Google App还原到手机中,结果发现太慢了,几十分钟才还原了两个,总共有28个,遂无法忍受放弃了,然后又在360市场下载专门安装谷歌服务的App,结果这些谷歌服务安装器无一例外,全部安装失败,其结果就是不停的弹框报错,谷歌服务已停止运行。后来在下面这个地方知道了OpenGapps,这才了解了正确的安装姿势:

好马配好鞍,安装谷歌服务框架的正确姿势

虽然OpenGapps可以直接打开,但是下载OpenGapps却要翻&墙,博主下了4、5次,下着下着就没有速度了,有一次下载到99%就卡住了,实在不行放弃了,到OpenGapps的贴吧去下了一个别人上传到网盘的版本,解决问题。

接着刷入,然而,并不是一开始就成功了,刷完AOSP,解决完sim卡的问题,再来刷入OpenGapps,结果一开机就报错,说无法启动配置向导,然后不停的弹窗,无法忍受,接着百度,在机锋论坛看到别人的帖子,才知道刷完ROM,也就是我们编的AOSP后要马上刷入OpenGapps,不能开机,否则就会不停的弹窗报错。然而刷完之后,还是不停弹窗报谷歌服务已停止,再百度一下,给谷歌服务打开所有权限,世界就平静了,终于成功了。

(5) 刷机的问题
刷机的水真是深啊,各种知识,各种概念,各种术语,什么卡刷,线刷,解锁,救砖,oem锁,bootloader锁,进recovery,双清,OTA升级,xposed,绿色守护等,博主补了一个上午,看贴吧、论坛才把这些概念搞懂了。非得要自己动手操作,才能体会这些东西。

14 总结

纸上得来终觉浅,绝知此事要躬行。终于把AOSP从谷歌官网下载、编译、刷入nexus 6p了,接下来就可以开始定制系统了。
博客中所有内容都是经过博主亲手实践得来的,转载请注明出处,谢谢。如果你在实践中遇到什么问题,欢迎给我留言。
欢迎关注我的博客——-梧桐那时雨。

15 转载请注明来自”梧桐那时雨”的博客:http://blog.csdn.net/fuchaosz/article/details/52473660

Tips:
如果觉得这篇博客对你有帮助或者喜欢博主的写作风格,就关注一下博主或者给博主留个言呗,鼓励博主创作出更多优质博客,Thank you.

作者:fuchaosz 发表于2016/9/27 14:37:42 原文链接
阅读:648 评论:0 查看评论

数据结构之二叉树的非递归实现

$
0
0

非递归实现二叉树的前序,中序以及后序遍历.

    在非递归实现二叉树的前序,中序以及后序遍历时用到了栈这种数据结构.

    一.非递归实现前序遍历.

    我们知道二叉树的前序遍历是先访问根结点,然后访问左子树,最后再访问右子树.那仫如何非递归去前序遍历一颗树呢?

       

      二 .非递归实现中序遍历.

      非递归的中序遍历类似前序遍历只不过将访问当前结点的位置切换到访问左子树对应到代码中就是在入栈之后再访问当前结点.

     

	void PrevOrderNoR()
	{
		stack<Node *> s;
		Node *cur=_root;
		while (cur || !s.empty())
		{
			while (cur)    //一直向左遍历直到左为空时跳出
			{
				cout<<cur->_data<<" ";   //访问根结点
				s.push(cur);
				cur=cur->_left;
			}
			//此时已经访问过当前结点且当前结点的左子树为空
			Node *top=s.top();
			s.pop();
			//访问当前结点的右子树
			cur=top->_right;
		}
		cout<<endl;
	}
	void InOrderNoR()
	{
		//类似非递归的先序遍历,不过要在访问过当前结点的左子树之后再访问根结点
		stack<Node *> s;
		Node *cur=_root;
		while (cur || !s.empty())
		{
			while (cur)
			{
				s.push(cur);
				cur=cur->_left;
			}
			Node *top=s.top();
			cout<<top->_data<<" ";
			s.pop();
			cur=top->_right;
		}
		cout<<endl;
	}


 

      三.非递归实现后序遍历.

       

       如何使用非递归的方式遍历一颗树呢?我们知道后序遍历一棵树的原则是:先遍历左子树,再遍历右子树,最后再遍历根结点.而问题就出在根结点是在遍历了左子树和右子树之后才遍历的,如何在保证遍历了左右子树之后还能找到当前的根结点呢?

       刚开始的想法类似前序遍历和中序遍历.一直遍历该树的左子树直到为空时不再压栈,只要停止压栈那仫栈顶结点的左子树已经访问过了而且一定为空,此时另当前元素为栈顶元素,如果判断当前结点的右子树为空或者右子树已经访问过了直接访问当前结点,如果右子树不为空或者右子树未被访问过则访问右子树.在实现中用到了一个prev指针过来记录当前结点的前一个结点.

      

	void PostOrderNoR()
	{
		stack<Node *> s;
		Node *cur=_root;
		Node *prev=NULL;
		while (cur || !s.empty())
		{
			while (cur)    //一直向左遍历直到左为空时跳出
			{
				s.push(cur);
				cur=cur->_left;
			}
			cur=s.top();
			//当前节点的右孩子为空或者已经访问过当前结点的右孩子则访问当前结点
			if (cur->_right == NULL || cur->_right == prev)
			{ 
				cout<<cur->_data<<" ";
				prev=cur;
				s.pop();
				cur=NULL;
			}
			else
			{
				//否则访问右孩子
				cur=cur->_right;
			}
		}
		cout<<endl;
	}


 

 完整代码实现>

    

       BinaryTree.h

    

template<typename T>
struct BinaryTreeNode
{
	BinaryTreeNode(const T& data=T())
		:_data(data)
		,_left(NULL)
		,_right(NULL)
	{}
	T _data;
	BinaryTreeNode *_left;
	BinaryTreeNode *_right;
};

template<typename T>
class BinaryTree
{
	typedef BinaryTreeNode<T> Node;
public:
	BinaryTree()
		:_root(NULL)
	{}
	BinaryTree(const T *array,size_t size,const T& invalid)
	{
		size_t index=0;
		_root=_CreatTree(array,size,index,invalid);
	}
	BinaryTree(const BinaryTree<T>& bt)
	{
		_root=_Copy(bt._root);
	}
	//BinaryTree<T>& operator=(const BinaryTree<T>& bt)
	//{
	//	//传统的写法
	//	if (this != &bt)
	//	{
	//		Node *root=_Copy(bt._root);
	//		_Destroy(_root);
	//		_root=root;
	//	}
	//	return *this;
	//}
	BinaryTree<T>& operator=(const BinaryTree<T>& bt)
	{
		//现代的写法
		if (this != &bt)   //防止自赋值
		{
			BinaryTree<T> tmp(bt);
			std::swap(_root,tmp._root);
		}
		return *this;
	}
	~BinaryTree()
	{
		_Destroy(_root);
	}
public:
	//递归的先序遍历,中序遍历,后序遍历
	void PrevOrder()
	{
		_PrevOrder(_root);
		cout<<endl;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout<<endl;
	}
	void PostOrder()
	{
		_PostOrder(_root);
		cout<<endl;
	}
	//非递归的先序遍历,中序遍历,后序遍历
	void PrevOrderNoR()
	{
		stack<Node *> s;
		Node *cur=_root;
		while (cur || !s.empty())
		{
			while (cur)    //一直向左遍历直到左为空时跳出
			{
				cout<<cur->_data<<" ";   //访问根结点
				s.push(cur);
				cur=cur->_left;
			}
			//此时已经访问过当前结点且当前结点的左子树为空
			Node *top=s.top();
			s.pop();
			//访问当前结点的右子树
			cur=top->_right;
		}
		cout<<endl;
	}
	void InOrderNoR()
	{
		//类似非递归的先序遍历,不过要在访问过当前结点的左子树之后再访问根结点
		stack<Node *> s;
		Node *cur=_root;
		while (cur || !s.empty())
		{
			while (cur)
			{
				s.push(cur);
				cur=cur->_left;
			}
			Node *top=s.top();
			cout<<top->_data<<" ";
			s.pop();
			cur=top->_right;
		}
		cout<<endl;
	}
	void PostOrderNoR()
	{
		stack<Node *> s;
		Node *cur=_root;
		Node *prev=NULL;
		while (cur || !s.empty())
		{
			while (cur)    //一直向左遍历直到左为空时跳出
			{
				s.push(cur);
				cur=cur->_left;
			}
			cur=s.top();
			//当前节点的右孩子为空或者已经访问过当前结点的右孩子则访问当前结点
			if (cur->_right == NULL || cur->_right == prev)
			{ 
				cout<<cur->_data<<" ";
				prev=cur;
				s.pop();
				cur=NULL;
			}
			else
			{
				//否则访问右孩子
				cur=cur->_right;
			}
		}
		cout<<endl;
	}
	void LevelOrder()   //层序遍历
	{
		_LevelOrder(_root);
		cout<<endl;
	}
	size_t Size()       //求该树的总结点数
	{
		size_t size=_Size(_root);
		return size;
	}
	size_t Depth()     //求该树的深度
	{
		size_t depth=_Depth(_root);
		return depth;
	}
	size_t LeafSize()   //求一个树叶子节点的总数
	{
		size_t leaf=_LeafSize(_root);
		return leaf;
	}
	size_t GetKLevel(int level)   //level层的所有节点的个数
	{
		size_t count=_GetKLevel(_root,level);
		return count;
	}
	Node *FindNode(const T& x=T())
	{
		return _FindNode(_root,x);
	}
protected:
	Node *_CreatTree(const T *array,size_t size,size_t& index,const T& invalid)
	{
		assert(array);
		Node *root=NULL;
		if (index < size && array[index] != invalid)
		{
			root=new Node(array[index]);
			root->_left=_CreatTree(array,size,++index,invalid);
			root->_right=_CreatTree(array,size,++index,invalid);
		}
		return root;
	}
	Node *_Copy(Node *root)
	{
		//proot是拷贝的新树的根节点
		Node *cur=root;
		Node *proot=NULL;
		if (cur)
		{
			proot=new Node(cur->_data);
			proot->_left=_Copy(cur->_left);
			proot->_right=_Copy(cur->_right);
		}
		return proot;
	}
	void _Destroy(Node *root)
	{
		Node *cur=root;
		if (cur)
		{
			_Destroy(cur->_left);
			_Destroy(cur->_right);
			delete cur;
			cur=NULL;
		}
	}
protected:
	void _PrevOrder(Node *root)
	{
		Node *cur=root;
		if (cur)
		{
			cout<<cur->_data<<" ";
			_PrevOrder(root->_left);
			_PrevOrder(root->_right);
		}
	}
	void _InOrder(Node *root)
	{
		Node *cur=root;
		if (cur)
		{
			_InOrder(cur->_left);
			cout<<cur->_data<<" ";
			_InOrder(cur->_right);
		}
	}
	void _PostOrder(Node *root)
	{
		Node *cur=root;
		if (cur)
		{
			_PostOrder(cur->_left);
			_PostOrder(cur->_right);
			cout<<cur->_data<<" ";
		}
	}
	void _LevelOrder(Node *root)
	{
		queue<Node *> q;
		Node *cur=root;
		q.push(cur);
		while(!q.empty())
		{
			Node *front=q.front();
			cout<<front->_data<<" ";
			if (front->_left != NULL)
				q.push(front->_left);
			if (front->_right != NULL)
				q.push(front->_right);
			q.pop();
		}
	}
	////方法一.
	//size_t _Size(Node *root)
	//{
	//	if (root == NULL)
	//	{
	//		return 0;     //空树
	//	}
	//	return 1+_Size(root->_left)+_Size(root->_right);
	//}
	//方法二.
	size_t _Size(Node *root)
	{
		static size_t count=0;
		if (root == NULL)
			return 0;
		count++;        //类似前序遍历.
		_Size(root->_left);
		_Size(root->_right);
		return count;
	}
	size_t _Depth(Node *root)
	{
		Node *cur=root;
		if (root == NULL)
			return 0;
		size_t depth1=_Depth(cur->_left)+1;
		size_t depth2=_Depth(cur->_right)+1;
		return depth1>depth2?depth1:depth2;
	}
	size_t _LeafSize(Node *root)
	{
		Node *cur=root;
		if (cur == NULL)   //空树
			return 0;
		else if ((cur->_left == NULL) && (cur->_right == NULL))
			//叶子结点--一个结点即没有左子树也没有右子树
			return 1;
		else
			return _LeafSize(cur->_left)+_LeafSize(cur->_right);
	}
	size_t _GetKLevel(Node *root,int level)
	{
		size_t count=0;
		if (root == NULL)
			return 0;
		if (level == 1)
			count++;
		else
		{
			count += _GetKLevel(root->_left,level-1);
			count += _GetKLevel(root->_right,level-1);
		}
		return count;
	}
	Node *_FindNode(Node *root,const T& x)
	{
		if (root == NULL)
			return NULL;
		if(root->_data == x)
			return root;
		Node *ret=_FindNode(root->_left,x);
		//当在当前结点的左子树中未找到该结点去右子树找
		if (ret == NULL)
		{
			ret=_FindNode(root->_right,x);
		}
		return ret;
	}
protected:
	Node *_root;
};


 

 

       test.cpp

      

void test1()
{
	int array[]={1,2,3,'#','#',4,'#','#',5,6};
	size_t size=sizeof(array)/sizeof(array[0]);
	BinaryTree<int> bt(array,size,'#');
	cout<<"前序遍历>";
	bt.PrevOrderNoR();   //1  2  3  4  5  6
	cout<<"中序遍历>";
	bt.InOrderNoR();     //3  2  4  1  6  5
	cout<<"后序遍历>";
	bt.PostOrderNoR();   //3  4  2  6  5  1
	cout<<"层序遍历>";
	bt.LevelOrder();     //1  2  5  3  4  6
	cout<<"Size>"<<bt.Size()<<endl;
	cout<<"Depth:"<<bt.Depth()<<endl;
	cout<<"LeafSize:"<<bt.LeafSize()<<endl;   //3  
	cout<<"Level1:"<<bt.GetKLevel(1)<<endl;  //1
	cout<<"Level2:"<<bt.GetKLevel(2)<<endl;  //2
	cout<<"Level3:"<<bt.GetKLevel(3)<<endl; //3

	BinaryTreeNode<int> *ret=bt.FindNode(6);
	assert(ret);
	cout<<"FindNode:6>"<<ret->_data<<endl;    //找到.
	ret=bt.FindNode(10);
	cout<<"FindeNode:10>"<<ret<<endl;         //没找到.
}


 

 

  结果展示>

      

     

      

 

作者:qq_34328833 发表于2016/9/27 15:24:06 原文链接
阅读:328 评论:0 查看评论

Android 6.0 - 动态权限管理的解决方案

$
0
0

Android 6.0 - 动态权限管理的解决方案

Android 6.0版本(Api 23)推出了很多新的特性, 大幅提升了用户体验, 同时也为程序员带来新的负担. 动态权限管理就是这样, 一方面让用户更加容易的控制自己的隐私, 一方面需要重新适配应用权限. 时代总是不断发展, 程序总是以人为本, 让我们为应用添加动态权限管理吧! 这里提供了一个非常不错的解决方案, 提供源码, 项目可以直接使用.

Android系统包含默认的授权提示框, 但是我们仍需要设置自己的页面. 原因是系统提供的授权框, 会有不再提示的选项. 如果用户选择, 则无法触发授权提示. 使用自定义的提示页面, 可以给予用户手动修改授权的指导.

本文示例的GitHub下载地址

在Api 23中, 权限需要动态获取, 核心权限必须满足. 标准流程:


如果用户点击, 不再提示, 则系统授权弹窗将不会弹出. 流程变为:


流程就这些, 让我们看看代码吧.

1. 权限

在AndroidManifest中, 添加两个权限, 录音修改音量.


<!--危险权限-->
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

    <!--一般权限-->
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

危险权限必须要授权, 一般权限不需要.

检测权限类

/**
 * 检查权限的工具类
 * <p/>
 * Created by wangchenlong on 16/1/26.
 */
public class PermissionsChecker {
    private final Context mContext;

    public PermissionsChecker(Context context) {
        mContext = context.getApplicationContext();
    }

    // 判断权限集合
    public boolean lacksPermissions(String... permissions) {
        for (String permission : permissions) {
            if (lacksPermission(permission)) {
                return true;
            }
        }
        return false;
    }

    // 判断是否缺少权限
    private boolean lacksPermission(String permission) {
        return ContextCompat.checkSelfPermission(mContext, permission) ==
                PackageManager.PERMISSION_DENIED;
    }
}

2. 首页

假设首页需要使用权限, 在页面显示前, 即onResume时, 检测权限,
如果缺少, 则进入权限获取页面; 接收返回值, 拒绝权限时, 直接关闭.

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_CODE = 0; // 请求码

    // 所需的全部权限
    static final String[] PERMISSIONS = new String[]{
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.MODIFY_AUDIO_SETTINGS
    };

    @Bind(R.id.main_t_toolbar) Toolbar mTToolbar;

    private PermissionsChecker mPermissionsChecker; // 权限检测器

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        setSupportActionBar(mTToolbar);

        mPermissionsChecker = new PermissionsChecker(this);
    }

    @Override protected void onResume() {
        super.onResume();

        // 缺少权限时, 进入权限配置页面
        if (mPermissionsChecker.lacksPermissions(PERMISSIONS)) {
            startPermissionsActivity();
        }
    }

    private void startPermissionsActivity() {
        PermissionsActivity.startActivityForResult(this, REQUEST_CODE, PERMISSIONS);
    }

    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 拒绝时, 关闭页面, 缺少主要权限, 无法运行
        if (requestCode == REQUEST_CODE && resultCode == PermissionsActivity.PERMISSIONS_DENIED) {
            finish();
        }
    }
}

核心权限必须满足, 如摄像应用, 摄像头权限就是必须的, 如果用户不予授权, 则直接关闭.

3. 授权页

授权页, 首先使用系统默认的授权页, 当用户拒绝时, 指导用户手动设置, 当用户再次操作失败后, 返回继续提示. 用户手动退出授权页时, 给使用页发送授权失败的通知.

/**
 * 权限获取页面
 * <p/>
 * Created by wangchenlong on 16/1/26.
 */
public class PermissionsActivity extends AppCompatActivity {

    public static final int PERMISSIONS_GRANTED = 0; // 权限授权
    public static final int PERMISSIONS_DENIED = 1; // 权限拒绝

    private static final int PERMISSION_REQUEST_CODE = 0; // 系统权限管理页面的参数
    private static final String EXTRA_PERMISSIONS =
            "me.chunyu.clwang.permission.extra_permission"; // 权限参数
    private static final String PACKAGE_URL_SCHEME = "package:"; // 方案

    private PermissionsChecker mChecker; // 权限检测器
    private boolean isRequireCheck; // 是否需要系统权限检测

    // 启动当前权限页面的公开接口
    public static void startActivityForResult(Activity activity, int requestCode, String... permissions) {
        Intent intent = new Intent(activity, PermissionsActivity.class);
        intent.putExtra(EXTRA_PERMISSIONS, permissions);
        ActivityCompat.startActivityForResult(activity, intent, requestCode, null);
    }

    @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getIntent() == null || !getIntent().hasExtra(EXTRA_PERMISSIONS)) {
            throw new RuntimeException("PermissionsActivity需要使用静态startActivityForResult方法启动!");
        }
        setContentView(R.layout.activity_permissions);

        mChecker = new PermissionsChecker(this);
        isRequireCheck = true;
    }

    @Override protected void onResume() {
        super.onResume();
        if (isRequireCheck) {
            String[] permissions = getPermissions();
            if (mChecker.lacksPermissions(permissions)) {
                requestPermissions(permissions); // 请求权限
            } else {
                allPermissionsGranted(); // 全部权限都已获取
            }
        } else {
            isRequireCheck = true;
        }
    }

    // 返回传递的权限参数
    private String[] getPermissions() {
        return getIntent().getStringArrayExtra(EXTRA_PERMISSIONS);
    }

    // 请求权限兼容低版本
    private void requestPermissions(String... permissions) {
        ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
    }

    // 全部权限均已获取
    private void allPermissionsGranted() {
        setResult(PERMISSIONS_GRANTED);
        finish();
    }

    /**
     * 用户权限处理,
     * 如果全部获取, 则直接过.
     * 如果权限缺失, 则提示Dialog.
     *
     * @param requestCode  请求码
     * @param permissions  权限
     * @param grantResults 结果
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE && hasAllPermissionsGranted(grantResults)) {
            isRequireCheck = true;
            allPermissionsGranted();
        } else {
            isRequireCheck = false;
            showMissingPermissionDialog();
        }
    }

    // 含有全部的权限
    private boolean hasAllPermissionsGranted(@NonNull int[] grantResults) {
        for (int grantResult : grantResults) {
            if (grantResult == PackageManager.PERMISSION_DENIED) {
                return false;
            }
        }
        return true;
    }

    // 显示缺失权限提示
    private void showMissingPermissionDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(PermissionsActivity.this);
        builder.setTitle(R.string.help);
        builder.setMessage(R.string.string_help_text);

        // 拒绝, 退出应用
        builder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() {
            @Override public void onClick(DialogInterface dialog, int which) {
                setResult(PERMISSIONS_DENIED);
                finish();
            }
        });

        builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
            @Override public void onClick(DialogInterface dialog, int which) {
                startAppSettings();
            }
        });

        builder.show();
    }

    // 启动应用的设置
    private void startAppSettings() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName()));
        startActivity(intent);
    }
}

注意isRequireCheck参数的使用, 防止和系统提示框重叠.
系统授权提示: ActivityCompat.requestPermissions, ActivityCompat兼容低版本.

效果


关键部分就这些了, 动态权限授权虽然给程序员带来了一些麻烦, 但是对用户还是很有必要的, 我们也应该欢迎, 毕竟每个程序员都是半个产品经理.

危险权限列表


作者:zhangyufeng0126 发表于2016/9/27 17:27:44 原文链接
阅读:201 评论:0 查看评论

最熟悉的陌生人:ListView 中的观察者模式

$
0
0

RecyclerView 得宠之前,ListView 可以说是我们用的最多的组件。之前一直没有好好看看它的源码,知其然不知其所以然。

今天我们来窥一窥 ListView 中的观察者模式。

不熟悉观察者模式的可以看看这篇 观察者模式 : 一支穿云箭,千军万马来相见 巩固一下。

在我们使用 ListView 的过程中,经常需要修改 Item 的状态,比如添加、删除、选中等等,通常的操作是在对数据源进行操作后,调用 notifyDataSetChanged() ,比如:

    public void addData(String data) {
        if (mData != null) {
            mData.add(data);
            notifyDataSetChanged();
        }
    }

随后 ListView 中的数据就会更新,我们可以猜到这个过程是把全部 Item View 重新绘制、数据绑定了一遍,这个场景跟观察者模式很一致,具体怎么实现的呢

前方高能预警,代码太多看不下去的可以先翻到篇尾看看流程图,有点印象再回来继续啃的,不然容易晕。

1.首先我们跟进去看下 notifyDataSetChanged() 源码,进入了系统的 BaseAdapter

    /**
     * Notifies the attached observers that the underlying data has been changed
     * and any View reflecting the data set should refresh itself.
     */
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

看注释,“通知观察者数据已经改变,任何和数据集绑定的 View 都应该刷新”,的确是观察者模式。

那发布者、观察者是谁?在什么时候注册的?观察者的 notifyChanged() 方法又做了什么呢?

2.在 BaseAdapter 中我们可以看到这几个方法:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public boolean hasStableIds() {
        return false;
    }

    /**
    * BaseAdapter 提供了 注册订阅方法
    */
    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    /**
    * 还提供了 解除订阅方法
    */
    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    /**
     * 数据更新时通知观察者
     */
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

    /**
     * 提醒观察者散了,别看了,数据不可用了
     * /
    public void notifyDataSetInvalidated() {
        mDataSetObservable.notifyInvalidated();
    }
    //省略无关代码
}

BaseAdapter 提供了 注册订阅、解除订阅、提醒观察者数据更新、告诉观察者数据不可用 等关键方法。

其中 DataSetObservable 是发布者:

/**
 * A specialization of {@link Observable} for {@link DataSetObserver}
 * that provides methods for sending notifications to a list of
 * {@link DataSetObserver} objects.
 */
public class DataSetObservable extends Observable<DataSetObserver> {
    /**
     * 发出更新提醒
     */
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

    /**
     * 发出数据集无法使用通知
     */
    public void notifyInvalidated() {
        synchronized (mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onInvalidated();
            }
        }
    }
}

可以看到 notifyChanged 方法的注释中,是倒序遍历观察者集合并进行通知,这是为了避免观察者列表的 iterator 被使用时,进行删除操作导致出问题。

DataSetObservable 继承自 Observable < DataSetObserver > ,看下 Observable 源码:

public abstract class Observable<T> {
    /**
     * 观察者列表,不能重复,不能为空
     */
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    /**
     * 注册一个观察者,不能重复,不能为空
     */
    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

    /**
     * 解除注册一个观察者
     */
    public void unregisterObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            int index = mObservers.indexOf(observer);
            if (index == -1) {
                throw new IllegalStateException("Observer " + observer + " was not registered.");
            }
            mObservers.remove(index);
        }
    }

    /**
     * 移除所有观察者
     */
    public void unregisterAll() {
        synchronized(mObservers) {
            mObservers.clear();
        }
    }
}

DataSetObserver 就是观察者抽象类,将来需要被具体观察者者继承:

/**
 * DataSetObserver must be implemented by objects which are added to a DataSetObservable.
 */
public abstract class DataSetObserver {
    /**
     * 数据改变时调用
     */
    public void onChanged() {
        // Do nothing
    }

    /**
     * 数据不可用时调用
     */
    public void onInvalidated() {
        // Do nothing
    }
}

了解发布者、观察者基类后,接下来去看下在什么时候进行注册、通知。

3.ListView.setAdapter 源码:

public void setAdapter(ListAdapter adapter) {
        //移除旧的观察者
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        //省略不相关内容...

        if (mAdapter != null) {
            //...

            //初始化新观察者并注册
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            //...
            if (mItemCount == 0) {
                // Nothing selected
                checkSelectionChanged();
            }
        } else {
            mAreAllItemsSelectable = true;
            checkFocus();
            // Nothing selected
            checkSelectionChanged();
        }

        requestLayout();
    }

可以看到在 ListView.setAdapter 方法中,先解除旧的观察者,然后初始化了新的观察者 AdapterDataSetObserver 并注册。

而 AdapterDataSetObserver 定义在 ListView 的父类 AbsListView 中:

class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }
    }

AdapterDataSetObserver 继承自 AdapterView.AdapterDataSetObserver,在 onChanged 和 onInvalidated 方法中先调用 AdapterView.AdapterDataSetObserver 对应的方法,然后调用了 mFastScroll.onSectionsChanged();

先看 AdapterView.AdapterDataSetObserver :

class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            //更新 数据修改状态
            mDataChanged = true;
            //更新 数据数量
            mOldItemCount = mItemCount;
            //更新 ItemView 数量
            mItemCount = getAdapter().getCount();

            // 监测是否有数据之前不可用、现在可用
            // 由于 BaseAdapter.hasStableIds() 默认返回 false ,所以我们直接看 else
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
               //记录当前状态,接下来刷新时要用到这些状态
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            if (AdapterView.this.getAdapter().hasStableIds()) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = AdapterView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;

            checkFocus();
            requestLayout();
        }

        public void clearSavedState() {
            mInstanceState = null;
        }
    }

看 onChanged() 方法,这个方法中先后更新了 数据更新状态(mDataChanged ),数据数量,而由于 BaseAdapter.hasStableIds() 默认返回 false , 所以我们直接看 else 情况下 rememberSyncState 方法:

 /**
     * 保存屏幕状态
     *
     */
    void rememberSyncState() {
        if (getChildCount() > 0) {
            mNeedSync = true;
            mSyncHeight = mLayoutHeight;
            if (mSelectedPosition >= 0) {
                //如果选择了内容,保存选择的位置和距离顶部的偏移量
                View v = getChildAt(mSelectedPosition - mFirstPosition);
                mSyncRowId = mNextSelectedRowId;
                mSyncPosition = mNextSelectedPosition;
                if (v != null) {
                    mSpecificTop = v.getTop();
                }
                mSyncMode = SYNC_SELECTED_POSITION;
            } else {
                // 如果没有选择内容就保存第一个 View 的偏移量
                View v = getChildAt(0);
                T adapter = getAdapter();
                if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
                    mSyncRowId = adapter.getItemId(mFirstPosition);
                } else {
                    mSyncRowId = NO_ID;
                }
                mSyncPosition = mFirstPosition;
                if (v != null) {
                    mSpecificTop = v.getTop();
                }
                mSyncMode = SYNC_FIRST_POSITION;
            }
        }
    }

rememberSyncState 方法中针对是否选择了 item,保存了当前状态,重新绘制时会恢复状态。当我们滑动 ListView 后进行刷新数据操作,ListView 并没有滚动到顶部,就是因为这个方法的缘故。

回到 AdapterDataSetObserver.onChanged() 方法:

class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            //更新 数据修改状态
            mDataChanged = true;
            //更新 数据数量
            mOldItemCount = mItemCount;
            //更新 ItemView 数量
            mItemCount = getAdapter().getCount();

            // 监测是否有数据之前不可用、现在可用
            // 由于 BaseAdapter.hasStableIds() 默认返回 false ,所以我们直接看 else
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
               //记录当前状态,接下来刷新时要用到这些状态
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }
        //...
}

保存数据状态后,进入 chekFocus 方法:


    void checkFocus() {
        final T adapter = getAdapter();
        final boolean empty = adapter == null || adapter.getCount() == 0;
        final boolean focusable = !empty || isInFilterMode();
        // The order in which we set focusable in touch mode/focusable may matter
        // for the client, see View.setFocusableInTouchMode() comments for more
        // details
        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
        super.setFocusable(focusable && mDesiredFocusableState);
        if (mEmptyView != null) {
            updateEmptyStatus((adapter == null) || adapter.isEmpty());
        }
    }

在这里设置 FocusFocusableInTouchMode 状态,关于 FocusableInTouchMode 不熟悉的可以 查看这篇文章

最后终于到了 View 的重新绘制 requestLayout, 这里将遍历 View 树重新绘制:

public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

至此,我们了解了 ListView 中的观察者模式的大概流程,看得人快吐血了,一层调一层啊,还是画个 UML 图和流程图来回顾一下:

ListView 中的观察者模式

这里写图片描述

ListView 注册观察者 流程图 :

这里写图片描述

ListView 通知观察者更新 流程图 :

这里写图片描述

备注:

设计模式代码在这里

ListView 另外牛的一点就是可以加载各种各样的 Item View,这得益于当初设计的 Adapter,下篇文章我们来分析下 ListView 中的适配器模式。

作者:u011240877 发表于2016/9/27 18:46:34 原文链接
阅读:547 评论:0 查看评论

Unity UGUI图文混排源码(四) -- 聊天气泡

$
0
0

这里有同学建议在做聊天气泡时,可以更改为一张图集对应多个Text,这样能节省资源,不过我突然想到每个Text一个图集,可以随时更换图集,这样表情图更丰富一些,于是我就先将现有的聊天demo改为了聊天气泡

于是一张图集对应多个Text的功能,只有下次更新,哈哈


1.我更新了原来的表情文件,不过资源也来源网络


2.在图文三的时候,为了做动态表情,将索引改为了ID,这里我有将ID改为了name,代码的检测中只要包含了name的图片都会加在动态数组里

 #region 解析动画标签
            List<string> tempListName = new List<string>();
            for (int i = 0; i < m_spriteAsset.listSpriteInfor.Count; i++)
            {
               // Debug.Log((m_spriteAsset.listSpriteInfor[i].name));
                if (m_spriteAsset.listSpriteInfor[i].name.Contains(match.Groups[1].Value))
                {
                    tempListName.Add(m_spriteAsset.listSpriteInfor[i].name);
                }
            }
            if (tempListName.Count > 0)
            {
                SpriteTagInfor[] tempArrayTag = new SpriteTagInfor[tempListName.Count];
                for (int i = 0; i < tempArrayTag.Length; i++)
                {
                    tempArrayTag[i] = new SpriteTagInfor();
                    tempArrayTag[i].name = tempListName[i];
                    tempArrayTag[i].index = match.Index;
                    tempArrayTag[i].size = new Vector2(float.Parse(match.Groups[2].Value) * float.Parse(match.Groups[3].Value), float.Parse(match.Groups[2].Value));
                    tempArrayTag[i].Length = match.Length;
                }
                listTagInfor.Add(tempArrayTag[0]);
                m_AnimSpiteTag.Add(listTagInfor.Count - 1, tempArrayTag);
                m_AnimIndex.Add(listTagInfor.Count - 1);
            }
#endregion

3.于是我们就可以更新一下表情文件的命令,一个动态表情的名称,尽量规范,大概看一下我的命名


4.然后就可以开始制作聊天气泡了,聊天气泡主要的因素就在于Item的制作,这里顺便看一下文本的表情表用格式,其实跟之前也是一样的


5.检测到输入的时候,再不断的生成预设就ok啦,具体一些参数,看看脚本应该能理解,并不难,于是就没怎么写出注释

float chatHeight = 10.0f;
    float PlayerHight = 64.0f;
    void ClickSendMessageBtn()
    {
        if (inputText.text.Trim() == null || inputText.text.Trim() == "")
            return;

        GameObject tempChatItem = Instantiate(goprefab) as GameObject;
        tempChatItem.transform.parent = goContent.transform;
        tempChatItem.transform.localScale = Vector3.one;
        InlieText tempChatText = tempChatItem.transform.FindChild("Text").GetComponent<InlieText>();
        tempChatText.text = inputText.text.Trim();
        if (tempChatText.preferredWidth + 20.0f < 105.0f)
        {
            tempChatItem.GetComponent<RectTransform>().sizeDelta = new Vector2(105.0f, tempChatText.preferredHeight + 50.0f);
        }
        else if (tempChatText.preferredWidth + 20.0f > tempChatText.rectTransform.sizeDelta.x)
        {
            tempChatItem.GetComponent<RectTransform>().sizeDelta = new Vector2(tempChatText.rectTransform.sizeDelta.x + 20.0f, tempChatText.preferredHeight + 50.0f);
        }
        else
        {
            tempChatItem.GetComponent<RectTransform>().sizeDelta = new Vector2(tempChatText.preferredWidth + 20.0f, tempChatText.preferredHeight + 50.0f);
        }

        tempChatItem.SetActive(true);
        tempChatText.SetVerticesDirty();
        tempChatItem.GetComponent<RectTransform>().anchoredPosition = new Vector3(-10.0f, -chatHeight);
        chatHeight += tempChatText.preferredHeight + 50.0f + PlayerHight + 10.0f;
        if (chatHeight > goContent.GetComponent<RectTransform>().sizeDelta.y)
        {
            goContent.GetComponent<RectTransform>().sizeDelta = new Vector2(goContent.GetComponent<RectTransform>().sizeDelta.x, chatHeight);
        }
        while (scrollbarVertical.value > 0.01f)
        {
            scrollbarVertical.value = 0.0f;
        }
        inputText.text = "";
    }
6.最后看一下效果截图,怕动态加载不出,多放一张静态图片:




作者:qq992817263 发表于2016/9/27 18:53:50 原文链接
阅读:175 评论:0 查看评论

Android日志的使用技巧

$
0
0

Android系统提供了logcat工具来记录打印log,先来聊一下logcat的使用

adb logcat --help
Usage: logcat [options] [filterspecs]
options include:
  -s              Set default filter to silent.
                  Like specifying filterspec '*:s'
  -f <filename>   Log to file. Default to stdout
  -r [<kbytes>]   Rotate log every kbytes. (16 if unspecified). Requires -f
  -n <count>      Sets max number of rotated logs to <count>, default 4
  -v <format>     Sets the log print format, where <format> is one of:

                  brief process tag thread raw time threadtime long

  -c              clear (flush) the entire log and exit
  -d              dump the log and then exit (don't block)
  -t <count>      print only the most recent <count> lines (implies -d)
  -g              get the size of the log's ring buffer and exit
  -b <buffer>     Request alternate ring buffer, 'main', 'system', 'radio'
                  or 'events'. Multiple -b parameters are allowed and the
                  results are interleaved. The default is -b main -b system.
  -B              output the log in binary
  -C              colored output
filterspecs are a series of
  <tag>[:priority]

where <tag> is a log component tag (or * for all) and priority is:
  V    Verbose
  D    Debug
  I    Info
  W    Warn
  E    Error
  F    Fatal
  S    Silent (supress all output)

'*' means '*:d' and <tag> by itself means <tag>:v

If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS.
If no filterspec is found, filter defaults to '*:I'

If not specified with -v, format is set from ANDROID_PRINTF_LOG
or defaults to "brief"

比较常使用的命令有

adb logcat -f /sdcard/log.txt    将log输出到sdcard下的log.txt文件中

adb logcat -v time 输出log详细时间信息

adb logcat -v thread 输出log线程信息

adb logcat -t 50 输出最近50条log信息

adb logcat -c 清除日志缓冲区

adb logcat *:E 打印所有ERROR级别及更高级别的日志信息

adb logcat Test:I 打印所有INFO级别以及更高级别的TAG为Test的日志信息

linux系统和Mac系统下,logcat结合grep命令可以极大的提高效率

比如adb locat | grep Runtime可以过滤所有Runtime信息

windows系统下,建议结合强大的文本编辑器如notepad

在android代码中,我们通常这样调用:

    Log.i(tagname, message);
很多人,为了方便在IDE中清楚的查看log信息,使用error级别的log

    Log.e(tagname, message);
这其实是对log的一种误用,想要清楚的查看log信息,可以通过过滤tag,甚至是log本身的内容。通过提高log的级别来查看临时信息是很不科学的。这种代码放到线上容易被log误导,本来没有错误的,却打印了一堆ERROR级别的log,而且不方便追踪问题。

打印log的时候最好做一个开关,有些log调试的时候需要输出,但是发布的时候不需要输出。

Android系统本身提供了一个很好的开关

Log.isLoggable

 boolean isDebug=Log.isLoggable(TagName, Log.VERBOSE);
        if (isDebug) {
            Log.w(TagName, "log");
        }
isDebug默认是false,当执行

adb shell setprop log.tag.TagName VERBOSE 
之后,isDebug会变为true






欢迎扫描二维码,关注公众号

作者:robertcpp 发表于2016/9/27 20:21:36 原文链接
阅读:629 评论:0 查看评论

Android系统篇之----Hook系统的AMS服务实现应用启动的拦截功能

$
0
0

技术概念来源:[ 360开源插件框架,项目地址:https://github.com/DroidPluginTeam/DroidPlugin ]


一、Hook系统剪切板服务流程回顾

在之前的一篇文章中已经介绍了 Android中的应用启动流程,这个流程一定要理解透彻,这样我们才可以进行后续的Hook操作,在之前还介绍了Android中如何Hook系统的剪切板服务实现方法的拦截效果,实现原理就是:

1、先找到Hook点,这个一般是分析源码来得到,而一般的Hook点都是静态变量或者是单例方法。

2、构造一个需要拦截的代理对象,需要的条件是代理的对象必须实现一个接口,其次就是需要获取到原始对象实例

有了这两步就是用反射机制,把代理对象替换源对象即可,然后在InvocationHandler的invoke回调方法中进行制定方法的拦截。


那么有了之前的Hook知识点基础和启动流程分析,今天我们就要开始说Hook系统AMS服务的知识点的了,因为这个服务和应用启动相关的,所以一定要理解了应用启动流程。然后按照之前Hook系统的剪切板服务之后,在任何Hook操作之前第一步得先找到Hook点,那下面就开始找这个点。

还是按照之前的逻辑,如果我们想拦截方法,肯定是Hook本地化服务对象,比如之前Hook掉系统的剪切板的方法,肯定是Hook掉IClipboard对象,当时还记得找到的Hook点步骤思路是:

1、首先通过反射从ServiceManager中获取到Clipboard的远端Binder对象

2、Hook掉这个Binder对象,拦截他的queryLocalInterface方法,在这个方法中再次Hook掉本地化服务对象

3、然后在拦截本地化服务对象的具体实现方法即可

在这个过程中我们发现需要做两次Hook操作,因为我们拦截的对象是本地化服务对象,我们实际在使用服务的时候也是这个对象,而在第一步得先Hook掉服务对应的远端Binder对象,然后在Hook掉服务对象即可。


二、Hook系统的AMS服务

那么今天我们介绍如何Hook掉系统的AMS服务,来实现拦截Activity的启动流程,那么我们依然可以采用上面的这种方式来进行操作,但是我们还可以采用另外一种更方便的技术来做操作,在上面的三个步骤中,会发现第一次Hook其实是可以省略的,因为我们在Hook掉AMS服务的时候,可以发现在系统中某一个地方,有AMS本地化实际对象IActivityManager,我们只要找到他就可以先获取对象,然后在通过反射进行Hook代理对象的重置即可。

所以在之前剪切板服务没有这么做,是因为我们并没有发现系统中有一个地方可以去获取IClipboardManager本地化实际对象,所以在以后的Hook操作中,一定要记住这点,如果能够通过分析Android源码,获取服务的本地化对象那么就可以直接Hook即可,如果不行,那只能通过两次Hook操作了。


下面我们通过源码来找到AMS本地化实际对象

我们在前面一篇文章中介绍了Android中应用的启动流程,当时有这张图:


当时分析的时候也说了,这个是整个应用启动的第一处远程通信的地方,从图上可以清晰的看到,这里的对象之间的关系,那么我们其实就是需要Hook本地端的中间者,也就是ActivityManagerProxy对象,那么我们如何去查找这个对象呢?因为我们的目的就是要拦截startActivity方法,那么可以通过这个方法跟进源码,这里就不在说明了,在之前的启动流程中已经讲解的非常清楚了,我们最终会跟进到ActivityManagerNative类中的getDefault方法:


而这里的gDefault是Singleton类型实现单例模式功能的:


好了,这里发现其实内部就是先从ServiceManager中获取到远程的AMS的Binder对象,然后在转化成本地操作的实际对象,其实就是ActivityManagerProxy类型的,那么这里我们可以省略Hook远端的Binder对象操作了,同时会发现这里的方法和变量都是static类型的,那么对于反射来说就非常有利了,之前也说过在Hook技术中:对于单例方法和static变量以及static方法最好用反射了!

那么我们就可以这么弄:

1、使用反射机制获取到AMS的本地化对象

2、然后在使用动态代理技术,生成一个代理对象

3、最后把代理对象在重置回去即可

在之前介绍动态代理生成代理对象也说到,只要符合两个规则即可:

1》有原始对象,这里正好是第一步中获取到的本地化对象

2》原始对象必须实现接口,这里也正好符合,因为ActivityManagerProxy是实现了IActivityManager接口类型的

下面就开始操作吧:


这里我们可以拦截startActivity的方法:


我们运行程序,但是需要需要注意哦,一般会先把Hook操作就是上面的反射工作放到最开始的地方,最好是Application的attachBaseContext方法中,因为这个时机是最早的。


哈哈,看到了,这里我们拦截成功了,而且可以获取拦截的参数,从中可以知道启动的Activity信息了。


三、拦截应用启动流程

到这里我们就已经实现可以拦截系统中Activity启动的流程了,那么下面咋们就来实践一下,我们做一个案例,就是启动一个没有在AndroidManifest.xml中声明的Activity:

这个思路可以是这样,上面已经拦截了启动Activity流程,也得到了启动参数intent信息,那么就在这里,我们可以自己构造一个假的Activity信息的intent,然后启动:


然后我们启动activity的代码:


这里启动的是TargetActivity,但是需要在AndroidManifest.xml中声明StubActivity:


然后启动,会发现没有报错,而是启动成功了StubActivity了。

那么这里其实想想应该没问题的,虽然我们代码中启动的是TargetActivity,但是我们进行拦截然后替换成了StubActivity,而StubActivity在AndroidManifest.xml中声明了,也不会报错,所以运行成功了。


从上面的简单例子可以看到,我们没有在AndroidManifest.xml中声明TargetActivity,运行也没有报错的,原因是因为我们替换了StubActivity,而StubActivity声明了,运行也是不会报错的。那么如果我们把StubActivity的声明去了,运行会报错吗?

答案是肯定会报错的,而且报错的信息是:StubActivity没有声明,而不是TargetActivity没有声明,因为我们已经替换了。那么这里就引出第二个问题了,关于Activity启动的远端操作具体如何?

这个就需要去看ActivityManagerService源码了,在这里会做Activity启动前的校验工作,而这个是运行在远端的system_server进程中的。有的同学说那继续在这里Hook呀,要是把这个校验工作也给替换了,就可以实现真正意义上无需声明Activity就可以启动了,做当然可以做,但是不是本文重点,因为这个校验工作实在system_server进程中的,如果要Hook的话,就需要注入到进程中,而注入system_server系统进程是在这里讲到:Android中通过系统进程注入拦截应用行为


上面的一个例子中会发现虽然拦截了,也替换了启动参数信息,但是发现然并卵,因为我们最终想启动的还是TargetActivity,而不是替换的StubActivity,所以这里还需要在后面把StubActivity给换回来。那么这里就要去分析Activity校验之后开始启动的逻辑了,而这里就要涉及到之前文章分析的启动流程中第二个远程通信:


这个通信其实是为了Activity的生命周期工作,而在这个过程中和上面的AMS的通信不一样,这里的远端服务是在应用进程中,而本地服务实在系统进程中,也就是说系统进程会发送一些生命周期命令给应用进程,而具体实际的处理逻辑实在应用中的。

那么我们就又要开始进行Hook了,按照之前的逻辑,这里我们需要Hook掉ApplicationThreadProxy即可,但是这里不需要这么弄,因为还有一种更为方便的路径,就是在最终的处理地方进行拦截,这个就要分析ActivityThread类了,因为ApplicationThread类就是在这里定义,也就说具体处理逻辑就在这里,而在之前分析流程的时候介绍过了,其实最终的处理都是在ActivityThread中的Handler中:


看到了,在这里就是我们最好的还原时机,那么我们就需要Hook掉这个Handler机制了,但是我们在之前介绍了Handler源码内部机制解析 知道,最终处理消息dispatchMessage方法的处理逻辑:


1、如果传递的Message本身就有callback,那么直接使用Message对象的callback方法;
2、如果Handler类的成员变量mCallback存在,那么首先执行这个mCallback回调;
3、如果mCallback的回调返回true,那么表示消息已经成功处理;直接结束。
4、如果mCallback的回调返回false,那么表示消息没有处理完毕,会继续使用Handler类的handleMessage方法处理消息

有了这个逻辑,那么我们如果要拦截Handler的handleMessage方法,只需要构造一个变量mCallback即可:


这里我们就可以自定义一个Callback,然后设置到Handler的变量中,最后在自定义的Callback中处理消息实现拦截:


到这里我们就可以看一下实现的效果了:


看到了,这里就可以完全的实现了没有声明的TargetActivity的启动,但是这里还是需要StubActivity的声明,而对于StubActivity也叫作代理Activity,后续如果在启动Activity都可以把他当做代理Activity,而这个代理最大的作用就是欺骗AMS,让AMS检测通过即可,


四、知识总结与回顾

好了,下面咋们就来总结一下拦截流程:

1、首先通过源码分析得知,在ActivityManagerNative类中有一个静态方法和静态变量维护这一个AMS本地化对象

2、那么就可以使用反射机制获取到这个AMS本地化对象,也就是ActivityManagerProxy。

3、而我们正好想要拦截的方法都是在这个对象中,所以只需要Hook掉这个对象即可,因为使用动态代理生成代理对象必须符合两个规则:

1》有原始对象,这里就是ActivityManagerProxy对象

2》原始对象必须实现一个接口,正好ActivityManagerProxy对象实现了IActivityManager接口

那么这里既可以生成一个ActivityManagerProxy的代理对象,然后在使用反射把这个对象重置回去。

4、然后就可以在InvocationHandler中进行方法拦截了,本文中只对启动方法做了拦截操作

5、我们拦截方法成功之后,就实现了一个简单的功能,对启动的Activity进行调换,也就是通过修改参数实现的。

6、最后我们要完全实现真正意义上的拦截,还得在最后把原始Activity给替换回来。这里还需要拦截系统处理Activity的生命周期逻辑的Handler机制,这里主要借助Handler本身的处理消息机制,构造一个回调,然后重置变量即可。


在这整个过程中,我们其实可以发现,首先咋们得“狸猫换太子”,骗过AMS的检测,然后在最后生不知鬼不觉的在换回来,整个过程AMS完全无感知:



五、技术用途

到这里我们就实现了Android中无需声明Activity就可以启动的效果,那么这个有什么用呢?

1、现在很多应用有时候会集成微信和支付宝支付功能,但是这时候就需要在AndroidManifest.xml中声明一些Activity,而恶心的是,有些市场在审核个人开发者提交的app的时候,如果有支付功能是不能审核通过的,这个应该也是为了防止恶意扣费吧,那么对于个人开发者就是没辙了?想想路子还是有的:

1》可以先上一个没有支付功能的,先到市场再说,然后在自己的app中自升级带有支付功能的即可,完全绕过市场审核了,但是这种方式是需要自升级工作。

2》采用插件化开发,把支付功能SDK做成动态加载,这样市场在扫描包的时候是找不到指定支付api就可以的,同时还得把AndroidManifest.xml中的支付Activity给隐藏起来躲避检测,那么如何隐藏就用到了这里的技术了,咋们可以自定义一个代理假的Activity,然后通过这种方式启动真正的支付Activity即可。

2、上面也提到了,在插件化开发中处理Activity的生命周期问题,也是可以采用这种方式去做处理的。


六、Hook技术精华总结

通过之前的两篇文章了解了如何Hook掉系统的服务,规则和方法都是大同小异,主要有两种情况:

1、如果在系统中找不到指定服务的本地化对象,即没法使用反射机制获取,就需要进行两次Hook工作,先Hook远程服务的Binder对象,在Hook掉本地化服务对象拦截指定方法。像之前的剪切板服务ClipboardManager。

2、如果能够直接使用反射机制获取到本地化服务对象,那么只需要一次Hook操作即可完成拦截,像这里的AMS服务。


关于Android中的Hook技术其实全局可以分为两种:

1、第一种是获取root权限,利用进程注入技术,修改指定函数指针,达到拦截效果,这种方式可以拦截系统所有的服务。对系统所有应用有效果。

2、无需root权限,利用反射机制和动态代理技术,达到拦截效果,这种方式只能对本应用有效果。


项目案例地址http://download.csdn.net/detail/jiangwei0910410003/9641479


七、总结

到这里介绍完了如何Hook系统的AMS服务实现应用启动流程的拦截工作,同样的我们可以使用这种方式去Hook系统的其他服务,比如PMS服务等,当然到这里,我们全面的介绍了Android中的Hook技术实现,后续还有一个文章主要通过编译源码,添加一个系统服务,让每个应用都可以使用该服务,这个了解之后,那么对于Android中的Binder机制以及远端服务通信机制就有了非常深刻的了解了。


更多内容:点击这里

关注微信公众号,最新技术干货实时推送



作者:jiangwei0910410003 发表于2016/9/27 20:51:42 原文链接
阅读:408 评论:0 查看评论

Android自定义View——实现字母导航栏

$
0
0

实现字母导航栏


思路分析:

1、自定义View实现字母导航栏

2、ListView实现联系人列表

3、字母导航栏滑动事件处理

4、字母导航栏与中间字母的联动

5、字母导航栏与ListView的联动


效果图:



首先,我们先甩出主布局文件,方便后面代码的说明

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/search_border"
        android:drawableLeft="@android:drawable/ic_menu_search"
        android:padding="8dp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="@null" />

        <TextView
            android:id="@+id/tv"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerInParent="true"
            android:background="#888888"
            android:gravity="center"
            android:textColor="#000000"
            android:textSize="18dp"
            android:visibility="gone" />

        <com.handsome.tulin.View.NavView
            android:id="@+id/nv"
            android:layout_width="20dp"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:layout_margin="16dp" />
    </RelativeLayout>
</LinearLayout>


步骤一:分析自定义字母导航栏


思路分析:

1、我们在使用的时候把宽设置为20dp,高设置为填充父控件,所以这里获取的宽度为20dp

2、通过循环,画出竖直的字母,每画一次得重新设置一下颜色,因为我们需要一个选中的字母颜色和默认不一样

public class NavView extends View {

    private Paint textPaint = new Paint();
    private String[] s = new String[]{
            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K",
            "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", 
            "W", "X", "Y", "Z", "#"};
    //鼠标点击、滑动时选择的字母
    private int choose = -1;
    //中间的文本
    private TextView tv;

    public NavView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NavView(Context context) {
        super(context);
    }

    public NavView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void initPaint() {
        textPaint.setTextSize(20);
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.BLACK);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画字母
        drawText(canvas);
    }


    /**
     * 画字母
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        //获取View的宽高
        int width = getWidth();
        int height = getHeight();
        //获取每个字母的高度
        int singleHeight = height / s.length;
        //画字母
        for (int i = 0; i < s.length; i++) {
            //画笔默认颜色
            initPaint();
            //高亮字母颜色
            if (choose == i) {
                textPaint.setColor(Color.RED);
            }
            //计算每个字母的坐标
            float x = (width - textPaint.measureText(s[i])) / 2;
            float y = (i + 1) * singleHeight;
            canvas.drawText(s[i], x, y, textPaint);
            //重置颜色
            textPaint.reset();
        }
    }
}

步骤二:ListView实现联系人列表


思路分析:

1、在主Activity中,定义一个数据数组,使用工具类获取数组的第一个字母,使用Collections根据第一个字母进行排序,由于工具类有点长,就不贴出来了。

2、创建一个ListView子布局,创建一个Adapter进行填充。


主布局:

public class MainActivity extends AppCompatActivity {

    private TextView tv;
    private ListView lv;
    private NavView nv;

    private List<User> list;
    private UserAdapter adapter;
    private String[] name = new String[]{
            "潘粤明", "戴军", "薛之谦", "蓝雨", "任泉", "张杰", "秦俊杰",
            "陈坤", "田亮", "夏雨", "保剑锋", "陆毅", "乔振宇", "吉杰", "郭敬明", "巫迪文", "欢子", "井柏然",
            "左小祖咒", "段奕宏", "毛宁", "樊凡", "汤潮", "山野", "陈龙", "侯勇", "俞思远", "冯绍峰", "崔健",
            "杜淳", "张翰", "彭坦", "柏栩栩", "蒲巴甲", "凌潇肃", "毛方圆", "武艺", "耿乐", "钱泳辰"};


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initData();
    }

    private void initView() {
        tv = (TextView) findViewById(R.id.tv);
        lv = (ListView) findViewById(R.id.lv);
        nv = (NavView) findViewById(R.id.nv);
        nv.setTextView(tv);
    }

    private void initData() {
        //初始化数据
        list = new ArrayList<>();
        for (int i = 0; i < name.length; i++) {
            list.add(new User(name[i], CharacterUtils.getFirstSpell(name[i]).toUpperCase()));
        }
        //将拼音排序
        Collections.sort(list, new Comparator<User>() {
            @Override
            public int compare(User lhs, User rhs) {
                return lhs.getFirstCharacter().compareTo(rhs.getFirstCharacter());
            }
        });
        //填充ListView
        adapter = new UserAdapter(this, list);
        lv.setAdapter(adapter);
    }

}


ListView子布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_firstCharacter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#DBDBDA"
        android:padding="8dp"
        android:text="A"
        android:textColor="#000000"
        android:textSize="14dp" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:padding="8dp"
        android:text="张栋梁"
        android:textColor="#2196F3"
        android:textSize="14dp" />

</LinearLayout>

Adapter:

public class UserAdapter extends BaseAdapter {

    private List<User> list;
    private User user;
    private LayoutInflater mInflater;
    private Context context;

    public UserAdapter(Context context, List<User> list) {
        this.list = list;
        mInflater = LayoutInflater.from(context);
        this.context = context;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.adapter_user, null);
        }
        ViewHolder holder = getViewHolder(convertView);
        user = list.get(position);
        if (position == 0) {
            //第一个数据要显示字母和姓名
            holder.tv_firstCharacter.setVisibility(View.VISIBLE);
            holder.tv_firstCharacter.setText(user.getFirstCharacter());
            holder.tv_name.setText(user.getUsername());
        } else {
            //其他数据判断是否为同个字母,这里使用Ascii码比较大小
            if (CharacterUtils.getCnAscii(list.get(position - 1).getFirstCharacter().charAt(0)) <
                    CharacterUtils.getCnAscii(user.getFirstCharacter().charAt(0))) {
                //后面字母的值大于前面字母的值,需要显示字母
                holder.tv_firstCharacter.setVisibility(View.VISIBLE);
                holder.tv_firstCharacter.setText(user.getFirstCharacter());
                holder.tv_name.setText(user.getUsername());
            } else {
                //后面字母的值等于前面字母的值,不显示字母
                holder.tv_firstCharacter.setVisibility(View.GONE);
                holder.tv_name.setText(user.getUsername());
            }
        }
        return convertView;
    }

    /**
     * 获得控件管理对象
     *
     * @param view
     * @return
     */
    private ViewHolder getViewHolder(View view) {
        ViewHolder holder = (ViewHolder) view.getTag();
        if (holder == null) {
            holder = new ViewHolder(view);
            view.setTag(holder);
        }
        return holder;
    }

    /**
     * 控件管理类
     */
    private class ViewHolder {

        private TextView tv_firstCharacter, tv_name;

        ViewHolder(View view) {
            tv_firstCharacter = (TextView) view.findViewById(R.id.tv_firstCharacter);
            tv_name = (TextView) view.findViewById(R.id.tv_name);
        }
    }

    /**
     * 通过字符查找位置
     *
     * @param s
     * @return
     */
    public int getSelectPosition(String s) {
        for (int i = 0; i < getCount(); i++) {
            String firChar = list.get(i).getFirstCharacter();
            if (firChar.equals(s)) {
                return i;
            }
        }
        return -1;
    }
}


步骤三:字母导航栏滑动事件处理、字母导航栏与中间字母的联动


思路分析:

1、在自定义View中重写dispatchTouchEvent处理滑动事件,最后返回true。

2、在主Activity传进来一个TextView,在我们滑动的时候设置Text,松开的时候消失Text。设置Text的时候需要计算Text的位置,并且滑过多的话会出现数组越界的问题,所以我们在里面处理数组越界问题。

3最后,提供一个接口,记录我们滑到的字母,为了后面可以和ListView联动。


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //计算选中字母
        int index = (int) (event.getY() / getHeight() * s.length);
        //防止脚标越界
        if (index >= s.length) {
            index = s.length - 1;
        } else if (index < 0) {
            index = 0;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                setBackgroundColor(Color.GRAY);
                //选中字母高亮
                choose = index;
                //出现中间文字
                tv.setVisibility(VISIBLE);
                tv.setText(s[choose]);
                //调用ListView连动接口
                if (listener != null) {
                    listener.touchCharacterListener(s[choose]);
                }
                //重绘
                invalidate();
                break;
            default:
                setBackgroundColor(Color.TRANSPARENT);
                //取消选中字母高亮
                choose = -1;
                //隐藏中间文字
                tv.setVisibility(GONE);
                //重绘
                invalidate();
                break;
        }
        return true;
    }

    public onTouchCharacterListener listener;

    public interface onTouchCharacterListener {
        void touchCharacterListener(String s);
    }

    public void setListener(onTouchCharacterListener listener) {
        this.listener = listener;
    }

    /**
     * 传进来一个TextView
     *
     * @param tv
     */
    public void setTextView(TextView tv) {
        this.tv = tv;
    }

步骤四:字母导航栏和ListView的联动


思路分析:

1、我们已经通过接口传递过去了一个选择的字母,和在adapter写好了根据字母查询position的方法,这个时候只要主Activity对自定义View设置监听,判断即可。


        //ListView连动接口
        nv.setListener(new NavView.onTouchCharacterListener() {
            @Override
            public void touchCharacterListener(String s) {
                int position = adapter.getSelectPosition(s);
                if (position != -1) {
                    lv.setSelection(position);
                }
            }
        });

源码下载:使用工具Android Studio2.1,建议使用Import Module

作者:qq_30379689 发表于2016/9/27 21:29:47 原文链接
阅读:178 评论:0 查看评论

Android之RecycleView使用(瀑布流管理器及线性流管理器)

$
0
0

  首先我们先来了解一下RecycleView:

     RecycleView相对于原来的ListView和GridView要灵活很多,可以很快的在listView和gridView以及瀑布流之间进行切换,它主要提供了3中布局方式,LinearLayoutManager(ListView)、GridLayoutManager(GridView)、StaggeredGridLayoutManager(瀑布流)。

    RecyclerView比ListView,GridView之类控件都有哪些优点:

  • 1、数据绑定
  • 2、Item View创建
  • 3、View的回收以及重用等机制。
       下面我们来介绍一下RecycleView瀑布流的用法
 一、我们需要一个瀑布流的工具类 DividerItemDecoration.java
   
/*瀑布流
* */

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin +
                    Math.round(ViewCompat.getTranslationX(child));
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}
二、我们来建一个Activity   RecyclerActivity.java
       先来看一下布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.edu.jereh.android14.recyclerlistview.RecyclerActivity">
<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/rv"
    ></android.support.v7.widget.RecyclerView>
</RelativeLayout>
三、我们来看一下 具体代码的写法:
public class RecyclerActivity extends Activity {

    @Bind(R.id.rv)
    RecyclerView rv;
    private List<Map> list;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler);
        ButterKnife.bind(this);
        list =new ArrayList<>();
        initDate();
        MyAdapter myAdapter =new MyAdapter(list);

        //瀑布流管理器
        StaggeredGridLayoutManager sgm = new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL);
        rv.setLayoutManager(sgm);
        rv.setAdapter(myAdapter);
        //设置分割线
        DividerItemDecoration itemDecoration =
                new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST);
        rv.addItemDecoration(itemDecoration);
    }
    public void initDate(){
        HashMap map =new HashMap();
        map.put("img",R.mipmap.a);
        map.put("text","x1");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.b);
        map.put("text","x2");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.as);
        map.put("text","x3");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.e);
        map.put("text","x2");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.e);
        map.put("text","x3");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.as);
        map.put("text","x2");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.lh3);
        map.put("text","x3");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.b);
        map.put("text","x2");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.as);
        map.put("text","x3");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.lh1);
        map.put("text","x2");
        list.add(map);
        map=new HashMap();
        map.put("img",R.mipmap.lh2);
        map.put("text","x3");
        list.add(map);
    }
    public class MyAdapter extends RecyclerView.Adapter<MyViewHolder>{
        private List<Map> mapList;

        public MyAdapter(List<Map> mapList) {
            this.mapList = mapList;
        }

        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view=LayoutInflater.from(getBaseContext()).inflate(R.layout.recycle_stag,parent,false);
            MyViewHolder viewHolder = new MyViewHolder(view);
            return viewHolder;
        }
//绑定
        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
           Map map = mapList.get(position);
            holder.imageView.setImageResource((Integer) map.get("img"));
            holder.textView.setText(map.get("text").toString());

        }

        @Override
        public int getItemCount() {
            return mapList.size();
        }
    }
    public class MyViewHolder extends RecyclerView.ViewHolder{
       ImageView imageView;
        TextView textView;
        public MyViewHolder(View itemView) {
            super(itemView);
            imageView=(ImageView)itemView.findViewById(R.id.iv);
            textView =(TextView)itemView.findViewById(R.id.tvi1);


        }
    }
}
下面是效果图






    




作者:woainijinying 发表于2016/9/27 22:23:16 原文链接
阅读:59 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>