自己花了两个礼拜基本掌握了自定义View,无论是继承现有的TextView,LinearLayout等还是继承View,ViewGroup,我都实现了一遍,收获了许多。自定义View的基本流程是:自定义属性一>测量onMeasure一>布局onLayout一>绘制onDraw一>处理onTouchEvent等。实际使用时,只要处理其中几个环节就行。 像我自己绘制的天气中的折线图以及空调遥控器,关键就是onDraw方法,灵活运用paint,canvas的绘图技巧,加上精确计算。而ListView的下拉刷新就是onLayout和LayoutParams的使用。 这里参考了郭婶的一篇博客,主要部分加入了我自己的想法,附上链接郭婶的博客 原理:(引用郭婶的)先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头(header)和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏 。

header布局:一个下拉箭头,一个文字描述,一个刷新时的图片wait(这里简单就用ic_launcher),当正在刷新时,使得下拉箭头隐藏,wait图片可见。

</span><pre class="html" name="code"><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_head"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:orientation="horizontal"
    android:paddingBottom="30dp">

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2">
        <ImageView
            android:id="@+id/arrow"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:src="@drawable/arrow"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"/>
        <ImageView
            android:id="@+id/wait"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:visibility="gone"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:src="@mipmap/ic_launcher"/>
    </RelativeLayout>
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"
        android:layout_marginLeft="10dp">

        <TextView
            android:id="@+id/description"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:text="下拉可刷新"
           />
    </LinearLayout>
</LinearLayout>

1.新建一个类继承LinearLayout,先看看声明了哪些变量和常量:

public class MyRefreshView extends LinearLayout implements View.OnTouchListener {
    private static final String TAG = "MyRefreshView";
    public static final int STATUS_PULL_TO_REFRESH = 0;//下拉状态
    public static final int STATUS_RELEASE_TO_REFRESH = 1;//释放立即刷新状态
    public static final int STATUS_REFRESHING = 2;//正在刷新状态
    public static final int STATUS_REFRESH_FINISHED = 3;//刷新完成或未刷新状态
    //header下拉的最大的topMargin,效果是下拉到一定程度就下拉不了
    private static final int MAX_TOP_MARGIN=80;

    private PullToRefreshListener mListener;//回调接口
    private View header;//下拉头的View
    private ListView listView;
    private ImageView arrow;
    private ImageView wait;
    private TextView description;//文字描述

    private MarginLayoutParams headerLayoutParams;//下拉头的布局参数
    private int hideHeaderHeight;//下拉头的高度

    //当前状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
    //STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
    private int currentStatus = STATUS_REFRESH_FINISHED;


    private float yDown;//手指按下时的屏幕纵坐标
    private float yMove;//手指移动时的屏幕纵坐标
    private int touchSlop;//系统所能识别的被认为是滑动的最小距离
    private int top_Margin;//记录header的headerLayoutParams.topMargin

2.重写他的onLayout方法:刚开始时,让下拉头隐藏在屏幕上方

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        hideHeaderHeight = header.getHeight();//得到下拉头View的高度
        headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
        //让其LayoutParams.topMargin为负的下拉头高度,这样刚开始时,下拉头就会隐藏在屏幕上方
        headerLayoutParams.topMargin = -hideHeaderHeight;
        listView = (ListView) getChildAt(1);//得到ListView
        listView.setOnTouchListener(this);//设置onTouch监听,来处理下拉的具体逻辑
    }

    private void init(Context context) {
        header = LayoutInflater.from(context).inflate(R.layout.layout_myhead, null, true);
        wait = (ImageView) header.findViewById(R.id.wait);
        arrow = (ImageView) header.findViewById(R.id.arrow);
        description = (TextView) header.findViewById(R.id.description);
        //系统所能识别的被认为是滑动的最小距离
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        setOrientation(VERTICAL);
        addView(header, 0);
    }

3.只有在listView滑到顶部的前提下,才再考虑若手指向下滑动让下拉头显示的逻辑。这个方法就是判断listView是否滑到顶部:

private boolean IsAbleToPull() {
        View firstChild = listView.getChildAt(0);//得到listView的第一个item
        if (firstChild != null) {
            int firstVisiblePos = listView.getFirstVisiblePosition();
            if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
                //如果可视的第一个Item的position为0,说明当前的第一个Item为整个listView的第一个Item,并且
                // 第一个Item的上边缘距离父布局值为0,两者同时满足说明ListView滚动到了最顶部,此时允许下拉刷新
                return true;
            } else {
                if (headerLayoutParams.topMargin != -hideHeaderHeight) {
                    headerLayoutParams.topMargin =- hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                return false;
            }
        } else {
            return true;// 如果ListView中没有元素,默认允许下拉刷新
        }
    }

4.具体分析下拉情况:刚开始时,手指向下滑动,下拉头慢慢下移,下拉头内容(箭头下指,文字描述为下拉可刷新);当下拉头刚好完全显示时,下拉头内容变为(箭头上指,文字描述为释放立即刷新);释放时,下拉头内容(箭头隐藏,wait图片可见,文字描述为正在刷新...);完成刷新后,下拉头隐藏。我把这些写在了一个方法里,根据传入的参数具体变化:

public void headerInfoChange(int i) {
        ObjectAnimator anim;
        switch (i) {
            case STATUS_PULL_TO_REFRESH:
                description.setText("下拉可刷新");
                wait.setVisibility(GONE);
                arrow.setVisibility(VISIBLE);
                anim = ObjectAnimator.ofFloat(arrow, "rotation", 180, 0);
                anim.setDuration(300).start();
                break;
            case STATUS_RELEASE_TO_REFRESH:
                description.setText("释放立即刷新");
                wait.setVisibility(GONE);
                arrow.setVisibility(VISIBLE);
                anim = ObjectAnimator.ofFloat(arrow, "rotation", 0, 180);
                anim.setDuration(300).start();
                break;
            case STATUS_REFRESHING:
                wait.setVisibility(VISIBLE);
                description.setText("正在刷新");
                arrow.setVisibility(INVISIBLE);
                break;
        }
    }

5.关键:重写listView的onTouch方法,根据手指滑动,处理相应逻辑:

public boolean onTouch(View view, MotionEvent event) {
        if (IsAbleToPull()) {//判断listView滑到顶部
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    yDown = event.getRawY();
                    break;

                case MotionEvent.ACTION_MOVE:
                     yMove = event.getRawY();
                    int distance = (int) (yMove - yDown);
                    //distance>0说明手指是向下滑动(下拉),distance>touchSlop说明这次为有效下拉操作
                    if (distance > touchSlop) {
                        if (currentStatus != STATUS_REFRESHING) {
                            // 通过偏移下拉头的topMargin值,来实现下拉效果
                            //distance/2是为了有更好的下拉体验,你得向下滑动header高度的两倍,才能使header刚好全部显示
                            headerLayoutParams.topMargin = (distance / 2) - hideHeaderHeight;
                            if (headerLayoutParams.topMargin > MAX_TOP_MARGIN) {
                                headerLayoutParams.topMargin = MAX_TOP_MARGIN;//下拉到一定程度就下拉不了了
                            }
                            header.setLayoutParams(headerLayoutParams);

                            if (headerLayoutParams.topMargin > 0) {//当header全部显示出来时,箭头上指,释放立即刷新
                                if (currentStatus != STATUS_RELEASE_TO_REFRESH) {//加个判断是为了当headerLayoutParams.topMargin>0
                                    headerInfoChange(STATUS_RELEASE_TO_REFRESH);//的所有下拉过程中只执行一次
                                }
                                currentStatus = STATUS_RELEASE_TO_REFRESH;
                            } else {//当header没有全部显示时,箭头下指,下拉可刷新
                                if (currentStatus != STATUS_PULL_TO_REFRESH) {//加个判断是为了当headerLayoutParams.topMargin<0
                                    headerInfoChange(STATUS_PULL_TO_REFRESH);//的所有下拉过程中只执行一次
                                }
                                currentStatus = STATUS_PULL_TO_REFRESH;
                            }
                        }
                    }
                    break;

                case MotionEvent.ACTION_UP:
                default:
                    if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
                       headerInfoChange(STATUS_REFRESHING);//arrow隐藏,wait可见,正在刷新
                       new RefreshingTask().execute();// 松手时如果是释放立即刷新状态,就调用正在刷新的任务
                    } else if (currentStatus == STATUS_PULL_TO_REFRESH) {
                        hideHeader();// 松手时如果是下拉状态,就隐藏下拉头
                    }
                    break;
            }

            if (currentStatus == STATUS_PULL_TO_REFRESH || currentStatus == STATUS_RELEASE_TO_REFRESH) {
                // 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
                listView.setPressed(false);
                //Set whether this view can receive the focus. Setting this to false will also ensure that this view is not focusable in touch mode
                listView.setFocusable(false);
                listView.setFocusableInTouchMode(false);
                // 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
                return true;
            }

        }
        return false;
    }

6.隐藏下拉头方法:

public void hideHeader() {
        top_Margin = headerLayoutParams.topMargin;//先记录下header上移的初始位置
        final int height =top_Margin + hideHeaderHeight;//header从开始上移到结束上移的总高度
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                //valueAnimator.getAnimatedValue()从0一>1变化,当为0时,表示开始上移,这时headerLayoutParams.topMargin
                //应该为之前保存的top_Margin,当为1时,表示结束上移,这时headerLayoutParams.topMargin应该为负的hideHeaderHeight
                headerLayoutParams.topMargin = top_Margin - (int) ((float) valueAnimator.getAnimatedValue() * height);
                header.setLayoutParams(headerLayoutParams);

            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.start();
        currentStatus=STATUS_REFRESH_FINISHED;
    }

7.正在刷新时,调用回调接口。这里用AsyncTask实现:

class RefreshingTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentStatus = STATUS_REFRESHING;
            if (mListener != null) {
                mListener.onRefresh();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            hideHeader();
            currentStatus = STATUS_REFRESH_FINISHED;
        }


    }

8.最后是接口定义,给出设置接口的公共方法:

public interface PullToRefreshListener {

        /**
         * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。
         */
        void onRefresh();

    }

    public void setOnRefreshListener(PullToRefreshListener listener) {
        mListener = listener;

    }

至此,自定义View完成。这里有个问题:当刷新完成后,执行AsyncTask子类的onPostExecute方法里的hideHeader方法,应该有动画(下拉头慢慢移上去),但实际效果是下拉头瞬间隐藏。用Log输出hideHeader方法里的headerLayoutParams.topMargin值,发现当刷新完成后值为-hideHeaderHeight的,这本该是动画结束后的值。

后来我怀疑是多线程AsyncTask的原因,所以我在AsyncTask的doInBackgroud方法中获取到headerLayoutParams.topMargin值,在onProgressUpdate中再将值赋给headerLayoutParams.topMargin。问题解决了,但还是不清楚为什么会出现这个问题。希望以后能找到答案。附上修改后的代码:

class RefreshingTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentStatus = STATUS_REFRESHING;
            if (mListener != null) {
                mListener.onRefresh();
            }
            publishProgress(topMargin);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            hideHeader();
            currentStatus = STATUS_REFRESH_FINISHED;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            headerLayoutParams.topMargin = values[0];
        }

    }

 最后给出源码:点击打开链接