知识点梳理7 必备必会

屏幕适配

android 适配笔记

屏幕尺寸
含义:手机对角线的物理尺寸
单位:英寸(inch),1英寸=2.54cm
dp 屏幕像素密度
含义:每英寸的像素点数
单位:dpi(dots per ich)

知识点梳理7 必备必会_第1张图片
image.png
知识点梳理7 必备必会_第2张图片
image.png
知识点梳理7 必备必会_第3张图片
image.png
知识点梳理7 必备必会_第4张图片
image.png

自定义 view

自定义 view - 综述

自定义 view - 前置知识点

知识点梳理7 必备必会_第5张图片
image.png

自定义 view - 自定义属性

attr 中的自定义属性样式

    
        
        
    

自定义 view - 布局 onLayout

onMeasure
根据 view 的测量模式计算确定 view 的宽高
onLayout
ViewGroup 中对所有的子 view 排版,决定子 view 的位置
onDraw
具体绘制 view

知识点梳理7 必备必会_第6张图片
image.png
知识点梳理7 必备必会_第7张图片
image.png
知识点梳理7 必备必会_第8张图片
image.png

https://www.androidos.net.cn/android/8.0.0_r4/xref/frameworks/base/core/java/android/view/View.java

自定义 view - 测量 onMeasure

知识点梳理7 必备必会_第9张图片
image.png
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 获取宽的测量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 获取符控件提供的 view 宽的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}
知识点梳理7 必备必会_第10张图片
image.png

ViewGroup的测量过程主要用到了三个方法:

measureChildren()
遍历所有的childView
measureChild()
调用测量规格
getChildMeasureSpec()
确定测量规格

/**
  * 源码分析:getChildMeasureSpec()
  * 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //参数说明
         * @param spec 父view的详细测量值(MeasureSpec) 
         * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
         * @param childDimension 子视图的布局参数(宽/高)

            //父view的测量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的实际大小和模式(需要计算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


            // 当父view的模式为EXACITY时,父view强加给子view确切的值
           //一般是父view设置为match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 当子view的LayoutParams>0,即有确切的值  
                if (childDimension >= 0) {  
                    //子view大小为子自身所赋的值,模式大小为EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为MATCH_PARENT时(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小为父view大小,模式为EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
            // 多见于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小为子自身所赋的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }

自定义 view - 3大核心方法补充

getWidth() / getHeight():获得View最终的宽 / 高
getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高

UI 重绘方法:

invalidate()
重绘视图,必须 Ui 线程执行
postInvalidate()
重绘视图,使用 handle 消息机制允许异步执行
requestLayout()
重新布局
计算,布局,绘制 3个方法方法都触发了
requestFocus
局部刷新,只重绘焦点部分或是我们指定的部分

ViewGroup 绘制一次会调用子 view 的2次 onMeasure ,2次 onLayout ,1次 onDraw,部分 ViewGroup 具体子类会对自身测量2次,那么就会 调用子 view 的4次 onMeasure

FrameLayout 帧布局
view 不论用的哪种测量模式都会造成子 view 2次测量

ConstraintLayout 约束布局
view 使用 match_parent 会执行4测测量,使用具体的宽高值时,比如200dp,只会执行2次测量

RelativeLayout 相对布局
不管是 match_parent 还是 200dp ,都会触发自子 view 4次测量

LinearLayout 线性布局
同 FrameLayout 帧布局,只会2次测量

systrace 工具自动分析已经开始提示昂贵的测量,布局损耗了,仅仅只是 16 次测量而已,就花了 6 毫秒,大家看到没 view 的 测量很耗费 cpu 资源的

事件分发,滑动冲突

android 事件分发原理

事件分发3大方法
dispatchTouchEvent 事件分发
onInterceptTouchEvent 事件拦截
onTouchEvent 事件处理

onInterceptTouchEvent 方法默认是返回 false 的,表示不会拦截这个事件,但是若是返回 true 则表示这个事件我拦截住了,就不再往我下一级 view 传递了,然后会把事件交给自己的 onTouchEvent 方法

getParent().requestDisallowInterceptTouchEvent(true);

顺序:

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
知识点梳理7 必备必会_第11张图片
image.png

事件分发原理:责任链模式,事件层层传递,直到被消费。
View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后内容。
如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

android 滑动冲突

同方向滑动冲突:
比如ScrollView嵌套ListView,或者是ScrollView嵌套自己

不同方向滑动冲突:
比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,这种情况其实很典型。现在大部分应用最外层都是ViewPager+Fragment 的底部切换(比如微信)结构,这种时候,就很容易出现滑动冲突。

ScrollView嵌套ViewPager冲突
自定义一个MyScrollView继承ScrollView,重写 onInterceptTouchEvent方法,在 Action_Move事件中判断,如果水平滑动距离大于竖直滑动距离,则return false,表示不拦截事件,把事件分发到下一级控件,交由下一级处理

public class MyScrollView extends ScrollView{

    private float xDistance;
    private float yDistance;
    private float xLast;
    private float yLast;

    /**
     * 在该方法中进行判断
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDistance = yDistance = 0.0f;
                xLast = ev.getX();
                yLast = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float curX = ev.getX();
                final float curY = ev.getY();

                xDistance += Math.abs(curX - xLast);
                yDistance += Math.abs(curY - yLast);

                if(xDistance > yDistance)
                    return false;

                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

ScrollView 嵌套 ScrollView
在 onInterceptTouchEvent 事件拦截函数内巧用 getParent().requestDisallowInterceptTouchEvent(true) 就可以让所有的父控件不拦截我们的事件了。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.onInterceptTouchEvent(ev);
    }

NestedScrollView 嵌套 RecyclerView
这是最简单的处理嵌套滚动冲突的办法了,典型案例:NestedScrollView 嵌套 RecyclerView

参考:NestedScrollView+RecyclerView优雅的解决滑动冲突

//布局文件的RecyclerView中设置
android:nestedScrollingEnabled="false" 
//或者Java代码设置
recyclerView.setNestedScrollingEnabled(false);

说下缺点,禁用 RecyclerView 的滚动之后,在滚动嵌套中,局域内层的 RecyclerView 还是会首先收到触摸事件的,对于非滚动方向的事件在处理后,上层滚动控件比如 NestedScrollView 是收不到的。

典型的例子就是在这个嵌套页面中,我们在内层列表的位置斜的角度稍微大点上下滑,你会发现我滑动的手势被列表吃了,外面的滚动控件不会滚动。

禁止 Viewpager 滑动
平时我们有时是由这个需求的,那么怎么处理 Viewpager 呢,其实看过上面之后,这个问题给为其实可以自己解决的了了。

我们不让 Viewpager 拦截事件,Viewpager 就那不到事件就不能滑动,我们不让 Viewpager 消费事件,那么在 Viewpager 内层的 view 不处理事件时把事件回传给上级的 Viewpager 时,Viewpager 也不会实际产生话滑动,我们把 Viewpager 实际处理事件的代码全部删掉。然后这么写就行

public class CustomViewPager extends ViewPager {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

android 滑动拓展使用

滑动相关开源库汇总

  1. 列表项侧滑 深入聊聊Android事件分发机制
  2. 上拉显示新一页 Android滑动冲突解决方法(二)
  3. 上拉加载,下拉刷新 用事件分发的原理结合SwipeRefreshLayout写一个RecyclerView的上下拉
    这个例子是一般,对于列表的2个刷新还要是看更好的资料
  4. 右滑退出

查了好多资料,看到的大家都是使用 swipebacklayout 这个开源库,swipebacklayout 的代码简介,使用简单,易于理解。

SwipeBackActivity 需要注意的是我们的 BaseActivty 需要继承 SwipeBackActivity 这个类实现右滑退出功能,SwipeBackActivity 继承的是 AppCompatActivity 这个 Activity,AppCompatActivity 不能用的各位需要自行实现了

public class SwipeBackActivity extends AppCompatActivity implements SwipeBackActivityBase {
  ......
}

SwipeBackActivity 使用文章:

  • Android 集成右滑退出
  • android Activity右滑返回,退出当前activity
  • Android右滑退出+沉浸式(透明)状态栏
  • 100行代码实现Activity右滑退出
  • Android向右滑动关闭界面(仿iOS、SwipeBackActivity)
  • 权限

    • android 6.0动态权限
    • AndPermission 集成使用
    • RxPermission 集成使用
    • 简单对权限开源库进行功能性封装
  • Server 、ALDL

  • 创建使用 AIDL

AIDL 是 Android 特有的 IPC 进程间通讯方式

AIDL 的写法其实和绑定服务的代码差不多,IBinder 也是 android 默认提供的一个 AIDL 接口

需要注意的是 5.0 之后,不能隐式启动 Service,不能想以前一样定义 action 来启动服务了,尤其是不是跨应用启动服务,这也算是一种安全上的考虑

  • Service服务知识点总结

startService
bindService
ServiceConnection
IBinder
Binder

这样写,在 SDK 18之前,在通知栏不是显示通知, 18之后就会显示一条通知了,这是系统默认的对用户的友好提示,要是不想让用户看到,要再处理
SDK 21 之后,新添加了 startForegroundService(intent) 方法,可以直接启动服务为前台进程了。

如何启动一个没有Notification的前台服务呢
这是利用系统的 bug 了,据说这个把bug 在7.1修复了,但是还是可以易用不是,具体步奏:

编写2个 service ,A/B,B 就是消除通知的,注意 A和B 要使用同一个 通知的 id,核心就在这里
先启动 A 服务
再启动 B 服务,然后B 再关闭自己,利用前台服务在关闭后会同步关闭通知的特性,来实现没有通知显示的前台服务,这样 A 就达到目的了

Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service。如果使用隐式启动Service,会出没有指明Intent的错误

Service 的包活除了设置成前台服务之外,还可以在 onDestory 方法中再把自己跑起来,或者2个服务相互守护

  • 广播

    • 使用代码动态注册广播接收器
  • 其他

  • 简述一下Android系统的架构

知识点梳理7 必备必会_第12张图片
image.png
  • android 进程相关
    前台进程
    可见进程
    服务进程
    后台进程
    空进程

你可能感兴趣的:(知识点梳理7 必备必会)