Android中的自定义组件和android的动画

自定义View

Activity、PhoneWindow、DecorView、ViewRoot

 

1、Activity 控制器
a、【Activity】并【不负责视图控制】,它只是控制【生命周期】和【处理事件】。【真正控制视图】的是【Window】。

b、【一个Activity】包含了【一个Window】,【Window】才是【真正代表一个窗口】。

c、【Activity】就像【一个控制器】,【统筹视图】的【添加与显示】,以及通过其他回调方法,来与Window、以及View进行交互。

 

2、Window 承载器
a、Window是【视图的承载器】,承载【视图View】的显示。

b、【Window内部】持有一个 【DecorView】,而这个DecorView才是 【view 的根布局】。

c、Window是一个【抽象类】,实际在Activity中持有的是其【子类PhoneWindow】。

d、【PhoneWindow】中有个【内部类DecorView】,通过【创建DecorView】来【加载Activity】中设置的【布局R.layout.activity_main】。

e、【Window】 通过【WindowManager】将【DecorView加载】其中,并将【DecorView交给ViewRoot】,进行【视图绘制】以及其他交互。

 

WindowManager & WindowManagerService
带你彻底理解 Window 和 WindowManager

 

一、Window 分类
a、Window 有三种类型,分别是【 应用Window】、【子 Window 】和【系统 Window】。

b、【应用类 Window 对应一个 Acitivity】,子 Window 不能单独存在,需要依附在【特定的父 Window 】中,比如常见的一些 Dialog 就是一个子 Window。

c、【系统 Window】是【需要声明权限】才能【创建的 Window】,比如 Toast 和系统状态栏都是系统 Window。

d、【Window 是分层的】,【每个 Window】 都有对应的 【z-ordered】,层级大的会覆盖在层级小的 Window 上面,这和 HTML 中的 z-index 概念是完全一致的。

e、在三种 Window 中,应用 Window 层级范围是 【1 ~ 99】,子 Window 层级范围是 【1000 ~ 1999】,系统 Window 层级范围是 【2000 ~ 2999

 

二、WindowManagerService
a、WindowManagerService 就是【位于 Framework 层】(Android Application层)的【窗口管理服务】,它的职责就是【管理系统中的所有窗口】。

b、【窗口的本质】是什么呢?其实就是【一块显示区域】,在 Android 中就是【绘制的画布:Surface】,当一块 Surface 显示在屏幕上时,就是用户所看到的窗口了。

c、WindowManagerService 【添加一个窗口】的过程,其实就是 WindowManagerService 为其【分配一块 Surface】 的过程,一块块的 Surface 在 WindowManagerService 的管理下有序的排列在屏幕上,Android 才得以呈现出多姿多彩的界面。

d、于是根据对 【Surface 的操作类型】可以将 Android 的显示系统分为三个层次,如下图:

在这里插入图片描述

e、一般的开发过程中,我们操作的是 【UI 框架层】,对 Window 的操作通过 WindowManager 即可完成,而 WindowManagerService 作为系统级服务运行在一个单独的进程,所以 WindowManager 和 WindowManagerService 的交互是一个【 IPC】 过程。

 

三、WindowManager
a、在实际使用中无法直接访问 Window,我们对 Window 的操作是通过 【WindowManager 来完成】的,WindowManager 是一个接口,它继承自只有三个方法的 【ViewManager 接口】:

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

b、这三个方法其实就是 【WindowManager 对外提供的主要功能】,即【添加 View、更新 View 和删除 View】。

c、WindowManager 最终都会通过一个 IPC 过程将操作【移交给 WindowManagerService】 这个位于 Framework 层的窗口管理服务来处理。

 

3、DecorView 顶级View

a、【DecorView】是【FrameLayout的子类】,它可以被认为是【Android视图树的根节点/顶级视图】。用于【显示 & 加载视图】。

b、它内部包含一个【竖直方向的LinearLayout】,在这个LinearLayout里面有两个部分:【标题栏】(根据Theme设置,有的布局没有),下面的是【内容栏】。 具体情况和Android版本及主体有关,以其中一个布局为例,如下所示:
 

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <FrameLayout
        style="?android:attr/windowTitleBackgroundStyle"
        android:layout_width="match_parent"
        android:layout_height="?android:attr/windowTitleSize">

        <TextView
            android:id="@android:id/title"
            style="?android:attr/windowTitleStyle"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical" />
    </FrameLayout>

    <FrameLayout
        android:id="@android:id/content"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foreground="?android:attr/windowContentOverlay"
        android:foregroundGravity="fill_horizontal|top" />
</LinearLayout>

c、在Activity中通过【setContentView所设置的布局文件】其实就是【被加到内容栏】之中的,成为其【唯一子View】,就是上面的【id为content的FrameLayout】中,在代码中可以【通过content来得到对应加载的布局】。

ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
ViewGroup rootView = (ViewGroup) content.getChildAt(0);

4、ViewRoot 连接器
a、所有【View的绘制】以及【事件分发】等交互都是通过【ViewRoot来执行或传递】的。

b、【ViewRoot】对应【ViewRootImpl类】。它的作用包括:
(1)连接WindowManager 和 DecorView
(2)完成View的绘制流程

c、即【ViewRoot可以与WMS交互通讯,调整窗口大小及分布】;可以【接收事件】并【向DecorView分发】,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的;

d、可以完成View的三大绘制流程:【测量、布局和绘制】。

 

5、Activity & PhoneWindow & DecorView & ViewRoot 联系

在这里插入图片描述

 

6、DecorView 的创建 & 显示(View 绘制前准备)
a、工作流程

在这里插入图片描述

b、源码分析
总结:
(1) DecroView的创建
a、Activity 启动时过程【attach()方法中】,系统创建Window抽象子类PhoneWindow类实例对象,并为PhoneWindow类对象设置WindowManager对象

mWindow = new PhoneWindow(this, window);
mWindow.setWindowManager(...);

b、Activity 调用onCreate中通过setContentView(resId)中在PhoneWindow中【创建一个DecroView类对象】(初始布局根据系统主体样式),并为DecroView中content增加Activity中设置的布局文件。

installDecor();
mLayoutInflater.inflate(layoutResID, mContentParent);

(2)DecroView 的显示

a、将DecroView对象添加到WindowManager

wm.addView(mDecor, getWindow().getAttributes());

b、【创建ViewRoot】,WindowManager将DecroView对象交给ViewRoot。ViewRootImpl对象【通过Handler】向主线程发送了一条【触发遍历操作的消息】:【performTraversals()】;该方法用于执行View的绘制流程【(measure、layout、draw)】。并将DecroView设置为可见。

root = new ViewRootImpl(view.getContext(), display);
performTraversals(); 
mDecor.setVisibility(View.VISIBLE);

c、DecroView 的创建
c.1、这部分内容主要讲DecorView是怎么一层层嵌套在Actvity,PhoneWindow中的,以及DecorView如何加载内部布局。

c.2、创建Window抽象类的子类PhoneWindow类的实例对象,为PhoneWindow类对象设置WindowManager对象

c.3、Activity启动过程(由【ActivityThread】 中的 【performLaunchActivity() 】来完成整个启动过程,在这个方法内部会通过【类加载器】创建 【Activity 的实例对象】,并调用其 【attach 方法】为其【关联】运行过程中所依赖的一系列【上下文环境】变量)
c.4、Activity 的 【Window 创建】就发生在 【attach 方法】里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口

final void attach(Context context, ActivityThread aThread,
    Instrumentation instr, IBinder token, int ident,
    Application application, Intent intent, ActivityInfo info,
    CharSequence title, Activity parent, String id,
    NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window) {
        ..................................................................
        //1. 创建一个PhoneWindow对象
        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        //2. 设置回调,向Activity分发点击或状态改变等事件
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        .................................................................
        //3. 给Window设置WindowManager对象
        mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ....................................................................
}

 

c5、为【PhoneWindow类对象】创建【1个DecroView类对象】,并为【DecroView类对象中的content】增加Activity中设置的【布局文件

/**
  * 源码分析:Activity的setContentView()
  */
    public void setContentView(int layoutResID) {
    	// getWindow() 作用:获得Activity 的成员变量mWindow
    	// Window类实例的setContentView() ->>分析1
	    getWindow().setContentView(layoutResID);
	    initWindowDecorActionBar();
	}

/**
  * 分析1:Window类实例的setContentView()
  */
  public void setContentView(int layoutResID) {

  		// 1. 若mContentParent为空,创建一个DecroView(在PhoneWindow中嵌套添加DecroView)
  		// mContentParent即为内容栏(content)对应的DecorView = FrameLayout子类
        if (mContentParent == null) {
            installDecor(); // ->>分析2
        } else {
        	// 若不为空,则删除其中的View
            mContentParent.removeAllViews();
        }

        // 2. 为mContentParent添加子View,即Activity中设置的布局文件
        mLayoutInflater.inflate(layoutResID, mContentParent);

        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();//回调通知,内容改变
        }
    }

 /**
  * 分析2:installDecor()
  * 作用:创建一个DecroView
  */
  private void installDecor() {

    if (mDecor == null) {
    	// 1. 生成DecorView
        mDecor = generateDecor();
        ...
    }}
    // 2. 为DecorView设置布局格式 & 返回mContentParent ->>分析3
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); 
        ...
        } 
    }
}

/**
  * 分析3:generateLayout(mDecor)
  * 作用:为DecorView设置布局格式
  */
  protected ViewGroup generateLayout(DecorView decor) {

        // 1. 从主题文件中获取样式信息
        TypedArray a = getWindowStyle();

        // 2. 根据主题样式,加载窗口布局
        int layoutResource;
        int features = getLocalFeatures();

        // 3. 加载layoutResource
        View in = mLayoutInflater.inflate(layoutResource, null);

        // 4. 往DecorView中添加子View
        // 即文章开头介绍DecorView时提到的布局格式,那只是一个例子,根据主题样式不同,加载不同的布局。(上:titleBar 下:content)
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
        mContentRoot = (ViewGroup) in;

        // 5. 这里获取的是mContentParent = 即为内容栏(content)对应的DecorView = FrameLayout子类
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 
      
        return contentParent;
    }

d、DecroView 的显示
d.1、以上仅仅是将DecorView建立起来。通过setContentView()设置的界面,为什么【在onResume()之后才对用户可见】呢?
(1)将DecroView对象添加到WindowManager
(2)创建ViewRoot,WindowManager将DecroView对象交给ViewRoot。
(3)ViewRootImpl对象通过Handler向主线程发送了一条触发遍历操作的消息:performTraversals();该方法用于执行View的绘制流程(measure、layout、draw)

d.2、这就要从ActivityThread开始说起。

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    //在这里首先调用Activity.attach(),接着调用了Activity.onCreate()和Activity.onStart()生命周期,
    //但是由于只是初始化了mDecor,添加了布局文件,还没有把mDecor添加到负责UI显示的PhoneWindow中,所以这时候对用户来说,是不可见的
    Activity a = performLaunchActivity(r, customIntent);

    ......

    if (a != null) {
    //这里面执行了Activity.onResume()
    handleResumeActivity(r.token, false, r.isForward,
                        !r.activity.mFinished && !r.startsNotResumed);

    if (!r.activity.mFinished && r.startsNotResumed) {
        try {
                r.activity.mCalled = false;
                //执行Activity.onPause()
                mInstrumentation.callActivityOnPause(r.activity);
                }
        }
    }
}

d.3、重点看下【handleResumeActivity()】,在这其中,【DecorView】将会【显示】出来,同时【重要的一个角色】:【ViewRoot也将登场】。

final void handleResumeActivity(IBinder token, boolean clearHide, 
                                boolean isForward, boolean reallyResume) {

    //这个时候,Activity.onResume()已经调用了,但是现在界面还是不可见的
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            //decor对用户不可见
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;

            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                //DecorView被添加进WindowManager了,但是这个时候,还是不可见的
                wm.addView(decor, l);
            }

            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                //在这里,执行了重要的操作,使得DecorView可见 ->> 分析1
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
        }
    }
}

/**
  * 分析1:Activity.makeVisible()
  * 当我们执行了Activity.makeVisible()方法之后,界面才对我们是可见的。
  */
  void makeVisible() {
   if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            // 1. 将DecorView添加到WindowManager ->>分析2
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        // 2. DecorView可见
        mDecor.setVisibility(View.VISIBLE);
    }
/**
  * 分析2:wm.addView
  * 作用:WindowManager = 1个接口,由WindowManagerImpl类实现
  */
  public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow); ->>分析3
    }
}

/**
  * 分析3:WindowManagerGlobal 的addView()
  */
  public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {

             final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

             ...

             synchronized (mLock) {

             // 1. 实例化一个ViewRootImpl对象
             ViewRootImpl root;
             root = new ViewRootImpl(view.getContext(), display);
             view.setLayoutParams(wparams);

             mViews.add(view);
             mRoots.add(root);
             mParams.add(wparams);
             }

             // 2. WindowManager将DecorView实例对象交给ViewRootImpl 绘制View
             try {
                  root.setView(view, wparams, panelParentView);
                  // ->> 分析4

                	}catch (RuntimeException e) {
               }

            }
 }


/**
  * 分析4:ViewRootImpl.setView()
  */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                requestLayout(); // ->>分析5
    }

/**
  * 分析5:ViewRootImpl.requestLayout()
  */
    @Override
    public void requestLayout() {

        if (!mHandlingLayoutInLayoutRequest) {
        	// 1. 检查是否在主线程
            checkThread();
            mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
            // 2. ->>分析6
            scheduleTraversals();
        }
    }

/**
  * 分析6:ViewRootImpl.scheduleTraversals()
  */
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            // 通过mHandler.post()发送一个runnable,在run()方法中去处理绘制流程
            // 与ActivityThread的Handler消息传递机制相似
            // ->>分析7
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ``````
        }
    }

/**
  * 分析7:Runnable类的子类对象mTraversalRunnable
  * 作用:在run()方法中去处理绘制流程
  */
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal(); // ->>分析8
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

  /**
    * 分析8:doTraversal()
    */
    void doTraversal() {
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals(); 
            // 最终会调用performTraversals(),从而开始View绘制的3大流程:Measure、Layout、Draw
    }
    // 注:
    //    a. 我们知道ViewRootImpl中W类是Binder的Native端,用于接收WmS处理操作
    //    b. 因W类的接收方法是在线程池中的,故可通过Handler将事件处理切换到主线程中

d.4、【ViewRootImpl对象】中【接收的各种变化】(如来自WmS的窗口属性变化、来自控件树的尺寸变化 & 重绘请求等都【引发performTraversals()的调用】 & 在其中完成处理。

d.5、而【View的绘制】则是在【performTraversals()】中执行,即View的绘制流程:measure、layout、draw

 

View绘制流程

a、【View的绘制流程】开始于:ViewRootImpl对象的performTraversals()

/**
  * 源码分析:ViewRootImpl.performTraversals()
  */
  private void performTraversals() {

  		// 1. 执行measure流程
        // 内部会调用performMeasure()
        measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);

        // 2. 执行layout流程
        performLayout(lp, mWidth, mHeight);

        // 3. 执行draw流程
        performDraw();
    }

b、从上面的performTraversals()可知:【View的绘制流程】从【顶级View(DecorView)】的【ViewGroup】开始,【一层一层从ViewGroup】至【子View遍历测绘】,采用递归实现

c、即:自上而下遍历、由父视图到子视图、【每一个 ViewGroup】 负责测绘它【所有的子视图】,而【最底层的 View】 会负责【测绘自身

在这里插入图片描述

绘制的流程 =【 measure过程】 + 【layout过程】 + 【draw过程】

1、measure —— 测量View的宽 / 高
(1)Android 尺寸值

a、ViewGroup.LayoutParams 布局参数:指定视图View 的高度(height) 和 宽度(width)等布局参数。
可通过以下参数指定

参数解释
具体值dp / px
match_parent强制性使子视图的大小扩展至与父视图大小相等(不含 padding )
wrap_content自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )

b、MeasureSpec 测量规格、描述
b.1、View大小的测量依据:测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size

b.2、计算方法:【子View】的【MeasureSpec值】根据【子View】的【布局参数】(LayoutParams)和【父容器】的【MeasureSpec值】计算得来的

/**
  * 源码分析: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);  
        }  

- 计算方法

规律总结:

在这里插入图片描述

 

(2)measure流程

在这里插入图片描述

单一View

ViewGroup
a、【遍历和测量】【所有子View】的尺寸
b、【合并】将所有子View的【尺寸】,最终得到【ViewGroup父视图】的【测量值
自上而下、一层层地传递下去,直到完成整个View树的measure()过程

(1)自定义ViewGroup
a、需要【复写onMeasure()】从而实现【自定义子View测量】逻辑

b、因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。因此,ViewGroup无法对onMeasure()作统一实现。

c、所以需要根据自身的测量逻辑复写onMeasure(),分为4步:

c.1、遍历所有子View:measureChildren()】
c.2、对子View 进行测量(根据父容器的measureSpec & 布局参数layoutParams)并【递归调用View.measure()】:【measureChild()】
c.3、合并所有子View的尺寸大小,最终得到【ViewGroup父视图的测量值】(自身实现)
c.4、【存储测量后View宽/高的值】:调用【setMeasuredDimension()】

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        // 定义存放测量后的View宽/高的变量
        int widthMeasure ;
        int heightMeasure ;

        // 1. 遍历所有子View & 测量(measureChildren())
        // ->> 分析1
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        // 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
         void measureCarson{
             ... // 根据布局方式自身实现
         }

        // 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
        setMeasuredDimension(widthMeasure,  heightMeasure);  
  }
  // 从上可看出:
  // 复写onMeasure()有三步,其中2步直接调用系统方法
  // 需自身实现的功能实际仅为步骤2:合并所有子View的尺寸大小

/**
  * 分析1:measureChildren()
  * 作用:遍历子View & 调用measureChild()进行下一步测量
  **/ 

	protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
		// 参数说明:父视图的测量规格(MeasureSpec)

		        final int size = mChildrenCount;
		        final View[] children = mChildren;

		        // 遍历所有子view
		        for (int i = 0; i < size; ++i) {
		            final View child = children[i];
		             // 调用measureChild()进行下一步的测量 ->>分析1
		            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
		                measureChild(child, widthMeasureSpec, heightMeasureSpec);
		            }
		        }
		    }

/**
  * 分析2:measureChild()
  * 作用:a. 计算单个子View的MeasureSpec
  *      b. 测量每个子View最后的宽 / 高:调用子View的measure()
  **/ 
  protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 1. 获取子视图的布局参数
        final LayoutParams lp = child.getLayoutParams();

        // 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        // 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
        // 下面的流程即类似单一View的过程,此处不作过多描述
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    // 回到调用原处

(2)实例:LinearLayout extends ViewGroup


  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

      // 根据不同的布局属性进行不同的计算
      // 此处只选垂直方向的测量过程,即measureVertical()->>分析1
      if (mOrientation == VERTICAL) {
          measureVertical(widthMeasureSpec, heightMeasureSpec);
      } else {
          measureHorizontal(widthMeasureSpec, heightMeasureSpec);
      }
}

  /**
    * 分析1:measureVertical()
    * 作用:测量LinearLayout垂直方向的测量尺寸
    **/ 
  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
      
      /**
       *  其余测量逻辑
       **/
          // 获取垂直方向上的子View个数
          final int count = getVirtualChildCount();

          // 遍历子View获取其高度,并记录下子View中最高的高度数值
          for (int i = 0; i < count; ++i) {
              final View child = getVirtualChildAt(i);

              // 子View不可见,直接跳过该View的measure过程,getChildrenSkipCount()返回值恒为0
              // 注:若view的可见属性设置为VIEW.INVISIBLE,还是会计算该view大小
              if (child.getVisibility() == View.GONE) {
                 i += getChildrenSkipCount(child, i);
                 continue;
              }

              // 记录子View是否有weight属性设置,用于后面判断是否需要二次measure
              totalWeight += lp.weight;

              if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                  // 如果LinearLayout的specMode为EXACTLY且子View设置了weight属性,在这里会跳过子View的measure过程
                  // 同时标记skippedMeasure属性为true,后面会根据该属性决定是否进行第二次measure
                // 若LinearLayout的子View设置了weight,会进行两次measure计算,比较耗时
                  // 这就是为什么LinearLayout的子View需要使用weight属性时候,最好替换成RelativeLayout布局
                
                  final int totalLength = mTotalLength;
                  mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                  skippedMeasure = true;
              } else {
                  int oldHeight = Integer.MIN_VALUE;
     /**
       *  步骤1:遍历所有子View & 测量:measureChildren()
       *  注:该方法内部,最终会调用measureChildren(),从而 遍历所有子View & 测量
       **/
            measureChildBeforeLayout(

                   child, i, widthMeasureSpec, 0, heightMeasureSpec,
                   totalWeight == 0 ? mTotalLength : 0);
                   ...
            }

      /**
       *  步骤2:合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(自身实现)
       **/        
              final int childHeight = child.getMeasuredHeight();

              // 1. mTotalLength用于存储LinearLayout在竖直方向的高度
              final int totalLength = mTotalLength;

              // 2. 每测量一个子View的高度, mTotalLength就会增加
              mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                     lp.bottomMargin + getNextLocationOffset(child));
      
              // 3. 记录LinearLayout占用的总高度
              // 即除了子View的高度,还有本身的padding属性值
              mTotalLength += mPaddingTop + mPaddingBottom;
              int heightSize = mTotalLength;

      /**
       *  步骤3:存储测量后View宽/高的值:调用setMeasuredDimension()  
       **/ 
       setMeasureDimension(resolveSizeAndState(maxWidth,width))

    ...
  }

2、layout —— 计算视图(View)的位置,即计算View的四个顶点位置:Left、Top、Right 和 Bottom

在这里插入图片描述

在这里插入图片描述

 

基础:

在这里插入图片描述

View的位置由4个顶点决定的(如下A、B、C、D),4个顶点的位置描述分别由4个值决定:
(请记住:View的位置是相对于父控件而言的
Top:子View上边界到父view上边界的距离
Left:子View左边界到父view左边界的距离
Bottom:子View下边距到父View上边界的距离
Right:子View右边界到父view左边界的距离

单一View

ViewGroup

a、计算自身ViewGroup在父布局的位置:layout()(= setFrame())

b、遍历ViewGroup的所有子View在ViewGroup的位置(调用子View 的 layout()):onLayout()
自上而下、一层层地传递下去,直到完成整个View树的layout()过程
(1)自定义ViewGroup
先计算自身在父容器中位置 setFrame(),再计算子View在父容器中相对位置onLayout():
必须重写onLayout()抽象方法,计算该ViewGroup包含所有的子View在父容器的位置。因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现。
根据自身逻辑复写布局方法onLayout():

c、循环遍历子View

d、计算当前子View相对于父容器(ViewGroup)的位置(根据具体布局)& 递归调用View.layout()

// 计算该ViewGroup包含所有的子View在父容器的位置()
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // 参数说明
     // changed 当前View的大小和位置改变了 
     // left 左部位置
     // top 顶部位置
     // right 右部位置
     // bottom 底部位置

     // 1. 遍历子View:循环所有子View
          for (int i=0; i<getChildCount(); i++) {
              View child = getChildAt(i);   

              // 2. 计算当前子View的四个位置值
                // 2.1 位置的计算逻辑
                ...// 需自己实现,也是自定义View的关键

                // 2.2 对计算后的位置值进行赋值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom

              // 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
              // 即确定了子View在父容器的位置(mLeft,mTop,mRight,mBottom)均是相对于父容器的位置
              child.layout(mLeft, mTop, mRight, mBottom);
              // 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
          }
      }
  }

(2)实例:LinearLayout extends ViewGroup

/**
  * 源码分析:LinearLayout复写的onLayout()
  * 注:复写的逻辑 和 LinearLayout measure过程的 onMeasure()类似
  */ 
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

      // 根据自身方向属性,而选择不同的处理方式
      if (mOrientation == VERTICAL) {
          layoutVertical(l, t, r, b);
      } else {
          layoutHorizontal(l, t, r, b);
      }
  }
      // 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程 ->>分析1

/**
  * 分析1:layoutVertical(l, t, r, b)
  */
    void layoutVertical(int left, int top, int right, int bottom) {
       
        // 子View的数量
        final int count = getVirtualChildCount();

        // 1. 遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {

                // 2. 计算子View的测量宽 / 高值(measure()过程中测量的结果)
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                // 3. 确定自身子View的位置
                // 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->>分析2
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);

                // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
                // 这符合垂直方向的LinearLayout的特性
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

/**
  * 分析2:setChildFrame()
  */
    private void setChildFrame( View child, int left, int top, int width, int height){
        // setChildFrame()仅仅只是调用了子View的layout()而已
        child.layout(left, top, left + width, top + height);
        }
    // 在子View的layout()又通过调用setFrame()确定View的四个顶点
    // 即确定了子View的位置
    // 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置

 

3、draw —— 绘制View视图

在这里插入图片描述

 

在这里插入图片描述

(1)单一View

/**
  * 源码分析:draw()
  * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。
  * 绘制过程:
  *   1. 绘制view背景
  *   2. 绘制view内容
  *   3. 绘制子View
  *   4. 绘制装饰(渐变框,滑动条等等)
  * 注:
  *    a. 在调用该方法之前必须要完成 layout 过程
  *    b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
  *    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
  *    d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
  */ 
  public void draw(Canvas canvas) {

    ...// 仅贴出关键代码
  
    int saveCount;

    // 步骤1: 绘制本身View背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

    // 若有必要,则保存图层(还有一个复原图层)
    // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
    // 因此在绘制时,节省 layer 可以提高绘制效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {

    // 步骤2:绘制本身View内容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默认为空实现,需复写
        // ViewGroup中:需复写

    // 步骤3:绘制子View
    // 由于单一View无子View,故View 中:默认为空实现
    // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
        dispatchDraw(canvas);
        
    // 步骤4:绘制装饰,如滑动条、前景色等等
        onDrawScrollBars(canvas);

        return;
    }
    ...    
}

(2)ViewGroup
a、dispatchDraw默认逻辑:

b、ViewGroup绘制自身(含背景、内容)

c、循环遍历子View

d、绘制子View(递归 调用View.draw() 绘制背景、内容、装饰)

e、绘制装饰(滚动指示器、滚动条、前景)

/**
  * 源码分析:dispatchDraw()
  * 作用:遍历子View & 绘制子View
  * 注:
  *   a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法
  *   b. View中默认为空实现(因为没有子View可以去绘制)
  */ 
    protected void dispatchDraw(Canvas canvas) {
        ......

         // 1. 遍历子View
        final int childrenCount = mChildrenCount;
        ......

        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                  // 2. 绘制子View视图 ->>分析1
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ....
        }
    }

/**
  * 分析1:drawChild()
  * 作用:绘制子View
  */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        // 最终还是调用了子 View 的 draw ()进行子View的绘制
        return child.draw(canvas, this, drawingTime);
    }

 

 

自定义View

1、注意点

a、支持特殊属性
(1)支持wrap_content
如果不在onMeasure()中对wrap_content作特殊处理,那么wrap_content属性将失效
(2)支持padding & margin
如果不支持,那么padding和margin(ViewGroup情况)的属性将失效
对于继承View的控件,padding是在draw()中处理
对于继承ViewGroup的控件,padding和margin会直接影响measure和layout过程

b、多线程直接使用post
View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。

c、避免内存泄露
主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。

d、处理好滑动冲突
当View带有滑动嵌套情况时,必须要处理好滑动冲突,否则会严重影响View的显示效果。
在onTouchEvent()中处理

2、基本步骤
(1)创建自定义View类,复写相关方法(集成View类,具体绘制需要复写onDraw)
(2)在布局文件中添加自定义View类的组件
(3)Activity中setContentView(resId)显示

3、案例
(1)RefreshListView
(2)含一键删除&自定义样式的SuperEditText
(3)简单好用的搜索框(含历史搜索记录)
(4)时间轴
(5)一个可爱 & 小资风格的Android加载等待自定义View

 

View刷新/重绘机制

 

1、时机

a、视图本身内部状态发生变化,比如显示属性由GONE到VISIBLE;

b、ViewGroup中添加或删除了视图导致需要重新为子视图分配位置

c、视图本身的大小发生变化,比如TextView中的文本内容变多变少了

2、方法

a、requestLayout
requestLayout()用于重新布局,该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImplperformTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法重新设置位置,不一定 会触发onDraw()方法。

b、invalidate() & postInvalidate()
invalidate()和postInvalidate()均用于View的重绘。该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestedefalse不会 触发onMesaure()与onLayout()方法,会触发onDraw()方法

c、invalidate()是在UI线程中使用,必须配合handler使用;postInvalidate可以在非UI线程中使用,不用使用handler。

d、invalidate主要给需要重绘的视图添加DIRTY标记,并通过不断回溯父视图做矩形运算求得真正需要绘制的区域,并最终保存在ViewRoot中的mDirty变量中,最后调用scheduleTraversals发起重绘请求,scheduleTraversals会发送一个异步消息,最终调用performTraversals()执行重绘(performTraversals()遍历所有相关联的 View ,触发它们的 onDraw 方法进行绘制)

e、postInvalidate只是实现了一个消息机制,让用户能够在非UI线程使用,最终还是调用到invalidate()方法来触发重画,实现界面更新动作。

3、流程

a、View的【界面刷新有三种方法invalidate(请求重绘)、requestLayout(重新布局)、requestFocus(请求焦点)

b、View界面刷新的所有方法均会递归调用父容器的相关方法,从View树向上层层找到最顶层的DecorView,通过DecorView的mParent,即ViewRootImpl执行scheduleTraversals()方法进行界面绘制。

c、调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。

d、当VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程的消息队列中。

e、主线程从消息队列中取出并调用performTraversals()执行三大流程: onMeasure()-onLayout()-onDraw()

 

 

View的事件分发机制

Touch事件的传递 & 拦截机制

 

1、事件分发 简介

a、本质
由于Android的View是树形结构,多个View会重叠在一起,View事件分发的本质就是解决将点击事情(Touch)产生的MotionEvent对象传递到哪一个具体的View然后消耗处理这个事件的整个过程。

b、分发对象
Android事件分发顺序:Activity(Window) -> ViewGroup(容纳UI组件的容器,一组View的集合,如DecorView、Layout等) -> View(所有UI的基类)

c、传递对象
事件(MotionEvent)
当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)。Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象
主要发生的Touch事件有如下四种:
MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
MotionEvent.ACTION_MOVE:滑动View
MotionEvent.ACTION_UP:抬起View(与DOWN对应)

d、事件分发对应方法

在这里插入图片描述

 

2、事件分发 流程

在这里插入图片描述

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View。
(1)Activity 事件分发
即先将事件传递给Activity,Activity再传递给Window,最后Window再传递给DecorView,DecorView接收到事件后,就会按照事件分发机制去分发事件。即调用调用ViewGroup的dispatchTouchEvent。


(2)ViewGroup 事件分发
此时顶级ViewGroup的dispatchTouchEvent就会被调用,这个方法用于事件分发。如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用来消耗事件并返回true;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素View。


(3)View 事件分发
接着子元素的dispatchTouchEvent方法就会被调用,如果子元素是View,则它不会拦截事件,要么将事件消费,要么不处理直接回传。事件会按层级依此回传,最终会告诉Activity.dispatchTouchEvent。

(4)在某个View拦截触摸事件

a、设置View 的 <View android:clickable = “false” 不可点击 android:focusable = “false” 无法获取焦点 android:focusableInTouchMode = “false” 不可通过触摸获取焦点> 即使当前View不可获取点击事件,此时将事件回传给上一级父组件处理

b、设置View 的 onTouchEvent 返回值为false

c、设置View 的 父组件ViewGroup 的 onInterceptTouchEvent / dispatchTouchEvent 返回值为 true

 

dispatchTouchEvent 代码描述

// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {

    //代表是否消耗事件
    boolean consume = false;


    if (onInterceptTouchEvent(ev)) {
    //如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
    //则该点击事件则会交给当前View进行处理
    //即调用onTouchEvent ()方法去处理点击事件
      consume = onTouchEvent (ev) ;

    } else {
      //如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
      //则该点击事件则会继续传递给它的子元素
      //子元素的dispatchTouchEvent()就会被调用,重复上述过程
      //直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }

    return consume;

 

3、事件分发 场景

在这里插入图片描述

原理分析:
类似侧滑菜单中若为一个列表,则对侧滑菜单SlideView的左右滑动事件可能会被列表的子元素ListViewItem消费,从而使左右滑动菜单显示/隐藏菜单功能失效
解决:
侧滑菜单组件的onInterceptTouchEvent方法进行重写,滑动时获取x,y方向上的偏移值。若x方向上的偏移值>y方向上的偏移值 & x方向偏移值大于一个阈值,则返回true拦截此次触摸事件,交给侧滑菜单处理(调用侧滑菜单SlideView的滑动事件onScroll),否则交给子元素处理(ListViewItem的onClick)

// 复写onInterceptEventTouch方法进行拦截处理
public class SlideView extends View{
	...
	public final int MIN_OFFSET_X = 5;
	// 拦截处理
	@override
	public boolean onInterceptTouchEvent(MotionEvent ev){
		switch(ev.getAction()){
			case MotionEvent.ACTION_DOWN:
				downX = ev.getX();	// 触摸初始X值
				downY = ev.getY();	// 触摸初始Y值
			break;
			case MotionEvent.ACTION_MOVE:
				offsetX = Math.abs(ev.getX() - downX);	// 手指在X方向偏移距离
				offsetY = Math.abs(ev.getY() - downY);	// 手指在Y方向偏移距离
				
				if(offsetX > offsetY && offsetX > MIN_OFFSET_X){
					// 如果X方向偏移值>Y方向偏移值,且X偏移值大于阈值,则该触摸事件为滑动侧滑菜单
					// 拦截此次触摸事件,交给滑动菜单,进行菜单滑动
					return true;
				}
		}
		return super.onInterceptTouchEvent(ev);
	}
}

 

事件分发中的onTouch、onTouchEvent (和onClick) 有什么区别,又该如何使用?

这两个方法都在View.dispatchTouchEvent()中调用。
onTouch是View的onTouchListener中的方法。需要实现onTouchListener并且点击的View为enable时,View有touch事件便会调用。
onTouchEvent是复写的方法。屏幕有touch事件便会调用。
它们的区别在于
(1)onTouch优先级比onTouchEvent优先级高。当onTouch返回值为true,则表示事件已经被消费,便不会向onTouchEvent传递,也不会调用onClick(因为onClick是在onTouchEvent中执行的,onTouchEvent中performClick是onClick的入口方法)。只有当onTouch()的返回值为false。才会调用onTouchEvent()。
所以【优先级】为【onTouch > onTouchEvent > onClick
(2)【为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?】
滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(控件内置事件如滚动事件onScroll与点击事件onClick等等均基于onTouchEvent,优先级小于onTouch),因此解决办法就是在onTouch方法里返回false

 

动画

种类 & 特点 & 区别 &原理

 视图动画属性动画
类型补间动画逐帧动画属性动画
作用对象视图控件(View);如Android的TextView、Button等
不可作用于View组件的属性】,如:颜色、背景等
任意Java对象
不仅局限于视图View对象
原理通过确定开始的视图样式 & 结束的视图样式,中间动画变化过程由系统补全来确定一个动画将【动画拆分为帧】的形式,且定义【每一帧均是一张图片】,按顺序播放一组预先定义好的图片在【一定时间间隔】内,通过【不断对值进行更改】,并【不断传值给对象的属性】,从而实现对象在该属性上的动画效果
特点作用对象局限:View & 只能改变View的视觉效果而无法改变View的属性 & 动画效果单一
适合视图简单、基本的动画效果(如Activity、Fragment的切换效果,或视图组(ViewGroup)中子元素出厂效果)
作用对象扩展:面向属性,作用对象可以是任何一个Object对象 & 实际改变视图的属性 & 动画效果丰富:包括四种基本变化意外的其他动画效果
适合与属性相关,更为复杂的动画效果
使用四种基本变换类型:
平移动画(Translate)
缩放动画(Scale)
旋转动画(Rotate)
透明度动画(Alpha)
使用时避免使用尺寸大的图片,否则会引起OOM主要使用 【ValueAnimator & ObjectAnimator
区别是否改变动画本身的属性
视图动画仅仅对图像进行变化,视图的位置、相应区域等均在远地;而属性动画是通过过动态改变对象的属性从而达到动画效果

使用

补间动画
Android 补间动画:手把手教你使用 补间动画
(1)在 res/anim的文件夹里创建动画效果.xml文件
创建地址为:res/anim/view_animation.xml

(2)根据 不同动画效果(平移、缩放、旋转、透明度)的语法 设置 不同动画参数,从而实现动画效果

动画类型标签方法
公用/android:duration:动画持续时间
android:startOffset:动画延迟开始时间
android:repeatCount:动画重放次数
android:interpolator:插值器
平移< translate/ >android:fromXDelta:视图在水平方向x 移动的起始值
android:toXDelta:视图在水平方向x 移动的结束值
android:fromYDelta:视图在竖直方向y 移动的起始值
android:toYDelta:视图在竖直方向y 移动的结束值
缩放< scale/ >android:fromXScale:动画在水平方向X的起始缩放倍数
android:toXScale:动画在水平方向X的结束缩放倍数
android:fromYScale=“0.0”:动画开始前在竖直方向Y的起始缩放倍数
android:toYScale:动画在竖直方向Y的结束缩放倍数
android:pivotX:缩放轴点的x坐标
android:pivotY:缩放轴点的y坐标
旋转< rotate/ >android:fromDegrees=“0.0”:动画开始时 视图的旋转角度
android:toDegrees:动画结束时 视图的旋转角度
android:pivotX:旋转轴点的x坐标
android:pivotY:旋转轴点的y坐标
透明度< alpha/ >android:fromAlpha=“0.0”:动画开始时 视图的透明度
android:toAlpha:动画结束时 视图的透明度
组合< set/ >android:shareinterpolator:表示组合动画中的动画是否和集合共享同一个差值器

(3)在Java代码中创建Animation对象并播放动画

		Button mButton = (Button) findViewById(R.id.Button);
        // 步骤1:创建 需要设置动画的 视图View
        Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
        // 步骤2:创建 动画对象 并传入设置的动画效果xml文件
        mButton.startAnimation(translateAnimation);
        // 步骤3:播放动画

逐帧动画
Android 逐帧动画:关于 逐帧动画 的使用都在这里了!
(1)将动画资源(即每张图片资源)放到 drawable文件夹里
(2)从drawable文件夹获取动画资源 & 载入并启动动画


public class FrameActivity extends AppCompatActivity {
    private Button btn_startFrame,btn_stopFrame;
    private ImageView iv;
    private AnimationDrawable animationDrawable;

        iv = (ImageView) findViewById(R.id.iv);
        btn_startFrame = (Button) findViewById(R.id.btn_startFrame);
        btn_stopFrame = (Button) findViewById(R.id.btn_stopFrame);


        <-- 开始动画 -->
        btn_startFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                iv.setImageResource(R.drawable.knight_attack);
                // 1. 设置动画
                animationDrawable = (AnimationDrawable) iv.getDrawable();
                // 2. 获取动画对象
                animationDrawable.start();
                // 3. 启动动画
            }
        });
        //停止动画
        btn_stopFrame.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
                iv.setImageResource(R.drawable.knight_attack);
                // 1. 设置动画
                animationDrawable = (AnimationDrawable) iv.getDrawable();
                // 2. 获取动画对象
                animationDrawable.stop();
                // 3. 暂停动画
            }
        });

    }
}

属性动画
Android 属性动画:这是一篇很详细的 属性动画 总结&攻略

在这里插入图片描述

 

源码

Android 动画原理分析

问题

a、OOM:使用【逐帧动画】时避免使用【尺寸大的图片】,否则会引起OOM。

b、内存泄露:当我们把动画的repeatCount设置为无限循环时,如果在Activity退出时没有及时将动画停止,【属性动画】会导致Activity无法释放而导致【内存泄漏】,而【补间动画】却没有【内存泄漏】问题。因此,使用属性动画时切记在Activity执行 onStop 方法时顺便将动画停止。

c、在使用ValueAnimator或者ObjectAnimator时(ObjectAnimator继承ValueAnimator),如果没有及时做cancel取消动画,就可能造成内存泄露。ValueAnimator 有个AnimationHandler的单例,会持有属性动画对象自身的引用,属性动画对象持有view的引用,view持有activity引用,所以导致的内存泄露。
分析:补间动画和属性动画内存泄露

 

估值器

a、插值器用于设置属性值从初始值过渡到结束值变化规律的一个接口。用于实现非线性运动,如匀速、加速、减速的动画效果。

b、估值器用于设置属性值从初始值过渡到结束值的变化具体数值的一个接口。用于决定值的变化规律,如匀速、加速、减速的变化趋势。用于辅助插值器实现非线性运动。

 

 

ListView & RecycleView

ListView 定义 & 原理 & 优化 & 封装?

1、ListView & Adapter
a、列表 ListView 是 Android中的一种列表视图组件,继承自【AdapterView抽象类】。

b、【适配器 Adapter】 作为 【View】 和 【数据】 之间的【桥梁&中介】,将数据映射到列表要展示的View中。

c、ListView 仅作为容器(列表),用于【装载 & 显示数据】(即 列表项Item),而容器内的具体每一项的内容(列表项Item)则是由 【适配器(Adapter)提供】。

在这里插入图片描述

2、RecycleBin 缓存原理

在这里插入图片描述

a、为了节省空间和时间,ListView不会为每一个数据创建一个视图,而是采用了【RecycleBin】(Recycler组件),用于【回收 & 复用 View】。

b、当屏幕需显示x个Item时,那么ListView会创建 x+1个视图。移出屏幕的View控件会缓存到RecycleBin当中,当有View进入屏幕后,ListView会从RecycleBin里面取出一个缓存View控件,将其作为convertView参数传递到Adapter的getView中,从而达到View的复用,不必每次都加载布局(LayoutInflater.inflate())

 

3、ListView 优化

a、getView() 优化、convertView优化
主要优化加载布局的问题——减少getView方法每次调用LayoutInflater.inflate()方法

public View getView(int position, View convertView, ViewGroup parent){
	View view;
	if(convertView == null){
		// 没有缓存就加载布局
		view = LayoutInfalter.from(getContext()).inflate(resourceID,null);
	}
	else{
		// 有缓存直接使用缓存的convertView
		view = convertView;
	}
}

b、viewHolder优化(Google推荐ListView优化方案)
主要优化加载控件问题——减少getView方法每次调用findViewById()方法

public View getView(int position, View convertView, ViewGroup parent) {
     Log.d("MyAdapter", "Position:" + position + "---"
             + String.valueOf(System.currentTimeMillis()));
     ViewHolder holder;
     if (convertView == null) {
     // convertView为空时,viewHolder会将控件的实例放在ViewHolder中,然后用setTag方法将ViewHolder对象存储在View中
         final LayoutInflater inflater = (LayoutInflater) mContext
                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         convertView = inflater.inflate(R.layout.list_item_icon_text, null);
         holder = new ViewHolder();
         holder.icon = (ImageView) convertView.findViewById(R.id.icon);
         holder.text = (TextView) convertView.findViewById(R.id.text);
         convertView.setTag(holder);
     } else {
     // convertView不为空时,用getTag方法从View获取viewHolder对象
         holder = (ViewHolder) convertView.getTag();
     }
     holder.icon.setImageResource(R.drawable.icon);
     holder.text.setText(mData[position]);
     return convertView;
 }
  
 static class ViewHolder {
     ImageView icon;
     TextView text;
}

c、图片错乱
图片错乱:ContentView复用 + 异步加载网络图片

public View getView(int position, View convertView, ViewGroup parent) {
		String url = urlList.get(position);
		ViewHolder holder;
		// 1. 如果有可以复用的View ,则使用复用的View
        if (convertView == null) {
            view = inflater.inflate(R.layout.item, null);
            holder = new ViewHolder();
            holder.image = (ImageView) view.findViewById(R.id.image);
            view.setTag(holder);
        }else{
			view = convertView;
			holder = (ViewHolder)view.getTag();
		}
        // 2. downloadBitmapFromNet开启多线程(如 AsyncTask)异步加载网络图片
        BitmapDrawable drawable = downloadBitmapFromNet(url);
        // 3. 若此时该View已经移出屏幕,新的View进入屏幕,并复用这块image
        // 此时的drawable因为异步耗时操作刚刚取到网络图片
        // 则会在该View上显示错误的图片,从而造成图片乱序
        image.setImageDrawable(drawable);
        return view;
    }
    
public class ViewHolder{
	ImageView image;
}

d、假设屏幕上有7个条目,向上滑动。新的第8个条目进入界面就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片。由于网络操作耗时,刚进入的条目在图片下载完前会显示缓存中ImageView的图片(即第1个条目的图片),等到下载结束会变回网络图片。(因为第1个图片与第8个图片指向同一块ImageView实例)此时,若ListView快速滑动,移出屏幕的条目被进入的条目重新利用,若此时移出的条目发起的图片请求有了响应。则会造成不同位置显示图片错乱的现象。(显示第15个图片时,第8个图片得到响应,此时的image为第15个图片所复用,但显示的确是第8个图片)

e、解决方案通过对ImageView设置tag(通常用图片的url)防止图片错位
每次getView时(新的元素进入屏幕),对ImageView设置标签。当网络加载结束后,查询当前ImageView的标签,如果更改了,说明该ImageView被新的元素复用(因为移出屏幕的旧元素和进入屏幕的新元素指向的是同一块ImageView实例),则不显示加载的网络图片;否则仍为原来图片元素,显示加载的网络图片。

f、最优化方案的完整实现方案
(1)定义主xml布局:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:background="#FFFFFF"
    android:orientation="vertical" >
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

(2)根据需要,定义ListView每行所实现的xml布局(item布局):item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" 
android:layout_height="match_parent">
    <ImageView
        android:layout_alignParentRight="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ItemImage"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮"
        android:id="@+id/ItemBottom"
        android:focusable="false"
        android:layout_toLeftOf="@+id/ItemImage" />
    <TextView android:id="@+id/ItemTitle"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:textSize="20sp"/>
    <TextView android:id="@+id/ItemText"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:layout_below="@+id/ItemTitle"/>
</RelativeLayout>

(3)定义一个Adapter类继承BaseAdapter,重写里面的方法:MyAdapter.java

class MyAdapter extends BaseAdapter {
    private LayoutInflater mInflater;//得到一个LayoutInfalter对象用来导入布局 
    ArrayList<HashMap<String, Object>> listItem;

    public MyAdapter(Context context,ArrayList<HashMap<String, Object>> listItem) {
        this.mInflater = LayoutInflater.from(context);
        this.listItem = listItem;
    }//声明构造函数

    @Override
    public int getCount() {
        return listItem.size();
    }//这个方法返回了在适配器中所代表的数据集合的条目数

    @Override
    public Object getItem(int position) {
        return listItem.get(position);
    }//这个方法返回了数据集合中与指定索引position对应的数据项

    @Override
    public long getItemId(int position) {
        return position;
    }//这个方法返回了在列表中与指定索引对应的行id

//利用convertView+ViewHolder来重写getView()
    static class ViewHolder
    {
        public ImageView img;
        public TextView title;
        public TextView text;
        public Button btn;
    }//声明一个外部静态类
    @Override
    public View getView(final int position, View convertView, final ViewGroup parent) {
        ViewHolder holder ;
        if(convertView == null)
        {
            holder = new ViewHolder();
            convertView = mInflater.inflate(R.layout.item, parent, false);
            holder.img = (ImageView)convertView.findViewById(R.id.ItemImage);
            holder.title = (TextView)convertView.findViewById(R.id.ItemTitle);
            holder.text = (TextView)convertView.findViewById(R.id.ItemText);
            holder.btn = (Button) convertView.findViewById(R.id.ItemBottom);
            convertView.setTag(holder);
        }
        else {
            holder = (ViewHolder)convertView.getTag();

        }
        holder.img.setImageResource((Integer) listItem.get(position).get("ItemImage"));
        holder.title.setText((String) listItem.get(position).get("ItemTitle"));
        holder.text.setText((String) listItem.get(position).get("ItemText"));
        holder.btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("你点击了选项"+position);//bottom会覆盖item的焦点,所以要在xml里面配置android:focusable="false"
            }
        });

        return convertView;
    }//这个方法返回了指定索引对应的数据项的视图
}

(4)在MainActivity中构造Adapter对象,设置适配器,将ListView绑定到适配器上:MainActivity.java

public class MainActivity extends AppCompatActivity {
    private ListView lv;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        lv = (ListView) findViewById(R.id.listView1);
        /*定义一个以HashMap为内容的动态数组*/
        ArrayList<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();/*在数组中存放数据*/
        for (int i = 0; i < 100; i++) {
            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("ItemImage", R.mipmap.ic_launcher);//加入图片
            map.put("ItemTitle", "第" + i + "行");
            map.put("ItemText", "这是第" + i + "行");
            listItem.add(map);
        }
        MyAdapter adapter = new MyAdapter(this, listItem);
        lv.setAdapter(adapter);//为ListView绑定适配器

        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
                System.out.println("你点击了第" + arg2 + "行");//设置系统输出点击的行
            }
        });

}
}

 

ListView 性能优化

Bitmap优化
(1)用软引用存储图片信息
(2)图片压缩
(3)三级缓存

内存优化
(1)避免内存泄露,如使用Adapter传入context时注意context的生命周期(getApplicationContext)
(2)通过对View的复用减少内存
(3)分页机制

 

ListView 封装 —— 实现下拉刷新,上拉加载的具有分页机制的ListView
黑马视频:RefreshListView —— 下拉刷新 & 上拉加载
a、设计思路:

(1)初始化头布局,动画:自定义头布局,初始隐藏头布局(mHeaderView.setTopPadding(-measuredHeight))
(2)处理触摸事件,根据下滑偏移量的大小设置不同状态,并根据状态进行处理(修改头布局、数据请求等):

b、ACTION_MOVE && 列表头显示第一条数据(getFirstVisiblePosition == 0):
(a)if(offset < measuredHeight && currentState != PULL_TO_REFRESH) :不完全显示 => 下拉刷新,修改头布局
(b)if(offset >= measuredHeight && currentState != RELEASE_REFRESH) :完全显示 => 释放刷新,修改头布局

c、ACTION_DOWN
(1)if(currentState == RELEASE_REFRESH)
正在刷新,修改头布局,调用接口方法请求数据
(2)if(currentState == PULL_TO_REFRESH)
恢复头布局
(3)设置监听器,监听列表中数据变化:

d、控件创建监听器回调接口,并调用接口方法

e、用户实现接口方法,监听刷新事件,进行网络请求

 

ListView封装

public class RefreshListView extends ListView implements AbsListView.OnScrollListener {

    private View mHeaderView;           // 头布局
    private ImageView mArrowView;       // 箭头视图
    private TextView mTitleText;        // 标题视图
    private ProgressBar pb;             // 进度条
    // 头布局实现下拉刷新
    private int paddingTop;             // 头部局的内边距(状态切换的依据)
    int headerViewMeasureHeight;        // 头布局的高度
    private float downY;                // 按下时的y坐标
    private float moveY;                // 移动时的y坐标

    private int currentState = 0;       // 当前刷新模式,初始为下拉刷新模式
    // 定义默认刷新模式
    public static final int PULL_TO_REFRESH = 0;    // 下拉刷新模式
    public static final int RELEASE_REFRESH = 1;    // 释放刷新模式
    public static final int REFRESHING = 2;         // 正在刷新模式

    RotateAnimation rotateUpAnim;       // 向上旋转动画
    RotateAnimation rotateDownAnim;     // 向下旋转动画

    private View mFooterView;           // 脚布局
    private TextView mFooterText;        // 标题视图
    // 脚布局实现上拉加载
    int footerViewMeasureHeight;        // 脚布局的高度
    boolean isLoadingMore = false;      // 正在加载状态,初始为false

    OnRefreshListener mListener;         // 列表数据监听

    public RefreshListView(Context context) {
        super(context);
        init();
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    // 初始化头布局,脚布局,动画
    // 滚动监听
    public void init(){
        initHeadView();
        initFooterView();
        initAnimation();
        setOnScrollListener(this);
    }

    public void initHeadView(){
        // 1. 添加自定义头部局
        // layout_header_list 为自定义头部布局文件
        mHeaderView = View.inflate(getContext(), R.layout.layout_header_list,null);
        mArrowView = mHeaderView.findViewById(R.id.iv_arrow);
        mTitleText = (TextView)mHeaderView.findViewById(R.id.tv_title);
        pb = (ProgressBar) mHeaderView.findViewById(R.id.pb);
        // 2. 默认隐藏头部局
        // 设置内边距,可以隐藏当前控件:paddingTop = -自身高度
        mHeaderView.measure(0,0);   // 按照设置的规则测量高度
        // int headerViewHeight = mHeaderView.getHeight(); // 控件显示在界面上高度
        headerViewMeasureHeight = mHeaderView.getMeasuredHeight();    // 获得测量得到控件真实高度
        mHeaderView.setPadding(0,-headerViewMeasureHeight,0,0);
        // ListView.addHeadView(API)
        addHeaderView(mHeaderView);
    }

    public void initFooterView(){
        // 7. 创建自定义脚布局
        mFooterView = View.inflate(getContext(), R.layout.layout_footer_list,null);
        mFooterText = mFooterView.findViewById(R.id.tv_footer);
        mFooterView.measure(0,0);
        footerViewMeasureHeight = mFooterView.getMeasuredHeight();
        // 隐藏脚布局
        mFooterView.setPadding(0,-footerViewMeasureHeight,0,0);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev){
        // 3. 处理触摸事件,ListView下拉时,修改PaddingTop显示头部布局
        // 判断滑动距离,给Header设置paddingTop
        switch(ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                downY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                moveY = ev.getY();
                float offset = moveY - downY;     // 向下移动的偏移量

                // 处理:如果处于正在刷新的状态,则不处理头布局更新事件,调用父类方法(头布局不变,但仍可以滚动列表)
                if(currentState == REFRESHING)
                    return super.onTouchEvent(ev);

                // 显示头布局,则paddingTop = -自身高度 + 移动的偏移量
                // 只有偏移量 > 0 && 当前第一个可见条目的索引是0时,才下拉显示头部
                // ListView.getFirstVisiblePosition 返回值是当前可以看到的第一个item,在所有item中(包括看不到的)的位置
                if(offset >0 && getFirstVisiblePosition() == 0) {
                    paddingTop = (int) (-headerViewMeasureHeight + offset);
                    mHeaderView.setPadding(0, paddingTop, 0, 0);
                    if(paddingTop >= 0 && currentState != RELEASE_REFRESH){
                        // 完全显示 => 切换成释放刷新模式
                        currentState = RELEASE_REFRESH;
                        updateHeader();
                    }
                    else if(paddingTop < 0 && currentState != PULL_TO_REFRESH){
                        // 不完全显示 => 切换成下拉刷新模式
                        currentState = PULL_TO_REFRESH;
                        updateHeader();
                    }
                    return true;    // 当前事件被消费
                }
                break;
            case MotionEvent.ACTION_UP:
                // 5. 松手之后根据当前的paddingTop决定是否执行刷新
                if(currentState == PULL_TO_REFRESH){
                    // paddingTop < 0 不完全显示,不刷新,恢复初始状态 => 隐藏头布局
                    mHeaderView.setPadding(0,-headerViewMeasureHeight,0,0);
                }else if (currentState == RELEASE_REFRESH){
                    // paddingTop >= 0 完全显示,切换状态为正在刷新
                    mHeaderView.setPadding(0,0,0,0);
                    currentState = REFRESHING;
                    updateHeader();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

    // 初始化动画
    public void initAnimation(){
        // 向上转,围绕自身中心,逆时针180度 0 -> -180
        rotateUpAnim = new RotateAnimation(0f,-180f,
                Animation.RELATIVE_TO_SELF,0.5f,
                Animation.RELATIVE_TO_SELF,0.5f);
        rotateUpAnim.setDuration(300);
        rotateUpAnim.setFillAfter(true);    // 动画停留在结束位置
        // 向下转,围绕自身中心,逆时针180度 -180 -> -360
        rotateDownAnim = new RotateAnimation(-180f,-360f,
                Animation.RELATIVE_TO_SELF,0.5f,
                Animation.RELATIVE_TO_SELF,0.5f);
        rotateDownAnim.setDuration(300);
        rotateDownAnim.setFillAfter(true);    // 动画停留在结束位置
    }


    // 4. 根据状态更新头布局内容
    public void updateHeader(){
        switch (currentState){
            case PULL_TO_REFRESH:
                // 切换为下拉刷新,执行动画 + 修改标题
                mArrowView.startAnimation(rotateDownAnim);
                mArrowView.setVisibility(View.VISIBLE);
                pb.setVisibility(View.INVISIBLE);
                mTitleText.setText("下拉刷新");
                break;
            case RELEASE_REFRESH:
                // 切换为释放刷新,执行动画 + 修改标题
                mArrowView.startAnimation(rotateUpAnim);
                mArrowView.setVisibility(View.VISIBLE);
                pb.setVisibility(View.INVISIBLE);
                mTitleText.setText("释放刷新");
                break;
            case REFRESHING:
                // 切换为正在刷新,暂停动画 + 修改标题
                mArrowView.clearAnimation();
                mArrowView.setVisibility(View.INVISIBLE);
                pb.setVisibility(View.VISIBLE);
                mTitleText.setText("正在刷新中……");
                // 6. 设置监听器,(通过回调函数)通知用户执行网络操作刷新数据
                if(mListener != null)
                    mListener.onRefresh();
                break;
        }
    }

    public void onRefreshComplete(){
        // 用户通知刷新结束,恢复界面
        currentState = PULL_TO_REFRESH;
        updateHeader();
    }

    public interface OnRefreshListener{
        // 安卓面向接口编程,使用回调函数
        void onRefresh();   // 通知用户刷新
        void onLoadMore();  // 通知用户加载更多
    }

    public void setRefreshListener(OnRefreshListener listener){
        // 为列表设置监听器,监听数据的变化
        this.mListener = listener;
    }

    public void onLoadMoreComplete(){
        // 用户通知加载结束,恢复界面
        mFooterView.setPadding(0,-footerViewMeasureHeight,0,0);
        isLoadingMore = false;
    }

    // 8. 滚动监听
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        // 状态更新
//        public static int SCROLL_STATE_IDLE = 0; // 空闲
//        public static int SCROLL_STATE_TOUCH_SCROLL = 1;  // 触摸滑动
//        public static int SCROLL_STATE_FLING = 2; // 滑翔
        // 最新状态是空间状态 && 当前界面显示的条目最后一项是最后一条数据 => 加载更多
        // 脚布局恢复
        if(isLoadingMore){
            // 正在加载更多,避免重复加载更多数据
            return;
        }

        if(scrollState == SCROLL_STATE_IDLE && getLastVisiblePosition() >= (getCount() - 1)){
            isLoadingMore = true;
            mFooterView.setPadding(0,0,0,0);
            setSelection(getCount());   // 自动跳到最后一条数据
            if(mListener!=null)mListener.onLoadMore();  // 通知用户加载数据
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        // 滑动过程
    }
}

MainActivity调用

public class MainActivity extends AppCompatActivity {

    RefreshListView listView;
    MyAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (RefreshListView) findViewById(R.id.listview);
        adapter = new MyAdapter();
        listView.setAdapter(adapter);
        listView.setRefreshListener(new RefreshListView.OnRefreshListener(){
            @Override
            public void onRefresh() {
                // 访问网络获取数据
                refreshDataFromNet();
                // 获取结束,通知listView,调用onRefreshComplete
                adapter.notifyDataSetChanged();
                listView.onRefreshComplete();
            }

            @Override
            public void onLoadMore() {
                // 访问网络获取数据
                loadDataFromNet();
                // 获取结束,通知listView,调用onRefreshComplete
                adapter.notifyDataSetChanged();
                listView.onLoadMoreComplete();
            }
        });
    }
}

 

RecycleView 对比 & 应用

1、简介
用于代替ListView的滑动组件。相对于ListView功能更强大、支持定制样式更丰富、扩展性更高。

2、特点

在这里插入图片描述

mRecyclerView = findView(R.id.id_recyclerview);
//设置布局管理器
mRecyclerView.setLayoutManager(layout);
//设置adapter
mRecyclerView.setAdapter(adapter)
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(
                getActivity(), DividerItemDecoration.HORIZONTAL_LIST));

3、应用
RecyclerView 展示多种类型Item数据
(1)定义每个条目的bean

public class Goods {
    private String goodsName;
    private String goodsImg;
    private String goodsDescription;
    private Integer goodsType;  //1表示我的商品0表示别人的商品
    private String publisherName;

    public Goods(String goodsName, String goodsImg, String goodsDescription, Integer goodsType,String publisherName) {
        this.goodsName = goodsName;
        this.goodsImg = goodsImg;
        this.goodsDescription = goodsDescription;
        this.goodsType = goodsType;
        this.publisherName = publisherName;
    }

    public String getGoodsName() {
        return goodsName;
    }

    public void setGoodsName(String goodsName) {
        this.goodsName = goodsName;
    }

    public String getGoodsImg() {
        return goodsImg;
    }

    public void setGoodsImg(String goodsImg) {
        this.goodsImg = goodsImg;
    }

    public String getGoodsDescription() {
        return goodsDescription;
    }

    public void setGoodsDescription(String goodsDescription) {
        this.goodsDescription = goodsDescription;
    }

    public Integer getGoodsType() {
        return goodsType;
    }

    public void setGoodsType(Integer goodsType) {
        this.goodsType = goodsType;
    }

    public String getPublisherName() {
        return publisherName;
    }

    public void setPublisherName(String publisherName) {
        this.publisherName = publisherName;
    }
}

(2)定义各样式(ViewHolder)统一的委托接口

public interface IDelegateAdapter {

    // 查找委托时调用的方法,根据商品的类型选择样式
    boolean isForViewType(Goods goods);

    // 用于委托Adapter的onCreateViewHolder方法
    RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

    // 用于委托Adapter的onBindViewHolder方法
    void onBindViewHolder(RecyclerView.ViewHolder holder, int position, Goods goods);
}

(3)不同样式实现自己的Adapter,创建/复用 RecyclerView.ViewHolder
GoodsOfMineDelegateAdapter.java

public class GoodsOfMineDelegateAdapter implements IDelegateAdapter {

    @Override
    public boolean isForViewType(Goods goods) {
        if(goods.getGoodsType()==1)return true;
        return false;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //缓存池没有ViewHolder,则创建一个ViewHolder
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_goods_of_mine,parent,false);
        GoodsOfMineViewHolder viewHolder = new GoodsOfMineViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, Goods goods) {
        //缓存池已有ViewHolder,直接拿出来进行复用
        GoodsOfMineViewHolder viewHolder = (GoodsOfMineViewHolder) holder;
        viewHolder.goodsName.setText(goods.getGoodsName());
        viewHolder.goodsDescription.setText(goods.getGoodsDescription());
        Glide.with(holder.itemView.getContext()).load(goods.getGoodsImg()).into(viewHolder.goodsImg);
    }

    private static class GoodsOfMineViewHolder extends RecyclerView.ViewHolder{

        TextView goodsName;
        ImageView goodsImg;
        TextView goodsDescription;

        public GoodsOfMineViewHolder(View view){
            super(view);
            goodsName = (TextView) view.findViewById(R.id.name_goods_of_mine);
            goodsImg = (ImageView) view.findViewById(R.id.img_goods_of_mine);
            goodsDescription = (TextView) view.findViewById(R.id.description_goods_of_mine);
        }
    }
}

GoodsOfOthersDelegateAdapter.java

public class GoodsOfOthersDelegateAdapter implements IDelegateAdapter {

    @Override
    public boolean isForViewType(Goods goods) {
        if(goods.getGoodsType()==0)return true;
        return false;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //缓存池没有ViewHolder,则创建一个ViewHolder
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_goods_of_others,parent,false);
        GoodsOfMineViewHolder viewHolder = new GoodsOfMineViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, Goods goods) {
        //缓存池已有ViewHolder,直接拿出来进行复用
        GoodsOfMineViewHolder viewHolder = (GoodsOfMineViewHolder) holder;
        viewHolder.goodsName.setText(goods.getGoodsName());
        viewHolder.goodsDescription.setText(goods.getGoodsDescription());
        viewHolder.goodsPublisher.setText(goods.getPublisherName());
        Glide.with(holder.itemView.getContext()).load(goods.getGoodsImg()).into(viewHolder.goodsImg);
    }

    private static class GoodsOfMineViewHolder extends RecyclerView.ViewHolder{

        TextView goodsName;
        ImageView goodsImg;
        TextView goodsDescription;
        TextView goodsPublisher;

        public GoodsOfMineViewHolder(View view){
            super(view);
            goodsName = (TextView) view.findViewById(R.id.name_goods_of_others);
            goodsImg = (ImageView) view.findViewById(R.id.img_goods_of_others);
            goodsPublisher = (TextView) view.findViewById(R.id.publisher_goods_of_others);
            goodsDescription = (TextView) view.findViewById(R.id.description_goods_of_others);
        }
    }
}

(4)实现RecyclerViewAdapter 继承 RecyclerView.Adapter<RecyclerView.ViewHolder>

public class GoodsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<Goods> goodslist = new ArrayList<>();
    private List<IDelegateAdapter> delegateAdapters = new ArrayList<>();
    private Integer currentType = 0;

    public void setDataItems(List<Goods> goodslist){
        this.goodslist = goodslist;
        notifyDataSetChanged();
    }

    public void addDelegate(IDelegateAdapter delegateAdapter){
        delegateAdapters.add(delegateAdapter);
    }

    @Override
    public int getItemViewType(int position) {
    	// 根据不同位置,通过委托对条目的类别进行判断
        Goods goods = goodslist.get(position);
        for(IDelegateAdapter delegateAdapter : delegateAdapters){
            if(delegateAdapter.isForViewType(goods))
                currentType = goods.getGoodsType();
        }

        return currentType;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 缓冲池没有相应的ViewHolder,根据 getItemViewType得到的viewType设置相应的布局
        // 找到对应的委托Adapter
        IDelegateAdapter delegateAdapter = delegateAdapters.get(viewType);
        // 把onCreateViewHolder交给委托Adapter去处理
        RecyclerView.ViewHolder viewHolder = delegateAdapter.onCreateViewHolder(parent, viewType);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // RecyclerView 缓冲池有对应的ViewHolder,则复用
        int viewType = holder.getItemViewType();
        // 找到对应的委托Adapter
        IDelegateAdapter delegateAdapter = delegateAdapters.get(viewType);
        // 把onCreateViewHolder交给委托Adapter去处理
        delegateAdapter.onBindViewHolder(holder,position,goodslist.get(position));
    }

    @Override
    public int getItemCount() {
        return goodslist.size();
    }
}

(5)RecyclerViewActivity中创建适配器,RecyclerView,绑定。并为RecyclerView设置布局管理器
activity_recycler_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.sdu.chy.chytest.recyclerViewTest.RecyclerViewActivity">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/chy_recycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

RecyclerView.java

    public void initViews(){
        recyclerView = (RecyclerView)findViewById(R.id.chy_recycleView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));	// 线性布局管理器
        // recyclerView.setLayoutManager(new GridLayoutManager(this,3));

        goodsAdapter = new GoodsAdapter();
        goodsAdapter.setDataItems(goodsList);
        goodsAdapter.addDelegate(new GoodsOfMineDelegateAdapter());
        goodsAdapter.addDelegate(new GoodsOfOthersDelegateAdapter());

        recyclerView.setAdapter(goodsAdapter);
    }

对于固定块数的样式。比如硅谷商城界面,分为6块:横幅、频道、活动、秒杀、推荐和热卖。则getItemCount()中return 6(固定),此时getItemViewType(position)则会从0遍历到5,根据各个位置的布局,填充相应的样式(ViewHolder)即可。源码:

	// 6种类型
    public static final int BANNER = 0;	  // 横幅
    public static final int CHANNEL = 1;  // 频道
    public static final int ACT = 2;	  // 活动
    public static final int SECKILL = 3;  // 秒杀
    public static final int RECOMMEND = 4;// 推荐
    public static final int HOT = 5;	  // 热卖
    public int currentType = BANNER;	  // 当前种类
   /**
     * 根据位置得到类型-系统调用
     * @param position
     * @return
     */
    @Override
    public int getItemViewType(int position) {
        switch (position) {
            case BANNER:
                currentType = BANNER;
                break;
            case CHANNEL:
                currentType = CHANNEL;
                break;
            case ACT:
                currentType = ACT;
                break;
            case SECKILL:
                currentType = SECKILL;
                break;
            case RECOMMEND:
                currentType = RECOMMEND;
                break;
            case HOT:
                currentType = HOT;
                break;
        }
        return currentType;
    }

    // 返回总条数(布局分为6块)
    @Override
    public int getItemCount() {
        return 6;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == BANNER) {
            return new BannerViewHolder(mLayoutInflater.inflate(R.layout.banner_viewpager, null), mContext, resultBean);
        } else if (viewType == CHANNEL) {
            return new ChannelViewHolder(mLayoutInflater.inflate(R.layout.channel_item, null), mContext);
        } else if (viewType == ACT) {
            return new ActViewHolder(mLayoutInflater.inflate(R.layout.act_item, null), mContext);
        } else if (viewType == SECKILL) {
            return new SeckillViewHolder(mLayoutInflater.inflate(R.layout.seckill_item, null), mContext);
        } else if (viewType == RECOMMEND) {
            return new RecommendViewHolder(mLayoutInflater.inflate(R.layout.recommend_item, null), mContext);
        } else if (viewType == HOT) {
            return new HotViewHolder(mLayoutInflater.inflate(R.layout.hot_item, null), mContext);
        }
        return null;
    }

 

PopupWindow & Dialog

PopupWindow

在这里插入图片描述

定义popupWindow类

/**
 * 选择照片的PopupWindow
 * Created by chenlijin on 2016/4/12.
 */
public class SelectPicPopupWindow extends PopupWindow implements View.OnTouchListener, View.OnKeyListener {
    private Context mContext;
    private View rootView;

    public SelectPicPopupWindow(Context context) {
        mContext = context;
        LayoutInflater inflater = LayoutInflater.from(context);
        rootView = inflater.inflate(R.layout.popupwindow_selectpic, null);
        setContentView(rootView);
        ButterKnife.bind(this, rootView);
        //设置高度和宽度。
        this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        this.setFocusable(true);

        //设置动画效果
        this.setAnimationStyle(R.style.mypopwindow_anim_style);

        //当单击Back键或者其他地方使其消失、需要设置这个属性。
        rootView.setOnTouchListener(this);
        rootView.setOnKeyListener(this);
        rootView.setFocusable(true);
        rootView.setFocusableInTouchMode(true);

        //实例化一个ColorDrawable颜色为半透明
        ColorDrawable dw = new ColorDrawable(0xb0000000);
        //设置SelectPicPopupWindow弹出窗体的背景
        this.setBackgroundDrawable(dw);
    }


    //点击外部popup消失
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int height = rootView.findViewById(R.id.linearlayout_window).getTop();
        int y = (int) event.getY();
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (y < height) {
                dismiss();
            }
        }
        return true;
    }

    //点back键消失
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && this.isShowing()) {
            this.dismiss();
            return true;
        }
        return false;
    }


    @OnClick({R.id.button_take_photo, R.id.button_select_pic, R.id.button_cancal})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_take_photo:
                listener.onClickTakePhoto();
                this.dismiss();
                break;
            case R.id.button_select_pic:
                listener.onClickSelectPic();
                this.dismiss();
                break;
            case R.id.button_cancal:
                this.dismiss();
                break;
        }
    }

    private OnWindowItemClickListener listener;

    public void setOnWindowItemClickListener(OnWindowItemClickListener listener) {
        this.listener = listener;
    }

    public interface OnWindowItemClickListener {
        void onClickTakePhoto();

        void onClickSelectPic();
    }
}

自定义Style

<style name="MyPopup" parent="android:style/Theme.Dialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@color/popup</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>

定义进入和退出的动画:
进入:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:duration="200"
        android:fromYDelta="100.0%"
        android:toYDelta="0.0"/>
</set>

退出

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:duration="200"
        android:fromYDelta="0.0"
        android:toYDelta="100.0%"/>
</set>

动画的style

<style name="mypopwindow_anim_style">
        <item name="android:windowEnterAnimation">@anim/popup_in</item>
        <!-- 指定显示的动画xml -->
        <item name="android:windowExitAnimation">@anim/popup_out</item>
        <!-- 指定消失的动画xml -->
    </style>

在指定的位置显示

//显示窗口  
window.showAtLocation(MainActivity.this.findViewById(R.id.main), 
Gravity.BOTTOM|Gravity.CENTER_HORIZONT);

Dialog

在这里插入图片描述

定义style

<!--自定义布局的dialog-->
    <style name="MyDialog" parent="android:style/Theme.Dialog">
        <!-- 背景颜色及透明程度 -->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!-- 是否有标题 -->
        <item name="android:windowNoTitle">true</item>
        <!-- 是否浮现在activity之上,会造成macth_parent失效-->
        <item name="android:windowIsFloating">false</item>
        <!-- 是否模糊 -->
        <item name="android:backgroundDimEnabled">true</item>
        <item name="android:windowFrame">@null</item>
    </style>

 

动画: 和popupwindow一致
自定义Dialog:

/**
 * 选择图片对话框
 */
public class SelectPicDialog extends Dialog {
    public SelectPicDialog(Context context, int themeResId) {
        super(context, themeResId);
    }

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

    }

    @OnClick({R.id.linearlayout_out,R.id.textview_take_photo, R.id.textview_select_photo, R.id.textview_cancal})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.textview_take_photo:
                if(listener!=null){
                    listener.onClickTakePhoto();
                }
                this.cancel();
                break;
            case R.id.textview_select_photo:
                if(listener!=null){
                    listener.onClickSelectPic();
                }
                this.cancel();
                break;
            case R.id.linearlayout_out:
            case R.id.textview_cancal:
                this.cancel();
                break;
        }
    }

    private OnWindowItemClickListener listener;

    public void setOnWindowItemClickListener(OnWindowItemClickListener listener) {
        this.listener = listener;
    }

    public interface OnWindowItemClickListener {
        void onClickTakePhoto();
        void onClickSelectPic();
    }
}

在Activity中调用:

SelectPicDialog dialog = new SelectPicDialog(mContext,R.style.MyDialog);
        Window window = dialog.getWindow();
        window.setGravity(Gravity.BOTTOM);  //此处可以设置dialog显示的位置
        window.setWindowAnimations(R.style.mypopwindow_anim_style);  //添加动画
        dialog.show();
        dialog.setOnWindowItemClickListener(new SelectPicDialog.OnWindowItemClickListener(){

            @Override
            public void onClickTakePhoto() {
                startActivityForResult(createCameraIntent(), CREATE_CAMERA);   //选择拍照
            }

            @Override
            public void onClickSelectPic() {
                startActivityForResult(createPickIntent(), CREATE_PICK);   //选择启用系统的选择图片
            }
        });

PopupWindow & Dialog 区别

a、Popupwindow在显示之前一定要设置宽高,Dialog无此限制。

b、Popupwindow默认不会响应物理键盘的back,除非显示设置了popup.setFocusable(true);而在点击back的时候,Dialog会消失。

c、Popupwindow不会给页面其他的部分添加蒙层,而Dialog会。

d、Popupwindow没有标题,Dialog默认有标题,可以通过dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);取消标题

e、二者显示的时候都要设置Gravity。如果不设置,Dialog默认是Gravity.CENTER。

f、二者都有默认的背景,都可以通过setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));去掉。

 

其中最本质的差别就是:AlertDialog是【非阻塞式】对话框:AlertDialog弹出时,后台还可以做事情;而PopupWindow是【阻塞式】对话框:PopupWindow弹出时,程序会等待,在PopupWindow退出前,程序一直等待,只有当我们调用了dismiss方法的后,PopupWindow退出,程序才会向下执行。

两种区别的表现是:

a、AlertDialog弹出时,背景是黑色的,但是当我们点击背景,AlertDialog会消失,证明程序不仅响应AlertDialog的操作,还响应其他操作,其他程序没有被阻塞,这说明了AlertDialog是非阻塞式对话框;

b、PopupWindow弹出时,背景没有什么变化,但是当我们点击背景的时候,程序没有响应,只允许我们操作PopupWindow,其他操作被阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值