Android重点面试题

视频播放

  • 视频播放的实现方式?
1、使用其自带的播放器。指定Action为ACTION_VIEW,Data为Uri,Type为其MIME类型。

2、使用VideoView来播放。在布局文件中使用VideoView结合MediaController来实现对其控制。

3、使用MediaPlayer类和SurfaceView来实现,这种方式很灵活。

4、还有一些三方的(节操视频,饺子视频)的使用不在这详细说了(基本的会的话,三方的简单的没啥问题)
  • 视频播放怎么用?
  1.      Viderview的使用方法 
    videoView.start();  //开始播放
    videoView.pause();  //暂停
    videoView.stopPlayback();   //停止播放
    videoView.isPlaying(); //获取是否在播放状态
    videoView.setVideoURI(Uri uri); //设置视频播放uri
    videoView.setVideoPath(String path); //设置视频播放路径
    videoView.seekTo(int msec);  //跳转到设置时间
    videoView.getCurrentPosition();  //获取当前播放时间
    videoView.getDuration();  //获取该视频播放时长
    videoView.setMediaController(MediaController controller); //设置播放控制器
    videoView.setOnPreparedListener(MediaPlayer.OnPreparedListener listener); //加载完成监听
    videoView.setOnCompletionListener(MediaPlayer.OnCompletionListener listener); //设置播放完成监听
    videoView.setOnErrorListener(MediaPlayer.OnErrorListener listener);  //播放失败监听
    注:VideoView其实就是继承的SurfaceView、实现了MediaController.MediaPlayerController接口的组建
    
  2.       MediaPlayer类和SurfaceView的使用
    1,给SurfaceView添加CallBack监听(为了可以播放视频或者使用Camera预览,我们需要指定其Buffer类型)
    2,开始实例化MediaPlayer对象
    3,然后指定需要播放文件的路径,初始化MediaPlayer
    4,取得当前Display对象(getWindowManager().getDefaultDisplay())
    5,重写(SurfaceView几个方法) eg:
     @Override  
     publicvoidsurfaceChanged(SurfaceHolderarg0,intarg1,intarg2,intarg3){    
      //当Surface尺寸等参数改变时触发    
     } 
      @Override    
        publicvoidsurfaceCreated(SurfaceHolderholder){    
            //当SurfaceView中的Surface被创建的时候被调用    
            //在这里我们指定MediaPlayer在当前的Surface中进行播放    
            //在指定了MediaPlayer播放的容器后,我们就可以使用prepare或者prepareAsync来准备播放了    
        }  
        
        @Override    
        publicvoidonVideoSizeChanged(MediaPlayerarg0,intarg1,intarg2){    
            //当video大小改变时触发    
            //这个方法在设置player的source后至少触发一次    
        }    
    
        @Override    
        publicvoidonSeekComplete(MediaPlayerarg0){    
           //seek操作完成时触发    
        }    
    
        @Override    
        publicvoidonPrepared(MediaPlayerplayer){    
    		//当prepare完成后,该方法触发,在这里我们播放视频    
    	}
    6,然后取得video的宽和高
      注意:如果video的宽或者高超出了当前屏幕的大小,则要进行缩放(选择大的一个进行缩放)
    7,设置surfaceView的布局参数()
     	方法.setLayoutParams()
    8,然后开始播放视频 
    	方法.start()
    9,当MediaPlayer播放完成后触发 
     publicvoidonCompletion(MediaPlayerplayer){ 
     	this.finish();
     }
    
    
  • 视频播放使用的好处?

    对于VideoView
       VideoView其实就是继承的SurfaceView、实现了MediaController.MediaPlayerController接口的组建。其中,SurfaceView用来显示视频内容,MediaPlayerController用来控制媒体播放。它能够实现包括快进、快退、播放、暂停按钮以及一个进度条的功能,它的优点就是简单易用,基本功能已经都帮我们封装好了,我们只要调用即可;缺点就是可扩展性差,无法实现一些自定义效果。
    
    对于MediaPlayer+SurfaceView
    	使用MediaPlayer+SurfaceView实现播放器可以实现自定义效果,缺点就是实现比较麻烦;使用MediaPlayer来控制媒体的播放,暂停,进度等;SufaceView则用来显示视频内容。
    

    视频录制

  • 视频录制的实现方式?

    1,调用自带的视频功能
    
    2,使用MediaRecorder
    
    其他使用看 http://www.cnblogs.com/Free-Thinker/p/4486588.html
    
  • 视频录制的使用方法?

    自带的视频功能(主要是在监听中写)
    	 @Override  
     public void onClick(View v) {  
                Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);  
    //在这里的QUALITY参数,值为两个,一个是0,一个是1,代表录制视频的清晰程度,0最不清楚,1最清楚  
    //没有0-1的中间值,另外,使用1也是比较占内存的,测试了一下,录制1分钟,大概内存是43M多  
    //使用0,录制1分钟大概内存是几兆  
                intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);  
                // 限制时长 ,参数61代表61秒,可以根据需求自己调,最高应该是2个小时。  
    //当在这里设置时长之后,录制到达时间,系统会自动保存视频,停止录制  
                intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 61);  
                // 限制大小 限制视频的大小,这里是100兆。当大小到达的时候,系统会自动停止录制  
                intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 1024 * 1024 * 100);  
    //在这里有录制完成之后的操作,系统会默认把视频放到照片的文件夹中  
                startActivityForResult(intent, 11);  
     }
    
    MediaRecorder使用(显示的时候需要用SurfaceView来预览视频)
    1,创建保存录制视频的视频文件,视频结果会保存在sd卡的根目录
    	new Feil(Environment.getExternalStorageDirectory()  
                            .getCanonicalFile() + "名字");
    2,创建MediaPlayer对象(并设置一系列参数)
     	mRecorder = new MediaRecorder();  
     	mRecorder.reset();
    设置从麦克风采集声音(或来自录像机的声音AudioSource.CAMCORDER)
    	mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    设置从摄像头采集图像
    	mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    设置视频文件的输出格式  
    必须在设置声音编码格式、图像编码格式之前设置  
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
    设置声音编码的格式  
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
    设置图像编码的格式  
        mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    我一开始将这里的分辨率调成了640,480调完之后,发现录出的视频会花屏,改成1280,720则没事  
        mRecorder.setVideoSize(1280, 720);  
           // 每秒 4帧  
        mRecorder.setVideoFrameRate(20);  
    	//数值越大,越清晰,占用内存越大,一开始我使用的是5*1024*1024,画面的质量和第一种方法质量为0的时候差不多,  
    //后来改成7*1024*1024,清晰度立马提升,后来写成了8*1024*1024  
         mRecorder.setVideoEncodingBitRate(8*1024*1024);  
         mRecorder.setOutputFile(videoFile.getAbsolutePath());
    // 指定使用SurfaceView来预览视频  
         mRecorder.setPreviewDisplay(sView.getHolder().getSurface()); // ①  
         mRecorder.prepare();
    3,在相应的按钮中设置 开始录制 结束录制  释放资源
    	mRecorder.start();开始录制
    	mRecorder.stop();结束录制
    	mRecorder.release();结束时并释放资源
    	
    
  • 两种视频录制的区别?

    一:使用第一种方法,方便快捷,不需要太多的代码量;使用第二种方法,代码量稍大;
    二:第一种方法视频清晰度只有两种,一个是最不清楚,一个是最清楚;第二种方法视频清晰度可根据数值自动往上调 ;
    三:第一种方法,清晰度为1的时候,会占用大内存;第二种方法 :清晰度越高的时候,内存也会增大;
    经测试:第一种方法清晰度为1,录制一分钟,大小在43M左右;第二种方法当为8*1024*1024时,录制一分钟,大小在57M;
    四:第一种方法在录制过程中,操作方便,有自己的暂停、录制、播放按钮 ;第二种只能自己去写暂停、录制或播放按钮;
    综上,如果内存足够大的时候,推荐使用第一种方法,方便快捷。
    
    

    Handler机制

  • Handler机制是什么?

    Handler 是 Android 给我们提供来更新 UI 的一套机制,也是一套消息处理的机制,我们可以发送消息,也可以通过它来处理消息,Handler 在我们的 framework 中是非常常见的。
    Android 在设计的时候,就封装了一套消息创建、传递、处理机制,如果不遵循这样的机制就没有办法更新 UI 信息,就会抛出异常信息。
    	来源:https://blog.csdn.net/wangning13ji/article/details/52289452
    
  • Handler机制中常用的类的概念?

    * Message 
    	消息,理解为线程间通讯的数据单元。例如后台线程在处理数据完毕后需要更新UI,则可发送一条包含更新信息的Message给UI线程。
    * Message Queue 
    	消息队列,用来存放通过Handler发布的消息,按照先进先出执行。
    * Handler 
    	Handler是Message的主要处理者,负责将Message添加到消息队列以及对消息队列中的Message进行处理。
    * Looper 
    	循环器,扮演Message Queue和Handler之间桥梁的角色,循环取出Message Queue里面的Message,并交付给相应的Handler进行处理。
    * 线程 
    	UI thread 通常就是main thread,而Android启动程序时会替它建立一个Message Queue。每一个线程里可含有一个Looper对象以及一个MessageQueue数据结构。在你的应用程序里,可以定义Handler的子类别来接收Looper所送出的消息。
    
  • Handler机制怎么用?

    1* 传递message(用于接受子线程发送的数据,并用此数据配合主线程更新 UI)
    	post(Ruannable);
    	postAtTime(Runnable, long);
    	postDelayed(Runnable long);
    注意:post类方法允许你排列一个 Runnable 对象到主线程队列中。
    
    2* 传递Runnable对象(用于通过 Handler 绑定的消息队列,安排不同操作的执行顺序)
    	sendEmptyMessage(int);
    	sendMessage(Message);
    	sendMessageAtTime(Message, long);
    	sendMessageDelayed(Message, long);
    注意:sendMessage 类方法,允许你安排一个带数据的 Message 对象到队列中,等待更新。
        1,使用 Handler 在子线程中向 UI 线程发送一个消息进行 UI 的更新
        2,创建一个 Message, Message msg = new Message(); msg.arg1 = 88;
        3,handler.sendMessage(msg); msg.obj = xxx; 可以传递一个对象
        4,当然不一定要用 new 一个 Message,也可以复用系统的 message 对象 Message msg = 			 handler.obtainMessage();
    
     3* 传递Callback对象(Callback 用于截获 handler 发送的消息,如果返回 true 就截获成功不会向下传递了)
     
    public Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            // TODO Auto-generated method stub
            Toast.makeText(getApplicationContext(), "HandleMessage 1", 	Toast.LENGTH_SHORT).show();
            return true;
        }
    }) {
        public void handleMessage(Message msg) {
            // TODO Auto-generated method stub
            Toast.makeText(getApplicationContext(), "handleMessage 1", Toast.LENGTH_SHORT).show();
        };
        注意:上面的示例中,第一个有返回值的 handlerMessage 方法是 Callback 的回调,如果返回true,则不执行下面的 handlerMessage 方法,从而达到拦截 handler 发送的消息的目的,如果返回 false,则会继续执行 handlerMessage 方法。
    
  • Handler原理

    1* Android为什么设计成只能通过Handler机制更新UI呢?
    	最根本的目的就是解决多线程并发的问题,假设在一个 Activity 当中,有多个线程去更新 UI,并且对更新的 UI 的操作进行枷锁处理的话又会产生什么样的问题呢? 那就是性能下降,Handler 通过消息队列,保证了消息处理的先后有序。
    
    鉴于以上问题的考虑,Android 给我们提供了一套更新 UI 的机制,我们只要使用一套机制就好,所有的更新 UI 的操作都是在主线程中轮询处理。
    
    2*Handler的原理是什么?
    	①、Handler 封装了消息的发送(主要包括消息发送给谁) Looper:
    		a,内部包含一个消息队列也就是 MessageQueue,所有 Handler 发送的消息都走向这个队列。
    		b,Looper.loop()方法,就是一个 for 死循环,不断的从 MessageQueue 取消息,如果有消息就处理消息,没有消息就阻塞。
    	②、MessageQueue,就是一个消息队列,可以添加消息,处理消息。
    	③、Handler 也不难,比较简单,在构造 Handler 时候内部会跟 Looper 进行关联,通过 	   	Looper.myLooper() 获取到 Looper,找到 Looper 也就找到了 MessageQueue。在 Handler 中发送消息,其实是向 MessageQueue 队列中发送消息。
    
    3* handler与Looper、MessageQueue的关系?
    	handler 负责发送消息,Looper 负责接收 Handler 发送消息,并直接把消息回传给 handler 自己,MessageQueue 就是一个存储消息的容器。
    	如下图:
    		图片来源:https://my.oschina.net/Draymond/blog/1015181
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJdgqUH2-1599705435159)(D:\学习成功之路\Android中级文件夹\图库\24163244_tSjI.png)]

4* HandlerThread是什么?
	HandlerThread 继承于 Thread,所以它本质就是个 Thread。与普通的 Thread 的差别就在于,它有个 Looper 成员变量。这个 Looper 其实就是对消息队列以及队列处理逻辑的封装,简单来说就是消息队列+消息循环。
	
当我们需要一个工作线程,而不是把它当作一次性消耗品,用过即废的话,就可以使用它。
eg:
private Handler mHandler = null;
private HandlerThread mHandlerThread = null;
 
private void sendRunnableToWorker(Ruannable run) {
    if (null == mHandlerThread) {
        mHandlerThread = new HandlerThread("WorkerThread");
        // 给工作者线程低优先级
        mHandlerThread.setPriority(Thread.MIN_PRIORITY);
        mHandlerThread.start();
    }
    if (null == mHandler) {
        mHandler = new Handler(mHandlerThread.getLooper());
    }
    mHandler.post(run);
}
5* 主线程与子线程之间的信息交互
	//创建主线程的Handler
private Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {
        Message mssage = new Message();
        System.out.println("main Handler");
        //向子线程发送消息
        threadHandler.sendMessageDelayed(message, 1000);
    };
};
//创建子线程的 handler
private Handler threadHandler;
 
@Override
protected void onCreate(Bundle saveInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    HandlerThread thread = new HandlerThread("handlerThread");
    //创建子线程的 handler
    threadHandler = new Handler(thread.getLooper()) {
        public void handlerMessage(Message msg) {
            Message message = new Message();
            //向主线程发送消息
            mHandler.sendMessageDelayed(message, 1000);
        };
    };
}
6* Android中更新UI的几种方式
	Android 中更新 UI 的 4 种方式:
        1,runOnUiThread
        2,handler 的 post
        3,handler 的 sendMessage
        4,View 自身的 post

性能优化

  • 概述
性能调优是开发中少不了的一个过程,同时也是一名优秀的程序员需要掌握的基本技能
	当应用内存不足时,将发生内存溢出。内存溢出会直接导致应用 crash。尽量减少应用的内存使用将很好的避免发生内存溢出
	并从一下三个方面进行优化
		内存泄露,布局优化和UI卡顿优化,还有其他优化
  • 内存优化
1* 内存泄漏
	首先我们应该确保我们的应用不存在内存泄漏的情况。内存泄漏是指长生命周期对象持有短生命周期对象的引用,而导致短生命周期对象无法及时释放的问题,例如一个静态变量持有某个 Activity 的引用而导致该 Activity 退出时无法被回收。

Android 中常见的内存泄漏场景有:
1,Handler
    我们可以使用静态内部类的 Handler,并让 Handler 只持有外层 Activity 的弱引用;在 Activity 不再需要时,可以手动清空对应 Handler 中的所有消息。
2,打开的资源未关闭
    例如文件、数据库等,打开后都是需要关闭的。这类问题通常比较好排查,使用一些代码检查工具(如 lint 等)都可以帮助你找到未关闭的资源。
3,注册的回调没有注销
    一些回调方法,通常命名如 registerCallback 对应有 unregisterCallback,addCallback 对应有 removeCallback。这些回调都是需要我们成对使用的,否则很可能产生内存泄漏。
4,单例模式持有的引用
	通常单例对象都是 static 的,其生命周期都很长。当单例对象持有某个短生命周期对象的引用时,如某个 Activity,将导致该 Activity 无法被系统回收。
我们应该手动清除 Activity 的引用,或者是当单例中需要上下文时,直接使用 Application 作为其上下文。
5,静态成员持有的引用
	该引用对象如果不再需要使用,应该手动将引用置为 null。
6,非静态内部类的静态实例
	非静态内部类对象会持有外部类的引用,如果该非静态内部类对象是静态的,也将导致外部类对象无法被回收。
2* 图片
	- 图片压缩:对于大图片,可以压缩后加载(图片压缩方法可自行百度)。
    - 图片使用完后,及时释放。
    - 图片素材放在合适的目录下,如 xxhdpi、xxxhdpi 等,系统加载不同目录下的图片资源时,会根据手机 dpi 对其进行一定的缩放。一张较大的图片放在较低的 dpi 目录下容易导致系统加载时又进行放大而带来很大的内存消耗。
    - 图片缓存:当图片众多时,可以使用 池 来管理,并设置恰当的池大小(通常图片框架中都有相似实现了)。
    - 善用开源的图片框架吧。
3* 使用合适和数据结构
	- 选用合适的数据结构,往往不仅可以减少内存的使用,也可以加快运算速度。你需要了解 栈、队列、数组、链表、树、哈希表 等常见数据结构,并根据实际场景选用合适的数据结构。

	- 这里介绍两个 Android 推荐使用的,类似 键值对 功能的类:SparseArray 和 ArrayMap。相比 HashMap,它们占用更少的内存,且功能相似,虽然性能上略逊一筹,但在 1000 以下的数量级上,性能上的差异基本可以忽略,但内存占用将少很多。
4* 其它优化
  - 使用 int 代替枚举
	枚举本质还是对象,比 int 多使用两倍左右的内存。不过枚举也有其优势,我们可以考虑使用 int 来代替枚举而减少内存。
  - 使用对象池
	对于需要频繁创建和释放的对象,我们可以考虑使用对象池来管理,通过重用对象来避免反复的创建释放。
  - ListView 的复用
	这个老生常谈了,通过 ViewHolder 重用布局。或者使用 RecyclerView。
  • 布局优化
1* 尽量选择使用简单的布局
	简单的控件加载起来更快,当简单的控件可以满足需求时,应尽量考虑使用简单的控件。
常用布局控件复杂度:FrameLayout < LinearLayout < RelativeLayout。
2* 减少布局的嵌套层级
	当布局的嵌套层级增大时,将大大减慢 xml 的解析速度,而影响到页面的显示。所以我们应当保证布局的嵌套层级尽可能的低。
3* 其它优化
	- 使用 include 标签重用布局。
	- 合理的使用 merge 标签,可优化布局的嵌套层级。
	- 使用 style 标签,抽离公共的风格,可减少代码量,也更易维护。
	- 使用 ViewStub,对于某些不常用的控件,可在需要的时候再进行加载。
  • UI卡顿优化
1* ANR
	ANR 即页面无响应,产生原因有:Activity 中超过 5s 无响应;BroadcastReceiver 中,前台广播超过 10s,后台广播超过 60s;Service 20s 未完成启动;ContentProvider 的 publish 在 10s 没进行完等。

解决方法:避免在 UI 线程中进行耗时的操作,注意四大组件的大多数回调均在主线程中。
2* View 绘制慢
	Android 的渲染需要在 16ms 内完成,否则会产生卡顿的现象。
避免在 onDraw() 方法中进行任何耗时操作,包括频繁创建局部对象(最好不要在该方法中创建局部对象)等。
避免频繁触发 view 的 layout 方法,因为会重新测量和绘制。
3* 动画
	避免在同一时间执行过多的动画,导致 CPU/GPU 负载过大。
实现动画时,可以考虑一些开源的优秀的动画框架。
尽量使用硬件加速来完成动画。
4* GC(垃圾回收)
	由于 GC 时将挂起其它所有线程,所以频繁的 GC 将带来卡顿的现象。
避免频繁创建释放对象,避免内存负荷大将减少 GC 的频率。
  • 其他优化
1* 线程池
	开启一个新的线程花销是很大的,如果应用需要经常创建新的线程,就考虑使用线程池吧。通过重用旧的线程对象减少创建新线程的开销。
2*  网络
	一个页面的数据尽可能的放在同一个接口里,从而减少网络访问的次数。
大量的数据可以使用分页加载、缓存等。
3* IO流
	尽量减少 IO 的访问次数,例如读取一个文件时,一个字节一个字节的读取的话将频繁的访问 IO,我们可以一次性读取更多的字节而减少 IO 访问次数。
注意:同理,数据库也是这样。
4*  代码量
	尽量精简我们的代码,移除无用的资源和代码,选择使用更轻量级的依赖库。这也将大大减小我们 apk 的体积。
5* 运算
	例如进行乘 2、除 2 等操作时,使用位运算(左移、右移)比乘除运算效率将好很多。尤其是这个运算发生在某个较大的循环体内时。
对于一些操作,我们可以选择使用 预处理 和 延时计算 的策略。
6* 异常、锁
	try - catch 和加锁的操作都是较重量级的,我们可以尽量不使用它们。例如一些线程同步的场景中选择使用 原子类 或 volatile 关键字代替锁。
如果需要使用的场景下,我们也应该尽量保证其粒度足够小,即其包含的语句尽量少。

MVC框架

  • MVC的概述
M是指逻辑模型,
V是指视图模型,
C则是控制器。
	一个逻辑模型可以对于多种视图模型,比如一批统计数据你可以分别用柱状图、饼图来表示。一种视图模型也可以对于多种逻辑模型。使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式,而C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新,这与《设计模式》中的观察者模式是完全一样。
	1) 视图层(View):一般采用XML文件进行界面的描述,使用的时候可以非常方便的引入。当然,如何你对Android了解的比较的多了话,就一定可以想到在Android中也可以使用JavaScript+HTML等的方式作为View层,当然这里需要进行Java和JavaScript之间的通信,幸运的是,Android提供了它们之间非常方便的通信实现。    
    
  2) 控制层(Controller):Android的控制层的重任通常落在了众多的Acitvity的肩上,这句话也就暗含了不要在Acitivity中写代码,要通过Activity交割Model业务逻辑层处理,这样做的另外一个原因是Android中的Acitivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。

  3) 模型层(Model):对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算等操作也是必须放在的该层的。就是应用程序中二进制的数据。
  • MVC的好处
	从用户的角度出发,用户可以根据自己的需求,选择自己合适的浏览数据的方式。比如说,对于一篇在线文档,用户可以选择以HTML网页的方式阅读,也可以选择以pdf的方式阅读。从开发者的角度,MVC把应用程序的逻辑层与界面是完全分开的,最大的好处是:界面设计人员可以直接参与到界面开发,程序员就可以把精力放在逻辑层上。而不是像以前那样,设计人员把所有的材料交给开发人员,由开发人员来实现界面。在Eclipes工具中开发Android采用了更加简单的方法,设计人员在DroidDraw中设计界面,以XML方式保存,在Eclipes中直接打开就可以看到设计人员设计的界面。
  • MVC对应每层明确的分工
Model 模型层
包括实体模型层,存放程序中调用的实体。
业务模型层,存放程序中调用的业务逻辑。
 
View 显示层
 Android很好的将显示层抽离,并放入res/目录的中以XML形式体现。 包括
main.xml 布局文件。 
string.xml 存放常量。
drawable 存放使用的图片文件。
 
Control 控制层
Control层是Activity的职责。你只有告诉Activity做什么。怎么做那是模型层的事。

MVP框架

  • MVP是什么?
所谓的mvp,即是(model-处理业务逻辑(主要是数据读写,或者与后台通信(其实也是读写数据)),view-处理ui控件,presenter-主导器,操作model和view)

View 层主要是用于展示数据并对用户行为做出反馈。在Android平台上,他可以对应为Activity, Fragment,View或者对话框。
Model 是数据访问层,往往是数据库接口或者服务器的API。
Presenter 层可以想View层提供来自数据访问层的数据,除此以外,他也会处理一些后台事务。
  • 为什么要使用MVP?
- 将数据处理逻辑和UI显示逻辑隔离,解耦合,职责分明,符合SRP原则。
- 数据逻辑和UI显示逻辑的修改互不影响,减少维护成本,可以很好的防止修改UI显示逻辑时不小心影响到数据逻辑导致bug的出现,多人配合时更好的分清楚各自的职责。
- 降低耦合度、
- 模块职责划分明显
- 利于测试驱动开发
- 代码复用
- 隐藏数据
- 代码灵活性
  • 使用MVP有什么缺点呢?
	由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了
  • MVC和MVP的使用有什么区别?
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。

在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

Okhttp网络框架

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tAUnwvfc-1599705435160)(D:\学习成功之路\Android中级文件夹\图库\16598307-c1a2aa7fa4e8242f.png)]

  • 什么是okhttp?
一般在Java平台上,我们会使用Apache HttpClient作为Http客户端,用于发送 HTTP 请求,并对响应进行处理。比如可以使用http客户端与第三方服务(如SSO服务)进行集成,当然还可以爬取网上的数据等。OKHttp与HttpClient类似,也是一个Http客户端,提供了对 HTTP/2 和 SPDY 的支持,并提供了连接池,GZIP 压缩和 HTTP 响应缓存功能。
注意:来源http://www.cnblogs.com/WUXIAOCHANG/p/10646545.html
  • okhttp的优点有哪些?
(1)支持HTTP2/SPDY(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验)
(2)socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟,共享Socket,减少对服务器的请求次数
(3)基于Headers的缓存策略减少重复的网络请求
(4)拥有Interceptors轻松处理请求与响应(自动处理GZip压缩)
  • okhttp的使用场景有哪些?
(1)一般的get请求
(2)一般的post请求
(3)基于Http的文件上传
(4)文件下载
(5)上传下载的进度回调
(6)加载图片
(7)支持请求回调,直接返回对象、对象集合
(8)支持session的保持
(9)支持自签名网站https的访问,提供方法设置下证书就行
(10)支持取消某个请求
  • okhttp怎么用?(使用步骤)
(1)get请求的步骤,首先构造一个Request对象,参数最起码有个url,当然你可以通过Request.Builder设置更多的参数比如:header、method等。
(2)然后通过request的对象去构造得到一个Call对象,类似于将你的请求封装成了任务,既然是任务,就会有execute()和cancel()等方法。
(3)最后,我们希望以异步的方式去执行请求,所以我们调用的是call.enqueue,将call加入调度队列,然后等待任务执行完成,我们在Callback中即可得到结果。
(4)onResponse回调的参数是response,一般情况下,比如我们希望获得返回的字符串,
可以通过response.body().string()获取;如果希望获得返回的二进制字节数组,则调用response.body().bytes();如果你想拿到返回的inputStream,则调用response.body().byteStream()
(5)看到这,你可能会奇怪,竟然还能拿到返回的inputStream,看到这个最起码能意识到一点,这里支持大文件下载,有inputStream我们就可以通过IO的方式写文件。不过也说明一个问题,这个onResponse执行的线程并不是UI线程。的确是的,如果你希望操作控件,还是需要使用handler等
(6)okHttp还支持GJson的处理方式
(7)okhttp支持同步请求和异步请求,Call call = client.newCall(request);为同步请求,发送请求后,就会进入阻塞状态,知道收到响应call.enqueue(new Callback()为异步请求
(8)在okhttp3.Callback的回调方法里面有个参数是Call 这个call可以单独取消相应的请求,随便在onFailure或者onResponse方法内部执行call.cancel()都可以。如果想取消所有的请求,则可以okhttpclient.dispatcher().cancelAll();
  • okhttp拦截器的使用
什么是拦截器
  首先我们需要了解什么事拦截器。打个比方,镖局押着一箱元宝在行走在一个山间小路上,突然从山上下来一群山贼拦住了镖局的去路,将镖局身上值钱的东西搜刮干净后将其放行。其中山贼相当于拦截器,镖局相当于一个正在执行任务的网络请求,请求中的参数就是镖局携带的元宝。拦截器可以将网络请求携带的参数进行修改验证,然后放行。这里面其实设计了AOP编程的思想(面向切面编程)。
  在介绍拦截器的作用和好处之前,我们还是要回到山贼这个角色上,如果让你做一次山贼,你会在什么地方埋伏?肯定是在镖局必经之路上埋伏。也就是说,拦截器就是在所有的网络请求的必经之地上进行拦截。
(1)拦截器可以一次性对所有的请求和返回值进行修改。
(2)拦截器可以一次性对请求的参数和返回的结果进行编码,比如统一设置为UTF-8.
(3)拦截器可以对所有的请求做统一的日志记录,不需要在每个请求开始或者结束的位置都添加一个日志操作。
(4)其他需要对请求和返回进行统一处理的需求….

OkHttp中拦截器分类
OkHttp中的拦截器分2个:APP层面的拦截器(Application Interception)、网络请求层面的拦截器(Network Interception)
(1)Application Interceptor是在请求执行刚开始,还没有执行OkHttp的核心代码前进行拦截,Application拦截器的作用:
1)不需要担心是否影响OKHttp的请求策略和请求速度。
2)即使是从缓存中取数据,也会执行Application拦截器。
3)允许重试,即Chain.proceed()可以执行多次。(当然请不要盲目执行多次,需要加入你的逻辑判断)
(2)Network Interception是在连接网络之前
1)可以修改OkHttp框架自动添加的一些属性(当然最好不要修改)。
2)可以观察最终完整的请求参数(也就是最终服务器接收到的请求数据和熟悉)

使用注意点
如果对拦截器不是很熟的同学,开发过程中,建议使用Application Interception。这样避免对OkHttp请求策略的破坏。

常见实际场景
(1)对请求参数进行统一加密处理。
(2)拦截不符合规则的URL。
(3)对请求或者返回参数设置统一的编码方式
(4)其它…。
  • okhttp的简单封装
public class OkHttp {
    
    
    
    public void get(String url, Callback callback) {
    
    
        
        //1.okhttpClient对象
        OkHttpClient okHttpClient = new OkHttpClient.Builder().
                //在这里,还可以设置数据缓存等
                //设置超时时间
                connectTimeout(15, TimeUnit.SECONDS).
                readTimeout(20, TimeUnit.SECONDS).
                writeTimeout(20,  TimeUnit.SECONDS).
                addInterceptor(appInterceptor).//Application拦截器
                //错误重连  
                retryOnConnectionFailure(true).
                build();
        
        //2构造Request,
        //builder.get()代表的是get请求,url方法里面放的参数是一个网络地址
        Request.Builder builder = new Request.Builder();
        
        Request request = builder.get().url(url).build();
        
        //3将Request封装成call
        Call call = okHttpClient.newCall(request);
        
        //4,执行call,这个方法是异步请求数据
        call.enqueue(callback);
        
    }
    
    public void post(String url, List<String> list, Callback callback, RequestBody requestBody) {
    
    
        
        //1.okhttpClient对象
        OkHttpClient okHttpClient = new OkHttpClient.Builder().
                //在这里,还可以设置数据缓存等
                //设置超时时间
                connectTimeout(15, TimeUnit.SECONDS).
                addInterceptor(appInterceptor).//Application拦截器
                readTimeout(20, TimeUnit.SECONDS).
                writeTimeout(20,  TimeUnit.SECONDS).
                //错误重连  
                retryOnConnectionFailure(true).
                build();
        
         RequestBody requestBodyPost = requestBody;
         
         Request requestPost = new Request.Builder()
         .url(url)
         .post(requestBodyPost)
         .build();
         
         okHttpClient.newCall(requestPost).enqueue(callback);

    }
    
    /**
     * 应用拦截器
     */
    Interceptor appInterceptor = new Interceptor() {
    
    
        @Override
        public Response intercept(Chain chain) throws IOException {
    
    
            
            Request request = chain.request();
            
            //———请求之前要做的事情————

            Response response = chain.proceed(request);
            
            //———请求之后要做事情————
            
            return response;
            
        }

    };
}
  • okhttp框架封装的优势
简介
  OKHttpUtils:一个专注于让网络请求更简单的网络请求框架,对于任何形式的网络请求只需要一行代码。它是OKHttp的一次二次封装,封装的目的是让网络请求更加方便。

OKHttpUtils优势
(1)性能高,使用主流的okhttp的进行封装
  OKHttp我们知道它支持http2和socket的重连。自动选择最好的路线,拥有自己维护socket维护的连接池。可以减少TCP的握手次数,同时它拥有队列线程池可以轻松的并发请求。
(2)特有的网络缓存模式
  OKHttpUtils是大多数网络框架不具备的,比如我们公司的网络老板要求不仅在有网的情况下,进行展示网络数据,在无网的情况下使用缓存数据。这时候我们使用普通网络请求,就需要大量的判断。当前是否有网和无网状态,根据不同的状态保存不同的数据。然后再决定是否使用缓存。但是这是一个通用的写法。于是OKHttpUtils使用自动网络缓存模式。让用户只关注数据处理。
(3)方便易用的扩展接口
  可以添加全局的公共参数、全局的拦截器、全局的超时时间,更可以对单个请求定制拦截器。请求参数修改等等。
(4)强大的Cookie的保存策略
  在客户端对Cookie的获取不是一个特别简单的事情,Cookie全程自动管理,并且提供了额外的Cookie管理方法,引入额外的自动管理中,添加任何你想创建的Cookie。

RxJava使用

  • 什么是RxJava
 Rx(Reactive Extensions)是一个库,用来处理事件和异步任务,在很多语言上都有实现,RxJava是Rx在Java上的实现。简单来说,RxJava就是处理异步的一个库,最基本是基于观察者模式来实现的。通过Obserable和Observer的机制,实现所谓响应式的编程体验。
  • 为什么要用Rxjava
比如说一个庞大的项目,一个事件传递的整个过程可能要经历很多方法,方法套方法,每个方法的位置七零八落,一个个方法跳进去看,跳过去跳过来很容易把脑袋弄晕,不够直观。但是Rxjava可以把所有逻辑用链式加闭包的方式呈现,做了哪些操作,谁在前谁在后非常直观,逻辑清晰,维护就会非常轻松。就算不是你写的你也可以很快的了解,你可以把它看作一条河流,整个过程就是对里面的水流做进行加工。懂了这个特性我们才知道在复杂的逻辑中运用Rxjava是多么的重要。
  假设有这样一个需求:界面上有一个自定义的视图 imageCollectorView ,它的作用是显示多张图片,并能使用 addImage(Bitmap) 方法来任意增加显示的图片。现在需要程序将一个给出的目录数组 File[] folders 中每个目录下的 png 图片都加载出来并显示在 imageCollectorView 中。需要注意的是,由于读取图片的这一过程较为耗时,需要放在后台执行,而图片的显示则必须在 UI 线程执行。常用的实现方式有多种,我这里贴出其中一种:

Retrofit框架

  • Retrofit的使用步骤
步骤1:添加Retrofit库的依赖
步骤2:创建 接收服务器返回数据 的类
步骤3:创建 用于描述网络请求 的接口
步骤4:创建 Retrofit 实例
步骤5:创建 网络请求接口实例 并 配置网络请求参数
步骤6:发送网络请求(异步 / 同步)
  • 网络请求参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4irgp2fj-1599705435162)(D:\学习成功之路\Android中级文件夹\图库\944365-c547f2344eef630b.png)]

a. @Header & @Headers
作用:添加请求头 &添加不固定的请求头

b. @Body
作用:以 Post方式 传递 自定义数据类型 给服务器
特别注意:如果提交的是一个Map,那么作用相当于 @Field

c. @Field & @FieldMap
作用:发送 Post请求 时提交请求的表单字段
具体使用:与 @FormUrlEncoded 注解配合使用

d. @Part & @PartMap
作用:发送 Post请求 时提交请求的表单字段
与@Field的区别:功能相同,但携带的参数类型更加丰富,包括数据流,所以适用于 有文件上传 的场景
具体使用:与 @Multipart 注解配合使用

e. @Query和@QueryMap
作用:用于 @GET 方法的查询参数(Query = Url 中 ‘?’ 后面的 key-value)
如:url = http://www.println.net/?cate=android,其中,Query = cate
具体使用:配置时只需要在接口方法中增加一个参数即可:

f. @Path
作用:URL地址的缺省值

g. @Url
作用:直接传入一个请求的 URL变量 用于URL设置

Glide三方插件

  • Glide是什么?
Android Glide是一个开源的图片加载和缓存处理的第三方框架。和Android的Picasso库类似,个人感觉比Android Picasso好用。Android Glide使自身内部已经实现了缓存策略,使得开发者摆脱Android图片加载的琐碎事务,专注逻辑业务的代码。Glide使用便利,短短几行简单明晰的代码,即可完成大多数图片从网络(或者本地)加载、显示的功能需求。
Glide是一款由Bump Technologies开发的图片加载框架,使得我们可以在Android平台上以极度简单的方式加载和展示图片。
目前,Glide最新的稳定版本是3.7.0,虽然4.0已经推出RC版了,但是暂时问题还比较多。因此,我们这个系列的博客都会使用Glide 3.7.0版本来进行讲解,这个版本的Glide相当成熟和稳定。
  • Glide怎么用?
基本使用
Glide.with(context)  
    .load("xxxx.png")  
    .into(imageView);
    
    Glide的with()可以接受的类型有如下:
    	Context context;
        Activity activity;
        FragmentActivity fragmentActivity;
        Fragment fragment;
    load()是加载目标资源,可以接受的参数类型有如下:
    	Uri uri;
        String uriString;
        File file;
        Integer resourceId;
        byte[] model;
        String model;
   	into()就是加载资源完成后作什么处理,它接受三种参数:
   		// 显示在控件上
into(ImageView imageView);

// 通过回调获得加载结果,可能在项目中,一个图片在多个地方使用,可以在回调中获得该图片的Bitmap操作
into(Target target);
Glide.with(mContext).load(url).asBitmap().into(new SimpleTarget<Bitmap>() {
            @Override
            public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
                image.setImageBitmap(resource);
            }
        });
Glide.with(mContext).load(url).asBitmap().into(new Target<Bitmap>() {
            @Override
            public void onLoadStarted(Drawable placeholder) {
                // 设置加载过程中的Drawable
            }

            @Override
            public void onLoadFailed(Exception e, Drawable errorDrawable) {
                // 设置加载失败的Drawable
            }

            @Override
            public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                // 设置加载成功的Bitmap
            }

            @Override
            public void onLoadCleared(Drawable placeholder) {
                // 设置加载被取消时的Drawable
            }

            @Override
            public void getSize(SizeReadyCallback cb) {}

            @Override
            public void setRequest(Request request) {}

            @Override
            public Request getRequest() {
                return null;
            }

            @Override
            public void onStart() {}

            @Override
            public void onStop() {}

            @Override
            public void onDestroy() {}
        });

// 指定期望的图片大小,返回一个Bitmap对象,要在子线程中获取,我觉得这个可以在应用使用,比如在主页我需要显示这个图片,我可以在欢迎页面加载这个图片,到了主页可以直接使用,至于为何要在线程中使用,有待研究。
into(int w, int h);
new Thread(new Runnable() {
        @Override
        public void run() {
            Bitmap bitmap = Glide.with(context)  
                .load(url)  
                .into(200, 200)
                .get();
        }
    }).start();
其他适用
1、设置bitmap或者gif
	.asBitmap();
	.asGif();
2、设置图片大小
	.override(int w, int h); 
3、加载缩略图
	.thumbnail(0.1f);   注:参数范围为0~1
4、设置占位图或者加载错误图:
	.placeholder(R.drawable.placeholder)  
	.error(R.drawable.imagenotfound) 
5、加载完成动画
	.animate(Animator animator);//或者int animationId 
6、图片适配scaleType
	.centerCrop(); // 长的一边撑满
	.fitCenter(); // 短的一边撑满
7、暂停\回复请求(当列表在滑动的时候,调用pauseRequests()取消请求,滑动停止时,调用resumeRequests()恢复请求。)
	Glide.with(context).resumeRequests(); 
	Glide.with(context).pauseRequests(); 
8、在后台线程当中进行加载和缓存
	downloadOnly(int width, int height)
	downloadOnly(Y target)// Y extends Target<File>
	into(int width, int height)
使用注释:Glide的downloadOnly()是将Image的二进制文件下载到硬盘缓存当中,以便在后续使用,可以在UI线程当中异步下载,也可以在异步线程当中同步调用下载,值得注意的是,如果在同步线程当中, 
downloadOnly使用一个target作为参数,而在异步线程当中则是使用width和height。 
在后台线程当中下载图片,可以通过如下的方式:
new Thread(new Runnable() {
        @Override
        public void run() {
            FutureTarget<File> future = Glide.with(applicationContext)
                                             .load(yourUrl)
                                             .downloadOnly(500, 500);
            File cacheFile = future.get();
        }
    }).start();
上面的调用只能在异步线程当中,如果在main Thread当中调用.get(),会阻塞主线程,会导致Crash。 
这个其实就是可以获取该Image的缓存路径,目前还没想好怎么封装一个获取缓存路径的通用类,有待研究。至于Glide的缓存策略,这里也简要介绍下,也是配置: 
缓存策略
.diskCacheStrategy(DiskCacheStrategy.ALL) //这个是设置缓存策略。
DiskCacheStrategy.NONE:不缓存 
DiskCacheStrategy.SOURCE:缓存原始图片 
DiskCacheStrategy.RESULT:缓存压缩过的结果图片 
DiskCacheStrategy.ALL:两个都缓存
9、转换器(第七点中的centerCrop和fitCenter是Glide默认的两种转换器,现在已经有了Glide集成库,提供各式各样的Api可以将图片转为各种形状,例如圆形,圆角型等等)
	(1)单独转换器效果(毛玻璃为例)
	Glide.with(this).load(R.mipmap.login).bitmapTransform(new BlurTransformation(this, 20)).into(imageView);
	(2)复合转换器效果
	Glide.with(this).load(R.mipmap.login).bitmapTransform(new BlurTransformation(this, 20), new CropCircleTransformation(this)).into(imageView);
  • Glide加载图片生命周期
对于图片请求会在onStop的时候自动暂停,然后在onStart的时候重新启动,gif的动画也会在onStop的时候停止,以免在后台消耗电量, 此外,当设备的网络状态发生改变的时候,所有失败的请求会自动重启,保证数据的正确性,还是比较人性化、自动化的。 
  • Glide使用场景
Glide使用最好是在主线程中使用,因为它需要context,在非主线程中使用Glide可能会导致内存泄露或者更严重的Crash,相信大家多Context的使用应该是非常谨慎的,非要在非主线程使用Glide的话就将context换成getApplicationContext
  • Glide在别的库的ImageView的表现
如果你刚好使用了这个圆形Imageview库或者其他的一些自定义的圆形ImageView,而你又刚好设置了占位的话,那么,你就会遇到一个问题,就是有的图片第一次加载的时候只显示占位图,第二次才显示正常的图片,这个问题有三个方案: 
(1)不设置占位图,当然不推荐这个方法 
(2)如果自定义ImageView的效果在Glide的Transformation API中有,那就使用Glide的Transformation API去实现,毕竟这是专门为Glide设计的库;如果没有,可以自定义Glide的Transformation,这个在上面的文章中已经写过啦。 
(3)只使用Glide加载,拿到Bitmap之后再去设置图片:
Glide.with(mContext)
    .load(url) 
    .placeholder(R.drawable.default)
    .into(new SimpleTarget<Bitmap>(width, height) {
        @Override 
        public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
            // setImageBitmap(bitmap) on imageView 
        } 
    };

屏幕适配

  • 为什么要屏幕适配?
由于Android碎片化严重,导致开发中一套代码在不同手机上运行起来效果不是很好,兼容性不是很好,这就需要对不同分辨率,不同屏幕大小的手机做屏幕适配。
  • 为什么要版本适配?
不同的系统版本api有所变更,既要适配高版本,也要做到兼容低版本。
  • 为什么要ROM适配?
这个是最难的,工作量也是最大的,如果没有不同版本手机适配的积累,遇到问题都不知道怎么解决

由于Android是开源的,不同的手机厂商有自己定制的ROM,对系统的api可能有变更,也有可能新增一些api,所以在开发中,要针对不同厂商的手机做一些特殊适配。
  • 什么是屏幕适配?
程序猿把设计狮制作的效果图应用到不同的手机,对不同的屏幕进行界面调整的过程,确保界面不变形,呈现效果图的位置、尺寸、比例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K07c6gpR-1599705435163)(D:\学习成功之路\Android中级文件夹\图库\6098829-7af2a655d1b52425.png)]

  • 屏幕适配的使用步骤?
1、以设计图最小宽度(单位为 dp)作为基准值,生成所有设备对应的 dimens.xml 文件
	- 在 Android Studio 中安装 ScreenMatch 插件
	- 在项目的默认 values 文件夹中需要一份 dimens.xml 文件
	- 执行生成(插件安装好后,在项目的任意目录或文件上右键,选择 ScreenMatch 选项,然后选择在哪个 module 下执行适配。即基于哪个 module 下的 res/values/dimens.xml 文件作为基准 dimens.xml 文件,生成的其他尺寸 dimens.xml 文件放在哪个 module 下。例如选择 app,然后点击 OK ,出现如下界面表示生成文件成功。
	- 然后再看看 res 目录下会自动生成一堆 dimens.xml 文件
	- 根据设计图填写最小宽度基准值,并填写需要适配的设备最小宽度 dp 值
	(步骤 4 是以插件默认的最小宽度基准值为 360dp,适配的设备最小宽度为
384,392,400,410,411,432,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365(包含了平板和 TV )生成的文件,但实际情况要根据设计图和需求设置。)
以上修改需要在配置文件里修改,即screenMatch.properties 文件,该配置文件是执行完上面第 3 步后自动生成在项目的跟目录下的。如下图:
打开配置文件,修改下图中 1、3、4 的值即可。(图中单位均为 dp)
    1:最小宽度基准值,填写设计图的最小宽度值即可。
    2:插件默认适配的最小宽度值,即默认情况下会生成如下值的 dimens.xml 文件。
    3:需要适配的最小宽度值,即你想生成哪些 dimens.xml 文件。
    4:忽略不需要适配的最小宽度值,即忽略掉插件默认生成的 dimens.xml 文件。
	- 配置文件修改完成后,重新执行第 4 步,生成新的 dimens.xml 文件。
2、根据设计图标注,在布局写上对应的值(设计图标注多少 dp,布局中就写多少 dp ,非常方便!)
	大多数 UI 设计师提供设计图有如下几种方式:
上传到蓝湖:显示多少 dp 就写多少 dp。
psd 源文件:用像素大厨查看,显示多少 dp 就写多少 dp(注意像素大厨需要选择与设计图对应的dpi 进行显示)
dp 单位的设计图:标注多少 dp 就写多少 dp。
px 单位的设计图:叫 UI 设计师标注为 dp 单位或跟她要 psd 源文件,如果都不行,那自己算吧
	代码中动态设置 dp 或 sp:
如果需要在代码中动态设置 dp 或 sp,则需要通过 getDimension()方法获取对应资源文件下的 dp 或 sp 值再设置(具体参考 github 上的 demo)

总结
	说了这么多,其实只需要简单的 2 步:
1:以设计图最小宽度(单位为 dp)作为基准值,利用插件生成所有设备对应的 dimens.xml 文件
2:根据设计图标注,标注多少 dp,布局中就写多少dp,格式为@dimen/dp_XX。

事件分发机制

  • 事件分发的对象是谁?
点击事件(Touch事件)
	定义
	当用户触摸屏幕时(View 或 ViewGroup派生的控件),将产生点击事件(Touch事件)
(Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象)

  • 事件类型(4种)
事件类型 具体
MotionEvent.ACTION_DOWN 按下View(所有事件的开始)
MotionEvent.ACTION_UP 抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE 滑动View
MotionEvent.ACTION_CANCEL 结束事件(非人为原因)
  • 特别说明:事件列
从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件

即当一个点击事件( MotionEvent )产生后,系统需把这个事件传递给一个具体的 View 去处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tvAeacfo-1599705435165)(D:\学习成功之路\Android中级文件夹\图库\944365-79b1e86793514e99.png)]

  • 事件分发的本质是什么?
将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程
	即 事件传递的过程 = 分发过程。
  • 事件在哪些对象之间传递?
Activity、ViewGroup、View
	Android的UI界面由Activity、ViewGroup、View 及其派生类组成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HsDDeaj8-1599705435166)(D:\学习成功之路\Android中级文件夹\图库\944365-ece40d4524784ffa.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5kgpaK0-1599705435166)(D:\学习成功之路\Android中级文件夹\图库\944365-02c588300f6ad741.png)]

  • 事件分发图解理解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1svo46r-1599705435168)(D:\学习成功之路\Android中级文件夹\图库\944365-d0a7e6f3c2bbefcc.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCLkL5ZW-1599705435168)(D:\学习成功之路\Android中级文件夹\图库\944365-ff627fea1a2244ad.png)]

  • 事件分发传递

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjmkvROP-1599705435169)(D:\学习成功之路\Android中级文件夹\图库\1426288-c74df1b2b756fd82.png)]

1.ACTION_DOWN事件传递的回路是这样的:
Activity.dispatchTouchEvent—>ViewGoup.dispatchTouchEvent
—>ViewGroup.onInterceptTouchEvent—>View.dispatchTouchEvent
—>View.onTouchEvent—>ViewGroup.onTouchEvent—>Activity.onTouchEvent
2.在此回路中,任何onTouchEvent返回true,则表示事件消费,之后的路线不再走
3.如果ViewGroup.onInterceptTouchEvent返回true,表示该ViewGroup需要拦截该事件,事件不会往子View传递。回路变成:Activity.dispatchTouchEvent
—>ViewGoup.dispatchTouchEvent—>ViewGroup.onInterceptTouchEvent
—>ViewGroup.onTouchEvent—>Activity.onTouchEvent
同样,若onTouchEvent返回true,结束该流程
4.若ViewGroup.onTouchEvent或View.onTouchEvent的ACTION_DOWN没有返回true,则之后ACTION_UP和ACTION_MOVE将不传入。
5.ViewGroup与View的dispatchTouchEvent方法若返回true,则表示消费,直接终止回路。若返回false,则直接跳到上一级的onTouchEvent,跳过中间回路部分。
即如果View.dispatchTouchEvent返回false,则回路是Activity.dispatchTouchEvent—>ViewGoup.dispatchTouchEvent
—>ViewGroup.onInterceptTouchEvent—>View.dispatchTouchEvent
—>ViewGroup.onTouchEvent—>Activity.onTouchEvent
如果ViewGroup.dispatchTouchEvent返回false,则回路是Activity.dispatchTouchEvent—>ViewGoup.dispatchTouchEvent
—>ViewGroup.onInterceptTouchEvent—>View.dispatchTouchEvent
—>Activity.onTouchEvent

Android版本6.0、7.0、8.0、9.0

  • Android6.0
6.0 权限、低耗电和App待机模式
1.为用户提供了两套相互独立的解决方案,即数据存储方案(1.一套存储工作资料,2.存储个人信息);
2.Android M系统层面加入了指纹识别(厂商自行开发的改为原生的,提升了指纹支付安全性);
3.APP关联(APP links),在微信打开京东链接,会提醒跳转到京东APP;
4.Android pay,Android支付唯一标准,简洁、安全、可选性;
5..运行时权限(危险权限和正常权限);
6.电源管理,APP standby(应用待机),Doze(瞌睡),Exemptions(豁免),生命周期等模式来加强电源管理。

Android 6.0引入了动态权限管理,将标记为危险的权限从安装时权限(Install Time Permission)模型移动到运行时权限模型(Runtime Permissions):
 
安装时权限模型(Android5.1以及更早):用户在应用安装和更新时,对危险权限授权。但是OEM和运行商预装的应用将自动预授权。
运行时权限(Android6.0及以后):用户在应用运行时,对应用授予危险权限。由应用决定何时去申请权限(例如,在应用启动时或者用户访问某个特性时),但必须容许用户来授予或者拒绝应用对特定权限组的访问。OEM和运营商可以预装应用,但是不能对权限进行预授权。
 
Android6.0并不改变正常权限的行为。正常权限指的是所有非危险(non-dangerous)权限,包括normal,system和signature 权限。正常权限风险较低,用于容许应用以最小风险来访问隔离的应用级别的特性。在Android5.1和早期版本中,系统在安装时自动将正常权限授予请求的应用,并不提示用户
 
运行时权限模型必须在所有运行Android6.0的设备上是一致的。这通过CTS来实施。
应用必须在运行时提示用户进行授权。
  • Android7.0
提升性能、生产效率和安全性,无缝更新
1.对文件数据加密,更加安全;
2.添加了分屏多任务;
3.全新下拉快捷开关页,例如WiFi的开关;
4.重新设计了通知,消息归拢,通知消息快捷回复,如视频全屏下,屏幕顶部出现两个按钮(接听、挂掉);
5.夜间模式;
6.流量保护;
7.全新设置样式;
8.改进Doze休眠机制;
9.系统级电话黑名单功能;
10.菜单键快速应用切换;
Android 7.0 新特性

Android7.0提供新功能以提升性能、生产效率和安全性。

关于Android N的性能改进,Android N建立了先进的图形处理Vulkan系统,能少的减少对CPU的占用。与此同时,Android N加入了JIT编译器,安装程序快了75%,所占空间减少了50%。

在安全性上,Android N加入了全新安全性能,其中包括基于文件的数据加密。谷歌移动版Chrome能识别恶意网站。

Android N可以进行无缝更新,与Chromebook一样,用户将不再需要下载安装,也不再需要进行重启。

在效率提升上,Android N可以自动关闭用户较长时间未使用的应用程序。在通知上新增了直接回复功能,并支持一键全部清除功能

分屏多任务

进入后台多任务管理页面,然后按住其中一个卡片,然后向上拖动至顶部即可开启分屏多任务,支持上下分栏和左右分栏,允许拖动中间的分割线调整两个APP所占的比例。

全新下拉快捷开关页

在Android 7.0中,下拉打开通知栏顶部即可显示5个用户常用的快捷开关,支持单击开关以及长按进入对应设置。如果继续下拉通知栏即可显示全部快捷开关,此外在快捷开关页右下角也会显示一个“编辑”按钮,点击之后即可自定义添加/删除快捷开关,或拖动进行排序。

新通知消息

通知消息快捷回复
加入了全新的API,支持第三方应用通知的快捷操作和回复,例如来电会以横幅方式在屏幕顶部出现,提供接听/挂断两个按钮;信息/社交类应用通知,还可以直接打开键盘,在输入栏里进行快捷回复。

通知消息归拢
Android7.0会将同一应用的多条通知提示消息归拢为一项,点击该项即可展开此前的全部通知,允许用户对每个通知执行单独操作。

菜单键快速应用切换
双击菜单键,就能自动切换到上一个应用。此外,如果你不停地点击菜单键的话,就会在所有应用中不间断地轮换,应用窗口会自动放大,顶部还会出现倒计时条,停止点击且倒计时结束后,当前应用会自动放大并返回到前台。

改进的Doze休眠机制
谷歌在Android7.0中对Doze休眠机制做了进一步的优化,在此前的Android6.0中,Doze深度休眠机制对于改善安卓的续航提供了巨大的作用。而在Android67.0中,谷歌对Doze进行了更多的优化,休眠机制的使用规则和场景有所扩展,例如只要手动在后台删掉应用卡片,关屏后该应用就会被很快深度休眠。

系统级电话黑名单功能
Android7.0将电话拦截功能变成了一个系统级功能。其它应用可以调用这个拦截名单,但只有个别应用可以写入,包括拨号应用、默认的短信应用等。被拦截号码将不会出现在来电记录中,也不会出现通知。另外用户也可以通过账户体系备份和恢复这个拦截名单,以便快速导入其它设备或账号。 

夜间模式
Android7.0中重新加入了夜间深色主题模式,该功能依然需要在系统调谐器中开启,从顶部下划打开快捷设置页,然后长按其中的设置图标,齿轮旋转10秒钟左右即可提示已开启系统调谐器,之后用户在设置中即可找到“系统调谐器”设置项。点开其中的“色彩和外观”,即可找到夜间模式,开启后即可使用全局的深色主题模式,同时亮度和色彩也会进行一定的调整,该功能可以基于时间或地理位置自动开启。另外,系统调谐器中也提供了RGB红绿蓝三色调节滑动条,允许用户手动精细调节,例如减少蓝色或增加红色以提供类似护眼模式的效果。

流量保护模式
新增的流量保护模式不仅可以禁止应用在后台使用流量,还会进一步减少该应用在前台时的流量使用。推测其有可能使用了类似Chrome浏览器的数据压缩技术。此外,谷歌还扩展了ConnectivityManager API的能力,使得应用可以检测系统是否开启了流量保护模式,或者检测自己是否在白名单中。Android7.0允许用户单独针对每个应用,选择是否开启数据保护模式。
  • Android8.0
1,收不到通知
Android O版本对通知做了规范性的控制,强制用户在发送通知的时候,对通知进		行系统性的管理,新增了channel渠道功能,所以创建通知时需要给通知添加通知		渠道NotificationChannel(其实7.0部分设备就有这个问题) 
2,不依附于activity的悬浮窗不能显示
在广播中弹出对话框时,需要将layout_flag设置为		windowmanager.layoutparams.TYPE_APPLICATION_OVERLAY.
3, 涉及到需要访问清单文件或者pakagemanager类的时候,需要加入权限	
<uses-permission 	android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  • Android9.0
1,网络请求错误
以前我在开发的时候遇到这样一个问题,就是网络请求在其他设备上都能成功获取数据,但在Android9.0设备上经常出现请求不成功的情况(java.net.UnknownServiceException: CLEARTEXT communication to www.mianfeibox.cn not     permitted by network)因为(Android P(9.0)) Android9.0以后限制了非加密的流量请求,导致出现 not permitted by network security policy,所以后来我在谷歌官方文档查了一些资料,其实解决也挺简单的,就是在res目录下新建一个xml文件夹,里边创建一个network-security-config文件,将cleartextTrafficPermitted设置为"true",然后在清单文件的application节点下,有一个networkSecurityConfig属性,将新建的xml文件引用到这个属性当中,这个问题就解决了。
2,错误弹窗
Android9.0以后部分手机打开应用会出现一个错误提示弹窗(detected problems with API compatibility),出现这种情况的原因是:Android P 后谷歌限制了开发者调用非官方公开API 方法或接口,如果代码中直接用反射调用源码就会出现这个错误弹窗,可以通过反射在APP启动的时候干掉这个弹窗。
3,非全屏的activity不能设置orientation
当activity的style设置了WindowsTranslucent 为 true或者WindowsTranslucent为false但windowSwipeToDismiss为true,或者WindowsFloating为true,Android9.0认为不是Fullscreen,这样做主要是想阻止非全屏activity锁定屏幕旋转而抢占全屏activity的焦点,所以会报错

4,qq分享报错,提示找不到schemeRegistry类
Android9.0完全弃用了Apache http客户端,想要继续使用,需要在清单文件中添加android:required=”false”

view得绘制流程

  • Android中View的绘制主要由三个核心流程实现
1. measure流程

【流程解析】 

为整个View树计算实际的大小,即设置实际的宽和高(View.java中对应的属性为mMeasureWidth和mMeasuredHeight)。每个View的大小都是由父VIew和本身决定的。

【特别说明】

设置View的宽高是通过调用setMeasureDimension()方法去设置实际的宽和高。

如果View对象是一个ViewGroup类型,需要重写其onMeasure()方法,并对子视图遍历measure()过程。

整个measure的调用过程是个树形的递归过程。

2. layout流程

【流程解析】

根据View树上各个View(ViewGroup)的大小和布局参数,将整个View树布局到合适的位置上。

【特别说明】

View.layout()方法会设置该View视图位于父视图的坐标轴,即mLeft, mTop, mRight, mBottom,接着会回调onLayout()方法。

 如果该View是ViewGroup类型,需要遍历每个子视图,调用该子视图的layout()方法去设置其布局坐标值。

3. draw流程

【流程解析】

draw流程主要是在View.draw()方法中实现,主要包括以下流程:

  1. 绘制该View的背景等。

  2. 调用onDraw()方法绘制View自身(每个View都需要重载改方法,ViewGroup不需要实现该方法)。

  3. 调用dispatchDraw()方法绘制子视图(如果该View不是ViewGroup类型,则不需要重载该方法),dispatchDraw()方法内部会遍历每个子视图,调用drawChild()方法去重新回调每个子视图的draw()方法。

【特别说明】

每次发起绘制时,并不会重新绘制整个View树,而只会绘制那些“需要绘制”的视图(由View内部变量DRAWN标志)。

ViewGroup类已经为我们重写了dispatchDraw()功能实现,我们一般不需要实现该方法。
  • View绘制流程三大核心分析
measure过程分析
因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌View或ViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。

在View类中measure过程主要涉及三个函数,函数原型分别为

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measure子View,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是实现这个功能的。

在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpec的makeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。

MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意义如下

如果是AT_MOST,specSize代表的是最大可获得的尺寸;

如果是EXACTLY,specSize代表的是精确的尺寸;

如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。

那么对于一个View的上述Mode和specSize值默认是怎么获取的呢,他们是根据View的LayoutParams参数来获取的:

参数为fill_parent/match_parent时,Mode为EXACTLY,specSize为剩余的所有空间;

参数为具体的数值,比如像素值(px或dp),Mode为EXACTLY,specSize为传入的值;

参数为wrap_content,Mode为AT_MOST,specSize运行时决定。

具体测量原理
上面提供的Mode和specSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单列表说明下三者的关系
父视图能力尺寸 子视图期望尺寸 子视图最终允许尺寸
EXACTLY + Size1 EXACTLY + Size2 EXACTLY + Size2
EXACTLY + Size1 fill_parent/match_parent EXACTLY+Size1
EXACTLY + Size1 wrap_content AT_MOST+Size1
AT_MOST+Size1 EXACTLY + Size2 EXACTLY+Size2
AT_MOST+Size1 fill_parent/match_parent AT_MOST+Size1
AT_MOST+Size1 wrap_content AT_MOST+Size1
UNSPECIFIED+Size1 EXACTLY + Size2 EXACTLY + Size2
UNSPECIFIED+Size1 fill_parent/match_parent UNSPECIFIED+0
UNSPECIFIED+Size1 wrap_content UNSPECIFIED+0
上述表格展现的是子视图最终允许得到的尺寸,显然1、4、7三项没有对Size1和Size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢? 

通过Demo演示的得到的结果,假如Size2比Size1的尺寸大,假如不使用滚动效果的话,子视图超出部分将被裁剪掉,该父视图中如果在该子视图后面还有其他视图,那么也将被裁剪掉,但是通过调用其getVisibility还是显示该控件是可见的,所以裁剪后控件依然是有的,只是用户没办法观察到;在使用滚动效果的情况下,就能将原本被裁剪掉的控件通过滚动显示出来。

对于第二个问题,根据源码View的OnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeight和android:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextView在AT_MOST+Size情况下,并不是以Size作为控件的最终尺寸,结果发现在TextView的源码中,重载了onMeasure函数,有价值的代码如下:
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (widthMode == MeasureSpec.AT_MOST) {

    width = Math.min(widthSize, width);

}
if (heightMode == MeasureSpec.AT_MOST) {

    height = Math.min(desired, heightSize);

}
layout过程分析
上述measure过程达到的结果是设定了视图的高和宽,layout过程的作用就是设定视图在父视图中的四个点(分别对应View四个成员变量mLeft,mTop,mLeft,mBottom)。同样layout也是被fianl修饰符限定为不能重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有子视图的布局设定。

那么上述的measure结果与layout有什么关系,截取ViewRoot和FrameLayout两个类中onLayout函数的部分代码如下:

//ViewRoot的performTraversals函数measure之后对layout的调用代码

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
//FrameLayou的onLayout函数部分源码
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    
        final int count = getChildCount();
        ……
        for (int i = 0; i < count; i++) {
    
    
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
    
    
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft = parentLeft;
                int childTop = parentTop;
                final int gravity = lp.gravity;
                if (gravity != -1) {
    
    
                    final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 
                    switch (horizontalGravity) {
    
    
                        case Gravity.LEFT:
                            childLeft = parentLeft + lp.leftMargin;
                            break;
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
                            break;
                        case Gravity.RIGHT:
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        default:
                            childLeft = parentLeft + lp.leftMargin;
                    } 
                    switch (verticalGravity) {
    
    
                        case Gravity.TOP:
                            childTop = parentTop + lp.topMargin;
                            break;
                        case Gravity.CENTER_VERTICAL:
                            childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;
                            break;
                        case Gravity.BOTTOM:
                            childTop = parentBottom - height - lp.bottomMargin;
                            break;
                        default:
                            childTop = parentTop + lp.topMargin;
                    }
                } 
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
从代码显然可知具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。

需要注意的是在自定义组合控件的时候,我们可以根据需要不用或只用部分measure过程计算得到的尺寸,具体可以看下之前做的下拉刷新控件直接重载的onLayout函数:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    
    if (getChildCount() > 2) {
    
    
        throw new IllegalStateException("NPullToFreshContainer can host only two direct child");
    }        
   View headView = getChildAt(0);
    View contentView = getChildAt(1);
    if(headView != null){
    
    
     headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值
    }
    if(contentView != null){
    
    
    contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());
    }        
    if (mFirstLayout) {
    
            
     HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();
       mFirstLayout = false;
    }
}
draw过程分析
View的Draw过程,其实相对来说应该比measure过程更为复杂,正因为其很复杂,所以android框架层已经将draw过程考虑得相当周全,虽然view类的Draw函数没用final修饰,但是我们自定义的View,一般也不需要去重载实现它,自己目前也没有自己去draw过界面,对整个过程,只能偷别人整理的逻辑,结合源码浏览了一下,在这里做个标注。

draw()方法实现的功能流程如下:

1、调用background.draw(canvas)绘制该View的背景

2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)

3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)

4、调用onDrawScrollBars(canvas)绘制滚动条

为了说明measure、layout和draw过程的连续性,摘得draw中的源码如下

if (mBackgroundSizeChanged) {
    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
    mBackgroundSizeChanged = false;
}
上述的mLeft,mTop,mLeft,mBottom就是我们在layout是设定的结果值,这里之所以要用减法获取高宽尺寸而不用measure过程设定的mMeasuredHeight和mMeasureWidth,个人感觉就是因为我们可以在代码中通过直接调用View的layout函数避开measure测算结果而导致真实高宽不等于mMeasuredHeight和mMeasureWidth这种情况。

上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout过程调用的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)这三个函数中对其修改为true。

到这里,除了具体的绘制外,我们对从Activity到View的绘制流程应该比较清楚了。
  • 为什么要进行View的绘制?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6KyafEV-1599705435169)(D:\学习成功之路\Android中级文件夹\图库\2397836-f1f6a200704884a2.png)]

DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

Window
Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。在介绍这个类之前,我们先来看看究竟什么是窗口呢?

实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:

独立绘制,不与其它界面相互影响;
不会触发其它界面的输入事件;
在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。

android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。

这个抽象类包含了三个核心组件:

WindowManager.LayoutParams: 窗口的布局参数;
Callback: 窗口的回调接口,通常由Activity实现;
ViewTree: 窗口所承载的控件树。
下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。

PhoneWindow
前面我们提到了,PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现。我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。
  • View 树的绘图流程
当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FVDDcsm-1599705435170)(file:///C:\Users\SAMSUNG\AppData\Local\Temp\ksohtml4684\wps1.jpg)]

需要说明的是,用户主动调用 request,只会出发 measure 和 layout 过程,而不会执行 draw 过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VmapuxNy-1599705435170)(file:///C:\Users\SAMSUNG\AppData\Local\Temp\ksohtml4684\wps2.jpg)]

自定义View

  • 为什么要自定义View
android提供了很多控件供我们使用 但有些功能是系统所提供的实现不 
了的 这时候我们就需要自定义一个View来实现我们所需要的效果. 
在Android中所有的控件都直接或间接的继承自View,分View和ViewGroup两部分. 
我们常用的一些View比如TextView,ImageView都是继承自View并添加了一些各自的特性,ViewGroup也是继承View但是它可以包含子View也就是我们经常用到的各种布局比如LinearLayout,RelativeLayout等。
总而言之:就是Android自带的无法实现的效果需要自定义View来实现
  • 如何自定义View
1、实现一个自己的View那就集成View并添加一些自己需要的特性即可. 
以下是一个最简单的自定义View

public class MVIew extends View {
    
    
    /**
     * 创建一个继承View的class
     *View一共有四个构造器 这里先说两个
     * 第一个构造器的参数就是context,这是在Activity实例化的时候所需的,比如Button button = new Button(context);
     * 第二个构造器有两个参数 第一个同样是context 第二个参数AttributeSet放入了一些属性,这是我们在写XML文件时用到的比如
     * android:layout_width="match_parent"
     * android:layout_height="match_parent"如果不写函数的话是无法通过XML添加View
     */
    public MVIew(Context context) {
    
    
        super(context);
    }

    public MVIew(Context context, @Nullable AttributeSet attrs) {
    
    
        super(context, attrs);
    }

    //重写onDraw方法
    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        canvas.drawColor(Color.RED);      //设置canvas的背景色
        float radius = 50;                //给定半径
        //给定圆心的的坐标
        float cx = 50;
        float cy = 50;
        Paint paint = new Paint();       //实例化一个Paint对象
        paint.setColor(Color.BLUE);      //设置圆的颜色
        //通过canvas的drawCircle方法画一个圆圈.
        canvas.drawCircle(cx, cy, radius, paint);
    }
}
2、这个自定义的View就是画了一个圆圈,接下来就是让这个view加载到activity上了.第一种就是直接在Activity上实例化一个并通过addView(View)来添加

protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MVIew mvIew = new MVIew(getApplicationContext());                                //实例化自定义的View
        RelativeLayout main_layout = (RelativeLayout) findViewById(R.id.main_layout);   //获取布局的对象
        main_layout.addView(mvIew);                                                     //向布局文件添加View
    }
}
3、第二种就是最常用的通过XML文件去添加
<cn.lipengfei.myview.MVIew
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  • View的测量
这涉及到View的测量. 
Android由一个MeasureSpec的类,可以通过它来说实现测量. 
MeasureSpec是一个int型的值,并且采用了位运算,高两位为测量的模式,30位是具体的值. 
由三种测量模式

- EXACTLY 精确模式 例如layout_height=50dp”或是=”match_parent”
- AT_MOST 最大值模式 就是warp_content
- UNSPECIFIED 通过字面意思就是没有指定尺寸
* Android通过onMeasure()测量View的大小,默认情况下是EXACTLY模式,所以刚才的例子虽然写了warp_concent依然没有起作用,如果想实现AT_MOST模式那就需要我们重写onMeasure()这个方法.

eg:
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        //先声明两个int值来表示最终的width和height并给定一个默认的大小
        int width_size  = 100;
        int height_size = 100;
        //使用MeasureSpec分别获取width和height的MODE和SIZE
        int HEIGHT_MODE = MeasureSpec.getMode(heightMeasureSpec);
        int HEIGHT_SIZE = MeasureSpec.getSize(heightMeasureSpec);
        int WIDTH_MODE = MeasureSpec.getMode(widthMeasureSpec);
        int WIDTH_SIZE = MeasureSpec.getSize(widthMeasureSpec);
        if (HEIGHT_MODE == MeasureSpec.EXACTLY) {
    
    
            height_size = HEIGHT_SIZE;       //如果测量模式是精确的话 那么就直接使用获取到的值就好了
        }else if (HEIGHT_MODE == MeasureSpec.AT_MOST){
    
      //如果是最大值模式的话 那么久比较获取的和设定的默认值那个小就使用那个.ps:Math.min(int a,int b)是比较数值大小的.
            height_size = Math.min(HEIGHT_SIZE,height_size);
        }
        if (WIDTH_MODE == MeasureSpec.EXACTLY){
    
    
            width_size = WIDTH_SIZE;
        }else if (WIDTH_MODE == MeasureSpec.AT_MOST){
    
    
            width_size = Math.min(WIDTH_SIZE,width_size);
        }
        //测量完毕后得到的了width和height通过setMeasuredDimension()设置width和height,真正决定具体大小的是setMeasuredDimension()的两个参数.
        setMeasuredDimension(width_size,height_size);
    }
  • onDraw
测量完成后就有了大小接下来就是内容了,我们需要在view上显示什么就重写onDraw来实现,以上例子是通过onDraw(Canvas canvas)绘制了一个圆圈. 
首先要说的是Canvas,刚才的例子就是通过 canvas.drawCircle(cx, cy, radius, paint);这样一个方法来画了一个圆.这里不对Canvas展开来说,只是说一些Canvas的简单用法. 
Canvas就是一个画板,可以在这个画板上话各种各样的东西
eg:
	  Paint paint = new Paint();       //实例化一个Paint对象
      paint.setColor(Color.BLACK);
      paint.setStrokeWidth(10);//因为默认实在是太细了 设定了一个宽度值
      canvas.drawLine(0,0,100,100,paint);
这就是在画了一条宽度为10的黑色的线drawLine()的前两个参数为开始点的坐标后两个为结束点的坐标,最后一个参数就是paint; 

还有很多方法可以调这里就不一一列举了,可以根据canvas.的提示来试试 
晚些时候我会把我总结的一些方法写出来方便初学者来看看. 
除了Canvas外还有一个Paint的对象也总用到 这个是画笔的意思,比如化园的例子就是通过这个画笔来设定的园的颜色,还有画线的例子也是通过画笔来设定宽度值,如果我们吧之前的代码改一下
canvas.drawColor(Color.RED);      //设置canvas的背景色
        float radius = 50;                //给定半径
        //给定圆心的的坐标
        float cx = 50;
        float cy = 50;
        Paint paint = new Paint();       //实例化一个Paint对象
        paint.setColor(Color.BLACK);     //设置圆的颜色
        paint.setAntiAlias(true);        //设置抗锯齿
        paint.setStyle(Paint.Style.STROKE);  //设置样式
        paint.setStrokeWidth(3);          //设置宽度
        //通过canvas的drawCircle方法画一个圆圈.
        canvas.drawCircle(cx, cy, radius, paint);

其他的都没有变 只是添加了三个
        paint.setAntiAlias(true);        //设置抗锯齿
        paint.setStyle(Paint.Style.STROKE);  //设置样式
        paint.setStrokeWidth(3);          //设置宽度
效果就是这个样子 在样式里面设置了STROKE表示只描边也就是空心效果与之相对的还有两个属性分别是FILL_AND_STROKE,FILL,刚开始有些人搞不清这俩有啥区别,譬如我.FILL就是填充上也就是一个实心的圆圈,FILL_AND_STROKE是不仅填充成一个实心圆而且还有边框,Paint并没有单独给STROKE设置颜色的方法(至少我没发现) 
这俩的区别我举个例子就很清楚了
paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(50); 
这是设置成FILL并添加边框出来的效果
这样的效果圆已经超出去一部分了,设定了填充+边框 不仅由填充效果还有边框,给边框设定了宽度值,再加上我设定的圆心坐标正好是半径值 所以边框的部分就出去了.但如果我们不设setStrokeWidth的话 这两者其实是没有什么区别的.另外要注意的是StrokeWidth的值是在园外面的,也就是说它并不占用园的实际大小,比如园的半径是100,这个半径指的是填充的部分,当把StrokeWidth设定为100时 这个圆会变大 
刚才我提到Paint并没有设置STROKE的颜色的方法,所以我是通过两个画笔来实现的,通过多个画笔多个画布叠加图层来实现我们想要的各种效果 
比如我现在想要一个边框为蓝色的填充为黑色的圆圈.
  @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        canvas.drawColor(Color.RED);     
        float radius = 200;       
        float cx = 500;
        float cy = 500;
        Paint paint = new Paint();       //实例化Paint
        paint.setColor(Color.BLACK);     //设置圆的颜色
        paint.setAntiAlias(true);        //设置抗锯齿
        paint.setStyle(Paint.Style.FILL_AND_STROKE);  //设置样式
        Paint paint2 = new Paint();      //实例化第二个paint对象
        paint2.setColor(Color.BLUE);     //设置颜色为蓝色
        paint2.setStyle(Paint.Style.STROKE);//设置样式
        paint2.setStrokeWidth(30);          //设定边框宽度
        //通过canvas的drawCircle方法画一个圆圈.
        canvas.drawCircle(cx, cy, radius, paint);
        canvas.drawCircle(cx, cy, radius, paint2);
        }
需要注意的是canvas.draw的先后顺序 因为这两个圆是一样大小的 只是一个多一个边框而已 所以先后是无所谓的.
    最终效果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hVRsAvrw-1599705435171)(D:\学习成功之路\Android中级文件夹\图库\20170319153234654.png)]

猜你喜欢

转载自blog.csdn.net/weixin_44440150/article/details/90518243
今日推荐