初识安卓之:微博下拉刷新效果的实现
主界面代码:给自定义控件添加监听,并设置触动监听时相应的动作:访问服务器请求资源
public class MainActivity extends Activity {
private List<String> data;
private MyAdapter adapter;
private MyListView myList;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//我犯了一个错误,耽误我很长时间:xml中id名不能和类名相同!!!!!!!!!!!!!!
setContentView(R.layout.main_listview);
//必须先给data集合设置数据后,再给控件添加适配器,适配器里才有值,否则就会空指针异常
data = new ArrayList<String>();
data.add("a");
data.add("b");
data.add("c");
data.add("d");
data.add("e");
myList = (MyListView) findViewById(R.id.listView);
adapter = new MyAdapter();
myList.setAdapter(adapter);
myList.setOnReflashListener(new onReflashListener() {
@Override
public void reflash() {
// TODO Auto-generated method stub
//异步执行专用类AsyncTask
new AsyncTask<Void, Void, Void>(){
//在后台开辟新线程,执行耗时操作
@Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
SystemClock.sleep(2000);
data.add("新出来的数据");
return null;
}
//当后台方法执行完以后的操作:
@Override
protected void onPostExecute(Void result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
//从服务器里获得了新数据后,一定要更新listview才回显示
adapter.notifyDataSetChanged();
//获得了新数据,也刷新完了以后,就可以做一些其他事情了,先把方法写在这里
myList.onHaveReflashed();
}
}.execute(null);
}
});
}
//我们使用的最多的就是继承基本适配器,之后再自定义操作
private class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
// TODO Auto-generated method stub
return data.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return data.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
TextView textTV=new TextView(getApplicationContext());
textTV.setText(data.get(position));
return textTV;
}
}
}
自定义控件代码:
public class MyListView extends ListView implements OnScrollListener {
private int firstVisibleItemIndex;
private onReflashListener reflashListener;
private boolean hasUsedReflashListener;
private View headView;
private ProgressBar progress;
private int headmeasuredWidth;
private int headmeasuredHeight;
private Animation animation;
private Animation reverseanimation;
boolean isRecord;//手指位置有没有被记录过,默认是没有被记录过
private int state;//用来记录当前下拉刷新状态
// 核心代码,头控件所处的四种状态:
// 1.正在拉出来/推上去状态:从看不到的状态往下不断的拉,直到头完全拉出来前的一段状态
private static final int PULL_TO_REFLASH = 0;
// 2.拉出来了未松手状态:
private static final int RELEASE_TO_REFLASH = 1;
// 3.正在刷新状态(松手就刷新):
private static final int REFLASHING = 2;
// 4.刷新完成状态
private static final int DONE = 3;
private float startY;
private float tempY;
private ImageView arrIV;
private TextView titlTV;
private TextView timeTV;
private boolean ispush;//正在往上推头控件
public MyListView(Context context) {
super(context);
init(context);
// TODO Auto-generated constructor stub
}
public MyListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
init(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}
// 首次打开软件,视图上需要进行的初始化操作:
private void init(Context context) {
headView = View.inflate(context, R.layout.header, null);
arrIV = (ImageView) headView.findViewById(R.id.arrow);
titlTV = (TextView) headView.findViewById(R.id.title);
timeTV = (TextView) headView.findViewById(R.id.time);
progress = (ProgressBar) headView.findViewById(R.id.progressBar);
arrIV.setMinimumWidth(70);
arrIV.setMinimumHeight(50);
// 测绘头控件的宽和高,自定义测绘方法
measureView(headView);
// 测绘完毕后,头控件就由了明确的大小,把它的长和宽记录一下,以后就可以用了
headmeasuredWidth = headView.getMeasuredWidth();
headmeasuredHeight = headView.getMeasuredHeight();
// 刚打开软件时,是最新的数据,不需要更新,此时头控件是隐藏在上面的,所以要给控件设置一个垫子(只在高度方向加垫子,垫高它,让它看不见)
headView.setPadding(0, -1 * headmeasuredHeight, 0, 0);
// 把头控件重绘一下,一般看到效果
headView.invalidate();
// 给自定义的MyListView视图设置一个顶部视图:
addHeaderView(headView);
// 给自定义视图设置滑动监听(自定义视图已经实现了滑动监听接口了)
setOnScrollListener(this);
// 定义一个翻转动画动作,等待箭头的调用
animation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF,
0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
// 设置动画的运行时间
animation.setDuration(250);
// 保存动画的运行效果
animation.setFillAfter(true);
// 设置动画效果扩展器,加深动画特效
animation.setInterpolator(new LinearInterpolator());
/**
* 常见动画特效: Interpolator 定义了动画的变化速度,可以实现匀速、正加速、负加速、无规则变加速等;
* AccelerateDecelerateInterpolator,延迟减速,在动作执行到中间的时候才执行该特效。
* AccelerateInterpolator, 会使慢慢以(float)的参数降低速度。 LinearInterpolator,平稳不变的
* DecelerateInterpolator,在中间加速,两头慢
* CycleInterpolator,曲线运动特效,要传递float型的参数。
*/
// 再定义一个转回来的动画特效
reverseanimation = new RotateAnimation(0, 180,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
reverseanimation.setDuration(250);
reverseanimation.setFillAfter(true);
reverseanimation.setInterpolator(new LinearInterpolator());
state=DONE;
isRecord=false;
}
// 测量控件宽和高的具体过程:
private void measureView(View childView) {
// 测绘之前先得到被测绘对象的所有布局参数集:params
ViewGroup.LayoutParams layoutParams = childView.getLayoutParams();
// 如果还没有设置过布局参数集的参数,则设置一下:
if (layoutParams == null) {
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
// 如果布局参数已经设置好了,则得到参数集中的宽和高的值
// 由于控件的宽度已经定死了不会变化,所以得到子类的测绘宽度值就通过测绘布局参数集中的宽度得到就行了,直接getChildMeasureSpec
int childViewMeasureWidth = ViewGroup.getChildMeasureSpec(0, 0,
layoutParams.width);
int childViewMeasureHeigth;
// 控件的高度值却是时刻变化的,不那么容易得到,无法直接get了,必须利用测绘值产生器MeasureSpec.makeMeasureSpec,根据实际情况产生该值
// 布局参数集中高度大于0时,也就是控件被拉出来了的时候,测绘高度,EXACTLY:父布局决定子布局的确切大小。不论子布局多大,它都必须限制在这个界限里
if (layoutParams.height > 0) {
childViewMeasureHeigth = MeasureSpec.makeMeasureSpec(
layoutParams.height, MeasureSpec.EXACTLY);
} else {
// 如果控件没有被拉出来,是处于隐藏状态,则模式未未指定模式,返回测绘高度就是0,代表被隐藏,看不到了
childViewMeasureHeigth = MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED);
}
// 有了测绘高度和宽度值,就可以唯一确定一个控件的大小了。该控件就可以使用了!
childView.measure(childViewMeasureWidth, childViewMeasureHeigth);
}
// 我们的自定义控件是实现了滑动监听的,根据动作事件的不同,有着不同的事件记录,主要是对手指位置的记录,传入手指事件对象
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
//同一时刻只有可能一种事件会发生:
//事件一:屏幕只是被按下了
case MotionEvent.ACTION_DOWN:
//只有第一个数据条目是在最顶端的时候,并且之前手指按下位置没有做过记录时,我们才认为此时的手指按下位置是有价值的,否则我们不对手指位置做任何记录
if(firstVisibleItemIndex==0&&isRecord){
//满足条件后,才记录手指按下时的位置
startY = event.getY();
isRecord=true;
}
break;
//事件二:屏幕不光被按住了,还在拖动
case MotionEvent.ACTION_MOVE:
//记录当前手指位置
tempY = event.getY();
//屏幕被按住拖动时,并不是一按下时的位置就能作为手指滑动初始位置的,因为如果第一个数据条目没有在最顶端或者已经记录过时,此时的手指位置对我们来言是没有
//任何意义的,什么时候,手指的即时位置才是对我们来说有意义的初始位置呢?当第一个条目刚刚被拉出来且之前没有记录过时,此时手指的即时是很有意义的,可以作为
//屏幕被按住拖动时,手指滑动的起始位置
if(firstVisibleItemIndex==0&&isRecord){
startY=tempY;
isRecord=true;
}
//刷新的时候头控件是不能被拖动的!
if(state!=REFLASHING){
//头控件在不同的状态时,拖动它会引发状态的不同改变,要一一分析:!!!!!!!!!!
//1.头控件本来就处于拉出来或者推上去状态时:
if(state== PULL_TO_REFLASH){
//手指触摸点的参照点设置到最上面条目上,避免bug,更加流畅
setSelection(0);
//1.1如果在拖动过程中,头控件被完全拖出来了,则头状态发生改变
if((tempY-startY)/3>headmeasuredHeight){
state=RELEASE_TO_REFLASH;
headStateChange();
}//1.2本来是出来了一点,但是又给推回去了时:
else if((tempY-startY)<0){
state=DONE;
headStateChange();
}
}
// 2.头控件本来处于:拉出来了未松手状态:
if(state== RELEASE_TO_REFLASH){
setSelection(0);
//2.1未松手时又给推上去了,但是没有完全推没
if((tempY-startY)/3<headmeasuredHeight&&(tempY-startY)>0){
state=PULL_TO_REFLASH;
ispush=true;
headStateChange();
}
//2.2完全推没了的时候:
else if((tempY-startY)<0){
state=DONE;
headStateChange();
}
}
//3.头控件本来就处于:刷新完成状态时
if(state==DONE){
//3.1只要一拉出来,就设置它处于松开刷新状态
if((tempY-startY)>1.5*headmeasuredHeight){
state=PULL_TO_REFLASH;
headStateChange();
}
}
if(state == PULL_TO_REFLASH || state == RELEASE_TO_REFLASH){
headView.setPadding(0, (int) ((tempY - startY)/3 - headmeasuredHeight), 0, 0);
}
}
break;
//事件三:手指离开屏幕时
case MotionEvent.ACTION_UP:
if(state != REFLASHING){
if(state == PULL_TO_REFLASH){//下拉刷新
state = DONE;
headStateChange();
}
if(state == RELEASE_TO_REFLASH){//松开刷新
state = REFLASHING;
headStateChange();
onRefresh();
}
}
break;
}
return super.onTouchEvent(event);
}
private void onRefresh() {
// TODO Auto-generated method stub
if(hasUsedReflashListener){
//调用监听器对象去执行刷新动作
reflashListener.reflash();
}
}
private void headStateChange() {
//当前状态如果不是正在刷新的话,那么就有可能是另外三种状态了:1.正在拉出来/推上去状态;2.拉出来了未松手状态; 3.正在刷新状态(松手就刷新): 4.刷新完成状态
//1.正在拉出来/推上去状态
switch (state) {
case PULL_TO_REFLASH:
arrIV.setVisibility(View.VISIBLE);
titlTV.setVisibility(View.VISIBLE);
timeTV.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
titlTV.setText("下拉可刷新");
arrIV.clearAnimation();//清空上次的动画效果
//如果这时是在推上去
if(ispush){
arrIV.setAnimation(animation);
//出了动画效果后一定要把状态改变,设置推上去状态为结束,以免再拉出来时还有这个动画
ispush=false;
}
break;
//2.拉出来了未松手状态
case RELEASE_TO_REFLASH:
arrIV.setVisibility(View.VISIBLE);
titlTV.setVisibility(View.VISIBLE);
timeTV.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
titlTV.setText("松手开始刷新");
arrIV.clearAnimation();//清空上次的动画效果
arrIV.setAnimation(reverseanimation);
break;
//3.正在刷新状态(松手就刷新)
case REFLASHING:
arrIV.setVisibility(View.GONE);
titlTV.setVisibility(View.VISIBLE);
timeTV.setVisibility(View.VISIBLE);
progress.setVisibility(View.VISIBLE);
arrIV.clearAnimation();
titlTV.setText("正在刷新中");
//正在刷新的时候,头控件不要动,把四周垫死
headView.setPadding(0, 0, 0, 0);
break;
// 4.刷新完成状态,和正在刷新一模一样的设置,但头控件位置有变化,要隐藏起来
case DONE:
arrIV.setVisibility(View.VISIBLE);
titlTV.setVisibility(View.VISIBLE);
timeTV.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
arrIV.clearAnimation();
titlTV.setText("下拉可刷新");
headView.setPadding(0, -1*headmeasuredHeight, 0, 0);
break;
}
}
// 滑动监听中的onscroll方法里有一个非常重要的参数:firstVisibleItem
// ,用来标记所有条目中的第一个,也就是最前面的那个条目的位置,需要记录下来,以后
// 在进行下拉动作时,要判断,只有第一个条目出来后还往下拉时,才触动刷新,访问服务器
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
firstVisibleItemIndex = firstVisibleItem;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
}
// 本自定义控件对外提供一个可回调的监听器,它作用如果本控件当时状态被判断为可以进行刷新,则触发该监听,主界面调用刷新方法,访问服务器
public interface onReflashListener {
abstract void reflash();
}
// 把接口对象放到公共方法里,以便暴露出去后能让主界面调用到,因为要想使用这个方法必须传入一个抽象类的对象,此时必须重写抽象方法
public void setOnReflashListener(onReflashListener listener) {
// 主界面调用到我们的自定义监听器的时候,一定会回传过来一个监听器对象,用一个值把它记录下来,方便本类中使用
reflashListener = listener;
// 只要回调了本方法,就证明你使用了我,就把它记下来,方便以后判断有没有被使用
hasUsedReflashListener = true;
}
// 刷新完成后有哪些必须的操作:下拉刷新的文字状态要更改,时间要更改
public void onHaveReflashed() {
// TODO Auto-generated method stub
state=DONE;
headStateChange();
timeTV.setText(new Date().toLocaleString());
}
//为了保证一开始就有时间显示,所以要重写setadapter方法,一开始就让文本框有一个初始的时间
@Override
public void setAdapter(ListAdapter adapter) {
// TODO Auto-generated method stub
timeTV.setText("更新于 : " + new Date().toLocaleString());
super.setAdapter(adapter);
}
}