自己花了两个礼拜基本掌握了自定义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];
}
}
最后给出源码:点击打开链接