android-SwipeLinearLayout解析及使用

本文介绍了SwipeLinearLayout的工作原理和使用方法,包括如何创建此类、处理触摸事件、拦截父视图的touch事件以及实现自动滑动动画。通过分析onLayout、onTouchEvent和onInterceptTouchEvent方法,理解其在滑动操作中的角色。同时,文章提供了使用SwipeLinearLayout构建实际项目的步骤,包括创建布局、适配器和最终效果展示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

工作中需要用到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);

    }
}

效果如图所示:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值