工作中需要用到SwipeLinearLayout这个框架,以前一直都没有用过,今天来学习一下。
首先,去github下这个demo。
跟着demo来边写边分析。
首先,创建一个SwipeLinearLayout类继承LinearLayout。
package com.mystudy.kibi.swipelinearlayout;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
/**
* Created by Nicole on 16/9/25.
*/
public class SwipeLinearLayout extends LinearLayout {
public SwipeLinearLayout(Context context) {
super(context);
}
public SwipeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SwipeLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
然后重写onLayout方法,获取第一层view的宽度以及第二层view的宽度。
// 左边部分, 即从开始就显示的部分的长度
int width_left = 0;
// 右边部分, 即在开始时是隐藏的部分的长度
int width_right = 0;
/**
* 该方法在ViewGroup中定义是抽象函数,继承该类必须实现onLayout方法
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
View left = getChildAt(0); //获取第一层View
View right = getChildAt(1); //获取第二层View
width_left = left.getMeasuredWidth();
width_right = right.getMeasuredWidth();
}
接下来重写它的触摸事件
float lastX;
float lastY;
float startX;
float startY;
boolean hasJudged = false;
boolean ignore = false;
static float MOVE_JUDGE_DISTANCE = 5;
/**
* 触摸事件
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// getAction:触摸动作的原始32位信息,包括事件的动作,触控点信息
// getActionMask:触摸的动作,按下,抬起,滑动,多点按下,多点抬起
// getActionIndex:触控点信息
switch (ev.getActionMasked()){
case MotionEvent.ACTION_DOWN:
/**
* 拦截父层的View截获touch事件
*/
hasJudged = false; //不允许判断
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float curX = ev.getX();
float cutY = ev.getY();
if(hasJudged == false){ //相当于一个触摸监听的锁
float dx = curX - startX;
float dy = cutY - startY;
if(dx * dx + dy * dy > MOVE_JUDGE_DISTANCE * MOVE_JUDGE_DISTANCE){
if(Math.abs(dy) > Math.abs(dx)){ //基本就是listview上下滑动,允许父层的View截获touch事件被父层拦截touch事件
/**
* 允许父层的View截获touch事件
*/
/**
* 是否有停止监听事件
*/
} else {
/**
* 监听事件,让这个框架干什么
*/
lastX = curX;
lastY = cutY;
}
hasJudged = true;
ignore = true;
}
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
接下来,看一下拦截或允许父层的View截获touch事件怎么实现的。
看demo中的关键方法是requestDisallowInterceptTouchEvent(boolean b)。
百度搜了一下相关信息:
当手指触摸到屏幕时,系统就会调用相应View的onTouchEvent,并传入一系列的action。当有多个层级的View时,在父层级允许的情况下,这个action会一直向下传递直到遇到最深层的View。所以touch事件最先调用的是最底层的onTouchEvent,如果View的onTouchEvent接收到某个touch action并作了相应处理,最后有两种返回方式return true和return false;return true会告诉系统当前的View需要处理这次的touch事件,以后的系统发出的ACTION_MOVE,ACTION_UP还是需要继续监听并接收的,而且这次的action已经被处理掉了,父层的View是不可能出发onTouchEvent了。所以每一个action最多只能有一个onTouchEvent接口返回true。如果return false,便会通知系统,当前View不关心这一次的touch事件,此时这个action会传向父级,调用父级View的onTouchEvent。但是这一次的touch事件之后发出的任何action,该View都不会再接受,onTouchEvent在这一次的touch事件中再也不会触发,也就是说一旦View返回false,那么之后的ACTION_MOVE,ACTION_UP等ACTION就不会在传入这个View,但是下一次touch事件的action还是会传进来的。
前面说了底层的View能够接收到这次的事件有一个前提条件:在父层级允许的情况下。假设不改变父层级的dispatch方法,在系统调用底层onTouchEvent之前会先调用父View的onInterceptTouchEvent方法判断,父层View是不是要截获本次touch事件之后的action。如果onInterceptTouchEvent返回了true,那么本次touch事件之后的所有action都不会再向深层的View传递,统统都会传给父层View的onTouchEvent,就是说父层已经截获了这次touch事件,之后的action也不必询问onInterceptTouchEvent,在这次的touch事件之后发出的action时onInterceptTouchEvent不会再次调用,知道下一次touch事件的来临。如果onInterceptTouchEvent返回false,那么本次action将发送给更深层的View,并且之后的每一次action都会询问父层的onInterceptTouchEvent需不需要截获本次touch事件。只有ViewGroup才有onInterceptTouchEvent方法,因为一个普通的View肯定是位于最深层的View,touch事件能够传到这里已经是最后一站了,肯定会调用View的onTouchEvent。
对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action。
总之,这个拦截父层的View截获touch事件的方法应该就是为了防止滑动的时候出现冲突的情况。
/**
* 拦截父层的View截获touch事件
*/
private void disAllowParentsInterceptTouchEvent(ViewParent parent){
if( null == parent){
return; //没有父层,那么直接return出去
}
parent.requestDisallowInterceptTouchEvent(true); //设置不允许
disAllowParentsInterceptTouchEvent(parent.getParent()); //一层一层向上设置
}
/**
* 允许父层的View截获touch事件
*/
private void allowParentsInterceptTouchEvent(ViewParent parent){
if( null == parent){
return; //没有父层,那么直接return出去
}
parent.requestDisallowInterceptTouchEvent(false); //设置允许
allowParentsInterceptTouchEvent(parent.getParent()); //一层一层向上设置
}
之后是重写onInterceptTouchEvent(MotionEvent ev)方法。
说实话,到这里我开始对onInterceptTouchEvent()和onTouchEvent()两个方法有点晕了。
百度搜了一下:
onInterceptTouchEvent()是用于处理事件(重点onInterceptTouchEvent这个事件是从父控件开始往子控件传的,直到有拦截或者到没有这个事件的view,然后就往回从子到父控件,这次是onTouch的)(类似于预处理,当然也可以不处理)并改变事件的传递方向,也就是决定是否允许Touch事件继续向下(子控件)传递,一但返回True(代表事件在当前的viewGroup中会被处理),则向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()处理;返回false,则把事件交给子控件的onInterceptTouchEvent()。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(hasJudged){ //当被触摸滑动的时候 return true
return true; //把事件传递给当前的控件的onTouchEvent()处理
}
return super.onInterceptTouchEvent(ev);
}
onInterceptTouchEvent方法中return true,显然接下来就是要看onTouchEvent()是怎么处理的。
先来了解一下onTouchEvent()方法:
onTouchEvent()用于处理事件(重点onTouch这个事件是从子控件回传到父控件的,一层层向下传),返回值决定当前控件是否消费(consume)了这个事件,也就是说在当前控件在处理完Touch事件后,是否还允许Touch事件继续向上(父控件)传递。返回false,则向上传递给父控件,详细一点就是这个touch事件就给了父控件,那么后面的up事件就是到这里touch触发,不会在传给它的子控件。如果父控件依然是false,那touch的处理就给到父控件的父控件,那么up的事件处理都在父控件的父控件,不会触发下面的。返回true,如果是子控件返回true,那么它的touch事件都在这里处理,父控件是处理不了,因为它收不到子控件传给他的touch,被子控件给拦截了。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
lastX = event.getX(); // 获取屏幕的x坐标
lastY = event.getY(); // 获取屏幕的y坐标
startScrollX = getScrollX(); //getScrollX() 就是当前view(不是整个屏幕)的左上角相对于母视图的左上角的X轴偏移量。
break;
case MotionEvent.ACTION_MOVE:
if(ignore){
ignore = false; //就是在手指还在移动这快view的时候,拦截以下操作
break;
}
float curX = event.getX();
float dX = curX - lastX;
lastX = curX;
if(hasJudged){
int targetScrollX = getScrollX() + (int)(-dX); //计算出还需要自动滑动的距离
if(targetScrollX > width_right){
scrollTo(width_right,0); //如果计算出需要的距离比里层view宽度长,那么就自动滑动横向滑动width_right距离
} else if(targetScrollX < 0 ){
scrollTo(0,0); //如果计算出需要的距离比0小,那么就不需要自动滑动了,直接不展示里层view
} else {
scrollTo(targetScrollX,0); //如果算出的距离卡在这两个之中间,那么就自动滑动这些距离
}
}
break;
case MotionEvent.ACTION_UP:
float finalX = event.getX();
if(finalX < startX) {
scrollAuto(DIRECTION_EXPAND); // 展开里层view
} else {
scrollAuto(DIRECTION_SHRINK); // 不展示里层view
}
}
return true;
}
其中的scrollAuto(int i)的方法就是实现一个自动滑动动画的方法:
public void scrollAuto(final int direction){
int curScrollX = getScrollX(); //获取当前滑动的横向距离
if(direction == DIRECTION_EXPAND){
mScroller.startScroll(curScrollX , 0 , width_right - curScrollX , 0 ,300); //展开里层view,动画间隔0.3s
} else {
mScroller.startScroll(curScrollX , 0 , - curScrollX , 0 ,300); //不展示里层view,动画间隔0.3s
}
invalidate(); //利用invalidate()刷新界面
}
注意上面方法中有用到startScroll方法,那么就必须重写computeScroll()方法。
computeScroll:主要功能是计算拖动的位移量、更新背景、设置要显示的屏幕(setCurrentScreen(mCurrentScreen);)。
重写computeScroll()的原因:
调用startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,做出滚动的响应。
computeScroll在父控件执行drawChild时,会调用这个方法。
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){ //判断scroller的移动动画是否完成,当调用startScroll()方法的时候会true,scrollTo()或scrollBy时一直为false
this.scrollTo(mScroller.getCurrX(),0);
invalidate();
}
}
最后就是对外提供滑动监听的接口以及监听设置的方法了。
public void setOnSwipeListener(OnSwipeListener listener) {
this.onSwipeListener = listener;
}
public interface OnSwipeListener {
/**
* 手指滑动方向明确了
* @param sll 拖动的SwipeLinearLayout
* @param isHorizontal 滑动方向是否为水平
*/
void onDirectionJudged(SwipeLinearLayout sll, boolean isHorizontal);
}
接下来就是来利用这个SwipeLinearLayout来写一个demo。
首先创建一个listview的item布局。
<?xml version="1.0" encoding="utf-8"?>
<com.mystudy.kibi.swipelinearlayout.SwipeLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sll"
android:layout_width="match_parent"
android:layout_height="88dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffda82">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="bula~!bula~!"
android:gravity="center"
android:textSize="30sp"
android:textColor="#FFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="100dp"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/ll_right_top"
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_weight="1"
android:text="!!!!"
android:background="#a0ec8f"
android:gravity="center"
android:textSize="30sp"
android:textColor="#FFF" />
<TextView
android:id="@+id/ll_right_buttom"
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_weight="1"
android:text="---"
android:background="#8fcdec"
android:gravity="center"
android:textSize="30sp"
android:textColor="#FFF" />
</LinearLayout>
</com.mystudy.kibi.swipelinearlayout.SwipeLinearLayout>
接下来写listview的适配器
package com.mystudy.kibi.swipelinearlayout;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.mystudy.kibi.swipelinearlayout.SwipeLinearLayout.OnSwipeListener;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Nicole on 16/9/26.
*/
public class MyAdapter extends BaseAdapter implements OnSwipeListener {
private Context mContext;
private LayoutInflater inflater;
List<SwipeLinearLayout> swipeLinearLayouts = new ArrayList<>();
public MyAdapter(Context context) {
mContext = context;
inflater = LayoutInflater.from(mContext);
}
@Override
public int getCount() {
return 20;
}
@Override
public Object getItem(int i) {
return null;
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(final int i, View view, ViewGroup viewGroup) {
ViewHolder holder;
if(null == view){
view = inflater.inflate(R.layout.item,null);
holder = new ViewHolder(view);
swipeLinearLayouts.add(holder.sll);
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
// 初始化状态,滚到收缩状态
holder.sll.scrollTo(0,0);
final ViewHolder finalHolder = holder;
holder.linearLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, "click item " + i, Toast.LENGTH_SHORT).show();
finalHolder.sll.scrollAuto(SwipeLinearLayout.DIRECTION_SHRINK);
}
});
holder.textView_top.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, "!!!!! item " + i, Toast.LENGTH_SHORT).show();
finalHolder.sll.scrollAuto(SwipeLinearLayout.DIRECTION_SHRINK);
}
});
holder.textView_buttom.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, "----- item " + i, Toast.LENGTH_SHORT).show();
finalHolder.sll.scrollAuto(SwipeLinearLayout.DIRECTION_SHRINK);
}
});
return view;
}
@Override
public void onDirectionJudged(SwipeLinearLayout sll, boolean isHorizontal) {
if(!isHorizontal){ //不为横向滑动
for(SwipeLinearLayout swipeLinearLayout : swipeLinearLayouts){
if (null == sll) {
continue;
}
sll.scrollAuto(SwipeLinearLayout.DIRECTION_SHRINK); //全部不展示
}
} else {
for (SwipeLinearLayout swipeLinearLayout : swipeLinearLayouts) {
if (null == sll) {
continue;
}
if (!sll.equals(sll)) {
//划开一个sll, 其他收缩
sll.scrollAuto(SwipeLinearLayout.DIRECTION_SHRINK);
}
}
}
}
class ViewHolder{
SwipeLinearLayout sll;
LinearLayout linearLayout;
TextView textView_top,textView_buttom;
public ViewHolder(View v){
sll = (SwipeLinearLayout) v.findViewById(R.id.sll);
linearLayout = (LinearLayout) v.findViewById(R.id.ll_left);
textView_top = (TextView) v.findViewById(R.id.ll_right_top);
textView_buttom = (TextView) v.findViewById(R.id.ll_right_buttom);
sll.setOnSwipeListener(MyAdapter.this); //设置监听
}
}
}
最后只需要将适配器放入listview中就可以了。
package com.mystudy.kibi.swipelinearlayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ListView;
public class MainActivity extends AppCompatActivity {
private ListView listView;
private MyAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.list);
adapter = new MyAdapter(this);
listView.setAdapter(adapter);
}
}
效果如图所示: