学习到了一个阶段需要总结,对程序员来说最好的总结方式就是把最近掌握的内容写一个demo出来,自定义View也看了蛮久了,最近一直在用网易新闻app,感觉下拉刷新上拉关闭的用户体验非常舒服,所以就自己照着写一个给最近的学习一个总结吧。

    这次没有直接继承ViewGroup而是直接继承了LinearLayout,所以onMeasure就不需要自己折腾了,下面直接上代码,然后讲解下思路:

package com.amuro.utils.custom_view;

import com.amuro.chapter3test.R;
import com.amuro.utils.MyUtils;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Scroller;
import android.widget.TextView;
import android.widget.Toast;

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class RefreshableView extends LinearLayout
{
	public interface OnRefreshListener
	{
		public void onRefresh();
		public void onPullFuction();
	}
	
	private OnRefreshListener onRefreshListener;
	
	public void setOnRefreshListener(OnRefreshListener onRefreshListener)
	{
		this.onRefreshListener = onRefreshListener;
	}
	
	private void notifyRefresh()
	{
		if(onRefreshListener != null)
		{
			onRefreshListener.onRefresh();
		}
	}
	
	private void notifyPullFunction()
	{
		if(onRefreshListener != null)
		{
			onRefreshListener.onPullFuction();
		}
	}
	
	private static final int VELOCITY_UNIT = 1000;
	private static final int Y_VELOCITY_THRESHOLD = 1000;
	
	private boolean canRefresh = true;
	private boolean refreshed = false;
	
	private View headerView;
	private TextView textViewHeaderTitle;
	private ProgressBar progressBarHeader;
	private int headerHeight;
	
	private View bottomView;
	private int bottomHeight;
	
	private boolean firstOnLayout = true;

	private int lastXIntercepted;
	private int lastYIntercepted;
	private int lastY;
	private Scroller scroller;
	private VelocityTracker velocityTracker;
	
	private int screenWidth;
	private int totalHeightWithoutHeader;
	private int heightOfTheWholeViewOfScreen;
	private int maxScrollY;
	
	private Handler handler;
	
	public RefreshableView(Context context)
	{
		this(context, null);
	}
	
	public RefreshableView(Context context, AttributeSet attrs)
	{
		this(context, attrs, 0);
	}

	public RefreshableView(Context context, AttributeSet attrs, int defStyleAttr)
	{
		super(context, attrs, defStyleAttr);
		init();
	}
	
	private void init()
	{
		scroller = new Scroller(getContext());
		velocityTracker = VelocityTracker.obtain();
		screenWidth = MyUtils.getScreenMetrics(getContext()).widthPixels;
		handler = new Handler();
		
		initHeaderView();
		initBottomView();
	}

	@SuppressLint("InflateParams")
	private void initHeaderView()
	{
		headerView = LayoutInflater.from(getContext()).inflate(
				R.layout.header_view_layout, null, true);
		textViewHeaderTitle = (TextView)headerView.findViewById(R.id.tv_title);
		progressBarHeader = (ProgressBar)headerView.findViewById(R.id.pb);
		headerView.measure(0, 0);
		headerHeight = headerView.getMeasuredHeight();
		
		setOrientation(VERTICAL);
		
		addView(headerView, 0, new LayoutParams(screenWidth, headerHeight));
	}

	@SuppressLint("InflateParams")
	private void initBottomView()
	{
		bottomView = LayoutInflater.from(getContext()).inflate(
				R.layout.bottom_view_layout, null, true);
		bottomView.measure(0, 0);
		bottomHeight = bottomView.getMeasuredHeight();
		
		
	}
	
	public void setCanRefresh(boolean canRefresh)
	{
		this.canRefresh = canRefresh;
	}
	
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b)
	{
		super.onLayout(changed, l, t, r, b);
		//这里处理第一次的layout
		if(changed && firstOnLayout)
		{
			int headerHeight = headerView.getHeight();
			((MarginLayoutParams)(headerView.getLayoutParams())).topMargin 
				= - headerHeight;
			
			heightOfTheWholeViewOfScreen = getHeight();
			
			addView(bottomView, new LayoutParams(screenWidth, bottomHeight));
			((MarginLayoutParams)(bottomView.getLayoutParams())).bottomMargin 
			= -bottomHeight;
			
			layoutInit();
			
			firstOnLayout = false;
		}
		
		//这里处理刷新后的界面变化
		if(refreshed)
		{
			layoutInit();
			refreshed = false;
		}
		
	}
	
	private void layoutInit()
	{
		totalHeightWithoutHeader = 0;
		
		int count = getChildCount();
		for(int i = 1; i < count - 1; i++)
		{
			totalHeightWithoutHeader += getChildAt(i).getMeasuredHeight();
		}
		
		maxScrollY = totalHeightWithoutHeader - heightOfTheWholeViewOfScreen;
		
		//这种情况下说明子View无法撑满整个屏幕,所以都置为0
		if(maxScrollY < 0)
		{
			maxScrollY = 0;
		}
		
		
	}
	
	public void requireBottomFunction(boolean isNeed)
	{
		if(isNeed)
		{
			bottomView.setVisibility(View.VISIBLE);
		}
		else
		{
			bottomView.setVisibility(View.INVISIBLE);
		}
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		boolean intercepted = false;
		
		int action = ev.getAction();
		int nowX = (int)ev.getX();
		int nowY = (int)ev.getY();
		
		switch(action)
		{
		case MotionEvent.ACTION_DOWN:
			intercepted = false;
			if(!scroller.isFinished())
			{
				scroller.abortAnimation();
				intercepted = true;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			int dx = nowX - lastXIntercepted;
			int dy = nowY - lastYIntercepted;
			if(Math.abs(dy) > Math.abs(dx))
			{
				//捕获上下滑动事件
				intercepted = true;
			}
			else
			{
				//其他事件放行
				intercepted = false;
			}
			
			break;
		case MotionEvent.ACTION_UP:
			intercepted = false;
			break;
		}
		
		lastXIntercepted = nowX;
		lastYIntercepted = nowY;
		lastY = nowY;
		
		return intercepted;
	}

	@SuppressLint("ClickableViewAccessibility")
	@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		velocityTracker.addMovement(ev);
		int action = ev.getAction();
		int nowY = (int) ev.getY();
		
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			if(!scroller.isFinished())
			{
				scroller.abortAnimation();
			}
			break;
		case MotionEvent.ACTION_MOVE:
			int dy = nowY - lastY;
			
			if(canRefresh)
			{
				int scrollY = -getScrollY();
				if(scrollY >= headerHeight / 3 && scrollY < 2 * (headerHeight / 3))
				{
					textViewHeaderTitle.setText("继续拉");
				}
				
				if((scrollY >= 2 * (headerHeight / 3) && scrollY < headerHeight))
				{
					textViewHeaderTitle.setText("再继续拉");
				}
				
				if(scrollY >= headerHeight)
				{
					textViewHeaderTitle.setText("骚年,可以了...");
				}
				
				scrollBy(0, -dy);
				
			}
			else
			{
				//设置不能下拉刷新的时候,当滑动量大于ScrollY(也就是HeaderView要展示了),
				//把滑动量减少为ScrollY的值
				if(dy > getScrollY())
				{
					scrollBy(0, -getScrollY());
				}
				else
				{
					scrollBy(0, -dy);
				}
			}
		
			
			break;
		case MotionEvent.ACTION_UP:

			velocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
			int yVelocity = (int) velocityTracker.getYVelocity();
			int scrollYEnd = getScrollY();

			int yToScroll = 0;
			
			//这里处理下拉刷新的部分
			if (scrollYEnd < 0)
			{
				if (scrollYEnd >= -headerHeight)
				{
					yToScroll = -scrollYEnd;
					giveUpRefresh();
				}
				else
				{
					yToScroll = (-scrollYEnd) - headerHeight;
					doRefresh();
				}

			}
			//上拉更多或其他
			else if(scrollYEnd >= maxScrollY)
			{
				yToScroll = -(scrollYEnd - maxScrollY);
				
				if(-yToScroll >= bottomHeight + 100)
				{
					doPullFunction();
				}
				
			}
			//正常的滑动
			else
			{
				//向上滑动的时候,弹性滑动距离超过整个view的高度的时候(也就是会导致Bottom出现),
				//此时将距离减少为整个 View剩余的在屏幕下方外沿的距离
				//不好理解可自行画图理解,确实蛋疼
				if(yVelocity < -Y_VELOCITY_THRESHOLD)
				{
					yToScroll = - (yVelocity / 10);
					
					if(yToScroll >= (maxScrollY - scrollYEnd))
					{
						yToScroll = maxScrollY - scrollYEnd;
					}
				}
				
				//向下滑动的时候,弹性滑动距离大于超过ScrollY(也就是会导致header出现),
				//此时将距离减小为ScrollY的值,原理和上滑是一样的
				if(yVelocity > Y_VELOCITY_THRESHOLD)
				{
					yToScroll = - (yVelocity / 10);
					
					if(-yToScroll > scrollYEnd)
					{
						yToScroll = - scrollYEnd;
					}
					
					
				}
			}

			Log.e("Amuro", "Velocity -> " + yVelocity);
			Log.e("Amuro", "SY -> " + scrollYEnd);
			Log.e("Amuro", "yToScroll -> " + yToScroll);
			
			scroller.startScroll(0, scrollYEnd, 0, yToScroll);
			invalidate();
			velocityTracker.clear();
			
			break;
		}
		
		lastY = nowY;
		
		return true;
	}
	
	private void giveUpRefresh()
	{
		resetHeaderTitle();
	}
	
	private void doRefresh()
	{
		textViewHeaderTitle.setText("正在刷新...");
		progressBarHeader.setVisibility(View.VISIBLE);
		notifyRefresh();
	}
	
	private void doPullFunction()
	{
		notifyPullFunction();
	}
	
	public void stopRefresh()
	{
		Toast.makeText(getContext(), "refresh finished", Toast.LENGTH_SHORT).show();
		
		progressBarHeader.setVisibility(View.GONE);
		resetHeaderTitle();

		scroller.startScroll(0, getScrollY(), 0, headerHeight);
		invalidate();
		
		refreshed = true;
		requestLayout();
	}

	private void resetHeaderTitle()
	{
		handler.postDelayed(new Runnable()
		{
			
			@Override
			public void run()
			{
				textViewHeaderTitle.setText("下拉刷新");
			}
		}, 50);
	}
	
	@Override
	public void computeScroll()
	{
		if(scroller.computeScrollOffset())
		{
			scrollTo(scroller.getCurrX(), scroller.getCurrY());
			postInvalidate();
		}
	}
	
	@Override
	protected void onDetachedFromWindow()
	{
		velocityTracker.recycle();
		super.onDetachedFromWindow();
	}

}

    算法上都写了详细注释了,这里主要讲下思路:

    1. 下拉刷新网上有很多例子了,基本就是写一个HeaderView然后在layout的时候通过负的margin把这个view放到屏幕上面去(注意这样做之后ScrollY=0的位置仍然是headerView之上而不是屏幕可见的topline之上,这个一开始也坑死偶了)。这里在下拉的三个阶段分别改变了textView的字段,这样可以有更好的用户体验,下拉的阈值就是HeaderView的高度,如果小于这个高度就认为用户放弃了更新,大于这个高度就通知监听器用户下拉刷新了。

    2. 下拉刷新之后通过scroller弹回headerView,弹回的时候需要重置headerView的title,这里用了handler做了个小延时,让用户看不到字的变化。

    3. 正常滑动仿照了上一篇的代码,onInterceptTouchEvent中吸收上下滑的事件,其他事件正常放行。

    4. 对外提供stopRefresh方法,在调用者的耗时操作完成后,通知我们的View将headerView隐藏。

    5. refresh完成后,要记得requestLayout,这时onLayout会被回调,在里面我们要重新获取内部View的高度以及可以允许用户滑动的最大scrollY。

    6. 用户放弃刷新也可提供相关回调供外部添加自己想要的事件,这里有兴趣的可以自己添加,没有难度。

    7. 上拉关闭其实是外部提供的功能,我们的RefreshableView其实只管添加这个bottomView然后回调这个事件给外部。另外上拉的阈值简单起见就写成了bottomView的高度加上100px,这个都可以根据需求定制,也可以和下拉一样,拉一点改几个字,就不赘述了。

    8. headerView和bottomView全部通过layout文件配置,可根据需求随意更改,demo就不做那么复杂了,大概效果能看出来就行了。

    9. 正常滑动的时候,为了有弹性的效果,在滑动加速度超过阈值的时候,在move action的scrollBy基础上,会多滑加速度的十分之一的距离,这个都可以配置,感觉还可以再大一点,弹性效果更好。

    然后看一下调用的Activity:

package com.amuro.main;

import java.util.ArrayList;
import java.util.Random;

import com.amuro.chapter3test.R;
import com.amuro.utils.custom_view.RefreshableView;
import com.amuro.utils.custom_view.RefreshableView.OnRefreshListener;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

public class MainActivity5 extends Activity implements OnRefreshListener
{
	private static final int ON_REFRESH_FINISHED = 0x01;
	
	private int pageCount = 1;
	private int itemCount = 0;
	
	@SuppressLint("HandlerLeak")
	private Handler handler = new Handler()
	{
		public void handleMessage(Message msg) 
		{
			switch (msg.what)
			{
			case ON_REFRESH_FINISHED:
				itemCount = 0;
				pageCount++;
				datas.clear();
				itemAccount = random.nextInt(10) + 10;
				for(int i = 0; i < itemAccount; i++)
				{
					datas.add("Page " + pageCount + ", item " + ++itemCount);
				}
				adapter.notifyDataSetChanged();
				refreshableView.stopRefresh();
				break;

			default:
				break;
			}
		}
	};
	
	private RefreshableView refreshableView;
	private ListView listView;
	private ArrayList<String> datas;
	private ArrayAdapter<String> adapter;
	
	private Random random;
	private int itemAccount;
	
	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
//		requestWindowFeature(Window.FEATURE_NO_TITLE);
//		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
//				WindowManager.LayoutParams.FLAG_FULLSCREEN);
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main_5_layout);
		
		initView();
	}

	private void initView()
	{
		random = new Random();
		initListView();
		initRefreshableView();
	}

	private void initListView()
	{
		listView = (ListView)findViewById(R.id.lv);
		datas = new ArrayList<>();
		itemAccount = random.nextInt(10) + 10;
		for(int i = 0; i < itemAccount; i++)
		{
			datas.add("Page " + pageCount + ", item " + ++itemCount);
		}
		adapter = new ArrayAdapter<>(
				this, android.R.layout.activity_list_item, android.R.id.text1, datas);
		listView.setAdapter(adapter);
	}
	
	private void initRefreshableView()
	{
		refreshableView = (RefreshableView)findViewById(R.id.rv);
		refreshableView.setCanRefresh(true);
		refreshableView.requireBottomFunction(true);
		refreshableView.setOnRefreshListener(this);
	}

	@Override
	public void onRefresh()
	{
		Thread th = new Thread(new Runnable()
		{
			
			@Override
			public void run()
			{
				try
				{
					Thread.sleep(2000);
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
				
				handler.sendEmptyMessage(ON_REFRESH_FINISHED);
			}
		});
		
		th.start();
	}

	@Override
	public void onPullFuction()
	{
		Toast.makeText(this, "Pull", Toast.LENGTH_SHORT).show();
		finish();
	}
}


        代码很好理解了,简单起见就起个线程sleep一下当耗时操作了,界面效果是刷新下就重新随机生成一坨list数据,上拉就finish,和网易新闻的效果一样一样的~

上下效果图最后:

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐