上次写了自定义View,这次在项目中看到项目中的下拉刷新控件觉得很有趣便想模仿效果写一个出来,想了很多实现方案,最后选择自定义ViewGroup来实现。思路很简单,就是通过自定义一个ViewGroup,第一个View用来放我们的header,即下拉时要展现的View,第二个View则是我们需要展示的内容,可以是RecyclerView,也可以是我们想展现的内容ViewGroup。自定义ViewGroup需要重写两个方法,onMeasure和onLayout,其中onMeasure()是用来测量我们ViewGroup的大小以及内部子View的大小,onLayout则是控制我们的布局,即子控件的摆放位置。下面看代码:
public class Mypulltoview extends ViewGroup {
Scroller scroller;
int mylastmove = 0;
int myDown = 0;
int mymove = 0;
boolean isfirst = true;
RefreshListener listener;//刷新监听器
int symbolline = 0; //记录下拉滑动的距离,只有headerview完全展示时才开始刷新
public void setListener(RefreshListener listener) {
this.listener = listener;
}
public Mypulltoview(Context context) {
super(context);
scroller = new Scroller(context);
}
public Mypulltoview(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
}
public Mypulltoview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//onMeasure函数,若我们的控件都是固定值或match_parent时,系统提供了默认的实现方式,如果有wrap_content的情况,我们必须要重写该函数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);//即对子view进行测量,如果没有这一句,我们的子View不会显示。
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
//开始布局,headerview不显示,下拉时才显示
View view = getChildAt(0);
view.layout(0, -view.getMeasuredHeight(), view.getMeasuredWidth(), 0);
View view1 = getChildAt(1);
view1.layout(0, 0, view1.getMeasuredWidth(), view1.getMeasuredHeight());
}
//重点,onInterceptTouchEvent负责拦截触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
myDown = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mymove = (int) ev.getRawY();
//判断当前内容View是否还能继续向上滑动,利用ViewCompat的canScrollVertically判断view是否还能在竖直方向滑动,对应还有canScrollHorizontal
boolean a = getChildAt(1).canScrollVertically(-1);
Log.d("tag", "move");
//如过是下拉,并且内容View不能继续向上滑动,拦截Move事件
if (myDown - mymove < 0 && !a) {
//Log.d("tag",getChildAt(1).getTop()+"");
mylastmove=mymove;
return true;
}
break;
case MotionEvent.ACTION_UP:
Log.d("tag", "释放");
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mymove = (int) event.getRawY();
int scrolly = mylastmove - mymove;
symbolline += Math.abs(scrolly / 2);
Log.d("tag", symbolline + "滑动距离");
Log.d("tag", getChildAt(0).getMeasuredHeight() + "第一个view高度");
scrollBy(0, scrolly / 2);
//判断下拉距离是否大于headerview的高度,是的话开始刷新动画
if (symbolline > getChildAt(0).getMeasuredHeight() && isfirst) {
listener.startRefresh();
isfirst = false;
}
mylastmove = mymove;
break;
case MotionEvent.ACTION_UP:
if (!isfirst) {
//用户手指抬起时,需要弹回一段距离,回到足够显示headrview的高度即可
scroller.startScroll(0, getScrollY(), 0, -getScrollY() - getChildAt(0).getMeasuredHeight());
isfirst = true;
listener.startUpdate();
} else {
scroller.startScroll(0, getScrollY(), 0, -getScrollY());
}
Log.d("tag", "up" + getScrollY());
symbolline = 0;
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
//必须重写该方法。其实就是不做处理即可,因为在recyclerview中,如果recyclerview监听到自己在滑动时,会调用该父View的该方法来禁用父view拦截事件的功能,这样我们就无法监听了,因此在这里我们不做处理,写为空。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
//更新成功时调用的方法。
public void success() {
scroller.startScroll(0, getScrollY(), 0, -getScrollY());
Log.d("tag", "up" + getScrollY());
invalidate();
}
}
上面就是我们的ViewGroup,注释也写在代码里,接下来看我的刷新view,其实就是一朵小红花不停的旋转。
public class RefreshView extends LinearLayout {
ImageView mImageView;
Context mContext;
ObjectAnimator animator;
public RefreshView(Context context) {
super(context);
init(context);
}
public RefreshView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RefreshView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void init(Context context) {
mContext = context;
this.setGravity(VERTICAL);
mImageView = new ImageView(mContext);
mImageView.setImageResource(R.drawable.redflower);
LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
addView(mImageView, params);
}
public void startAnimation() {
Log.d("tag", "执行动画");
animator = ObjectAnimator.ofFloat(mImageView, "rotation", 0, 359);
animator.setDuration(1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(10);
animator.start();
}
public void stopAnimation() {
animator.cancel();
}
}
利用属性动画来达到旋转的效果,哦需要注意的是为了能够看起来是在不停旋转,没有停顿,我们旋转的角度应该是0——359,并且由于属性动画的旋转动画默认并不是匀速的,因此我们需要使用匀速插值器。在这里有个坑(animator.setRepeatCount()不能设置Interger.MaxValue),我不清楚是为什么,后面在研究一下。
接下来就是MainActivity,主要就是模仿了一个数据刷新的过程。
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
List<String> list=new ArrayList<>();
Mypulltoview mypulltoview;
RefreshView refreshView;
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
if (msg.what==1){
recyclerView.setAdapter(new MyAdapter(list));
refreshView.stopAnimation();
mypulltoview.success();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView=findViewById(R.id.myrecycler);
for (int i=0;i<20;i++){
list.add("小明"+i);
}
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new MyAdapter(list));
mypulltoview=findViewById(R.id.mypullview);
refreshView=findViewById(R.id.myrefresh);
mypulltoview.setListener(new RefreshListener() {
@Override
public void startRefresh() {
refreshView.startAnimation();
}
@Override
public void successrefresh() {
refreshView.stopAnimation();
mypulltoview.success();
}
@Override
public void startUpdate() {
new Thread(new Runnable() {
@Override
public void run() {
try {
//更新逻辑
Thread.sleep(2000);
Collections.reverse(list);//反转list
//这里完全可以用runOnUIThread哈,只是我想温习下handler的用法
Message message=new Message();
message.what=1;
handler.sendMessage(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
});
}
class MyAdapter extends RecyclerView.Adapter{
List<String> list=new ArrayList<>();
public MyAdapter(List<String> list){
this.list=list;
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textView=itemView.findViewById(R.id.mytext);
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view= LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mylayoutitem,viewGroup,false);
ViewHolder holder=new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
ViewHolder holder= (ViewHolder) viewHolder;
holder.textView.setText(list.get(i));
}
@Override
public int getItemCount() {
return list.size();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
refreshView.stopAnimation();
}
}
好吧,本篇就到这,虽然有很多坑,但自己写一遍还是收益匪浅。