第四章 自定义组件、动画

文章目录

  • 描述一下View绘制流程
  • View的事件分发机制 / Touch事件的传递 & 拦截机制
  • 事件分发中的onTouch、onTouchEvent (和onClick) 有什么区别,又该如何使用?
  • View的刷新本质流程
  • requestLayout()、invalidate()与postInvalidate()有什么区别?
  • Android中的动画有哪几类,它们的特点和区别是什么?使用动画的步骤?动画框架源码?
  • Interpolator(插值器)和TypeEvaluator(估值器)的作用
  • ListView & RecycleView
    • ListView 定义 & 原理 & 优化 & 封装?
    • RecycleView

描述一下View绘制流程

View的绘制流程主要分为三步:按顺序依此是measure、layout、draw。
measure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成测量工作。

递归:父View先调用子View的测量方法,再测量自己

layout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的MeasuredWidth和MeasuredHeight以及在xml设置的Gravity、RelativeLayout的其他参数等等一起确定子View在父视图的具体位置。
draw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:① 绘制视图背景。② 绘制画布的图层。 ③ 绘制View内容。 ④ 绘制子视图。⑤ 还原图层。⑥ 绘制滚动条。

View的事件分发机制 / Touch事件的传递 & 拦截机制

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

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

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

  • 事件分发对应方法
    第四章 自定义组件、动画_第1张图片

  • 事件分发 流程
    第四章 自定义组件、动画_第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。

在某个View拦截触摸事件:

  • 设置View 的 即使当前View不可获取点击事件,此时将事件回传给上一级父组件处理
  • 设置View 的 onTouchEvent 返回值为false
  • 设置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的刷新本质流程

(1)View的界面刷新有三种方法invalidate(请求重绘)、requestLayout(重新布局)、requestFocus(请求焦点)
(2)View界面刷新的所有方法均会从View树向上层层找到最顶层的DecorView,通过DecorView的mParent,即ViewRootImpl执行scheduleTraversals()方法进行界面的三大流程。
(3)调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。
(4)当VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程的消息队列中。
(5)主线程从消息队列中取出并调用performTraversals()执行三大流程: onMeasure()-onLayout()-onDraw()
【ViewRootImpl如何和DecorView绑定起来?】
Activity的启动在ActivityThread中完成,handleLaunchActivity()会依次间接执行到onCreate()-onStart()-onResume()
之后会调用WindowManager的addView()将View和Window关联起来。
addView()会创建ViewRootImpl并调用其setView(decorView),内部调用decorView.assignParent(this),将ViewRootImpl设置为DecorView的mParent。
【界面的刷新为什么需要16.6ms?】
系统每16.6ms会发出一个VSYNC信号,发出信号后,才会开始进行测量、布局和绘制。
发出VSYNC信号时,还会将此时显示器的buffer缓冲区的数据取出,并显示在屏幕上。
【界面保持不变时,还会16.6ms刷新一次屏幕吗?】
对于底层显示器,每间隔16.6ms接收到VSYNC信号时,就会用buffer中数据进行一次显示。所以一定会刷新。

requestLayout()、invalidate()与postInvalidate()有什么区别?

requestLayout()用于重新布局,该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法重新设置位置,不一定 会触发onDraw()方法。
invalidate()和postInvalidate()均用于View的重绘。该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会 触发onMesaure()与onLayout()方法,会触发onDraw()方法。
invalidate()是在UI线程中使用,必须配合handler使用;postInvalidate可以在非UI线程中使用,不用使用handler。

Android中的动画有哪几类,它们的特点和区别是什么?使用动画的步骤?动画框架源码?

  • Android 动画种类 特点&区别
视图动画 属性动画
类型 补间动画 逐帧动画 属性动画
作用对象 视图控件(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 属性动画:这是一篇很详细的 属性动画 总结&攻略
    第四章 自定义组件、动画_第4张图片

  • 动画框架源码
    Android 动画原理分析

  • 使用问题 & 建议

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

  • 内存泄露:当我们把动画的repeatCount设置为无限循环时,如果在Activity退出时没有及时将动画停止,属性动画会导致Activity无法释放而导致内存泄漏,而补间动画却没有问题。因此,使用属性动画时切记在Activity执行 onStop 方法时顺便将动画停止。
    在使用ValueAnimator或者ObjectAnimator时(ObjectAnimator继承ValueAnimator),如果没有及时做cancel取消动画,就可能造成内存泄露。ValueAnimator 有个AnimationHandler的单例,会持有属性动画对象自身的引用,属性动画对象持有view的引用,view持有activity引用,所以导致的内存泄露。
    分析:补间动画和属性动画内存泄露

Interpolator(插值器)和TypeEvaluator(估值器)的作用

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

ListView & RecycleView

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

  • ListView & Adapter
    列表 ListView 是 Android中的一种列表视图组件,继承自AdapterView抽象类。
    适配器 Adapter 作为 View 和 数据 之间的桥梁&中介,将数据映射到列表要展示的View中。
    ListView 仅作为容器(列表),用于装载 & 显示数据(即 列表项Item),而容器内的具体每一项的内容(列表项Item)则是由 适配器(Adapter)提供。
    第四章 自定义组件、动画_第5张图片

  • RecycleBin 缓存原理
    第四章 自定义组件、动画_第6张图片
    为了节省空间和时间,ListView不会为每一个数据创建一个视图,而是采用了RecycleBin(Recycler组件),用于回收 & 复用 View。
    当屏幕需显示x个Item时,那么ListView会创建 x+1个视图。移出屏幕的View控件会缓存到RecycleBin当中,当有View进入屏幕后,ListView会从RecycleBin里面取出一个缓存View控件,将其作为convertView参数传递到Adapter的getView中,从而达到View的复用,不必每次都加载布局(LayoutInflater.inflate())

  • ListView 优化

  • 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;
	}
}
  • 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;
}
  • 图片错乱
    图片错乱: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;
}

假设屏幕上有7个条目,向上滑动。新的第8个条目进入界面就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片。由于网络操作耗时,刚进入的条目在图片下载完前会显示缓存中ImageView的图片(即第1个条目的图片),等到下载结束会变回网络图片。(因为第1个图片与第8个图片指向同一块ImageView实例)此时,若ListView快速滑动,移出屏幕的条目被进入的条目重新利用,若此时移出的条目发起的图片请求有了响应。则会造成不同位置显示图片错乱的现象。(显示第15个图片时,第8个图片得到响应,此时的image为第15个图片所复用,但显示的确是第8个图片)
解决方案:通过对ImageView设置tag(通常用图片的url)防止图片错位。
每次getView时(新的元素进入屏幕),对ImageView设置标签。当网络加载结束后,查询当前ImageView的标签,如果更改了,说明该ImageView被新的元素复用(因为移出屏幕的旧元素和进入屏幕的新元素指向的是同一块ImageView实例),则不显示加载的网络图片;否则仍为原来图片元素,显示加载的网络图片。

public View getView(int position, View convertView, ViewGroup parent) {
		String url = urlList.get(position);
		ViewHolder holder;
        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();
		}
		// 给ImageView 设置Tag为当前加载图片的url
		holder.image.setTag(url);
        Glide.with(mContext).load(pic_url).into(new SimpleTarget<GlideDrawable>(){
            @Override
            public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
				// 异步加载图片完成后去取tag,判断是否是加载的图片的url
                String urlTag = (String) holder.image.getTag();
                if (!TextUtils.isEmpty(urlTag) && urlTag.equals(url)) {
                	// 如果当前位置已经移除屏幕,则该holder.image的Tag被其他位置图片url覆盖,则不会满足上述条件,此时该位置的图片不会显示
                	// 若holder.image的Tag为当前url,则说明该image仍为当前元素所使用,没有被新的View复用。显示此次网络加载图片
                    holder.image.setImageDrawable(resource);
                }
            }
        });
        return view;
    }
    
public class ViewHolder{
	ImageView image;
}
  • 最优化方案的完整实现方案
    (1)定义主xml布局:activity_main.xml

<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


<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, null);
            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 —— 下拉刷新 & 上拉加载
    设计思路:
    (1)初始化头布局,动画:自定义头布局,初始隐藏头布局(mHeaderView.setTopPadding(-measuredHeight))
    (2)处理触摸事件,根据下滑偏移量的大小设置不同状态,并根据状态进行处理(修改头布局、数据请求等):
  • ACTION_MOVE && 列表头显示第一条数据(getFirstVisiblePosition == 0):
    (a)if(offset < measuredHeight && currentState != PULL_TO_REFRESH) :不完全显示 => 下拉刷新,修改头布局
    (b)if(offset >= measuredHeight && currentState != RELEASE_REFRESH) :完全显示 => 释放刷新,修改头布局
  • ACTION_DOWN
    (a)if(currentState == RELEASE_REFRESH)
    正在刷新,修改头布局,调用接口方法请求数据
    (b)if(currentState == PULL_TO_REFRESH)
    恢复头布局
    (3)设置监听器,监听列表中数据变化:
  • 控件创建监听器回调接口,并调用接口方法
  • 用户实现接口方法,监听刷新事件,进行网络请求

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

    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

你可能感兴趣的:(Android面试之旅)