学习到了一个阶段需要总结,对程序员来说最好的总结方式就是把最近掌握的内容写一个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 datas;
private ArrayAdapter 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();
}
}
上下效果图最后: