这里我不直接贴大段代码,不直接讲用的什么什么技术,什么什么框架,从设计思路出发,到难点剖析,以最基础也是最快速的方式搞定他。(这里未借鉴任何网上代码,全是自己想自己手撸的,其实真的不难)
思路:
你需要重写哪个控件?
是recyclerview还是包含item的relativelayout或者是lineralayout?我个人比较崇尚重用性,为了在任何下拉控件中都能使用,我选择了重写item。而且重写有个很严重的问题,他本身就自带很多手势,你再添加手势,不崩就已经很好了。
重写item的具体思路?
这里的黄色区域就是我们重写的relativelayout,蓝色的就是代表删除按钮textview,也可以是button。最外面的布局是FrameLayout,因为这个布局可以实现后面的控件覆盖前面的控件,看代码。
这样显示出来的是上面那个删除按钮被下面的总布局覆盖了。<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="80dp"> <TextView android:id="@+id/tv_delete" android:layout_width="80dp" android:layout_height="match_parent" android:layout_gravity="end" android:background="@color/colorAccent" android:text="删除"/> <com.example.test.HorizontalSlideView android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF"/> </com.example.test.HorizontalSlideView> </FrameLayout>
课外知识:为什么用relativelayout?
因为他在控件多的时候,可以通过比linearlayout更精确的定位规则,减少嵌套,就是不在relativelayout里嵌套另外的布局就可以轻松实现定位,嵌套一少的话性能就会大大增加,因为在视图绘制的时候,层级越深,需要的性能是更多更多的。
如何重写这个relativelayout?
你需要先学会实现拖动功能,就是学会自定义一个可拖动的控件,我们这里举个例子。
我们唯一需要做的就是,1.创建一个继承自relativelayout的类
2.重写onTouchEvent,这个是监听手势的。
注释的很清楚,代码也简单,可以迅速弄懂。@Override public boolean onTouchEvent(MotionEvent event) { //这是可以获取手势的方法,我们需要重写它,它来自View类 int x = (int) event.getX();//event就是类似手势信息了,这里获取了你触摸的X坐标 int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN://这里写你的触摸时按下那一刻的操作 lastX = x;//我们这里记录下手指初始的落点 lastY = y; break; case MotionEvent.ACTION_MOVE://这里写你的手指在移动时的操作,这个方法在移动中会执行很多次 int offsetX = x - lastX;//这里是手指的当前位置 和 初始落点之差 通过偏移量来改变位置 int offsetY = y - lastY; //getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); break; } return true; }
这个控件的全部代码
public class TestView extends RelativeLayout{ public TestView(Context context) { super(context); } public TestView(Context context, AttributeSet attrs) { super(context, attrs); } public TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } int lastX; int lastY; @Override public boolean onTouchEvent(MotionEvent event) { //这是可以获取手势的方法,我们需要重写它,它来自View类 int x = (int) event.getX();//event就是类似手势信息了,这里获取了你触摸的X坐标 int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN://这里写你的触摸时按下那一刻的操作 lastX = x;//我们这里记录下手指初始的落点 lastY = y; break; case MotionEvent.ACTION_MOVE://这里写你的手指在移动时的操作,这个方法在移动中会执行很多次 int offsetX = x - lastX;//这里是手指的当前位置 和 初始落点之差 通过偏移量来改变位置 int offsetY = y - lastY; //getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); break; } return true; } }
这个控件在xml中的使用代码(注意,这里不要粘贴,你的这个包名<com.example.test.TestView不能跟我一样,按照你建的路径来)
<com.example.test.TestView android:layout_width="30dp" android:layout_height="30dp" android:background="@color/colorAccent"/>
效果
这个会了已经是成功了一半了。因为我们的需求是水平滑动,所以你下一步需要把它修改为仅限于水平范围内的滑动。
//getLeft是你这个控件左边的横坐标,以此类推,通过不断把差值赋给layout,来重新放置你这个控件 //全屏幕滑动 //layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); //水平滑动 layout(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());你只需要修改一处代码就可以了,就是去掉offsetY,很简单。
效果
接下来要学会在指定区域内滑动
因为你要实现的是把它挪开,让删除按钮暴露出来,所以你滑多了不行,向右滑也不行。
所以怎么才能实现在指定范围内滑动呢?
1.你需要获取你这个自定义控件的宽度
2.你需要获取那个删除按钮的宽度
3.给layout加一个判断语句即可
获取你这个自定义控件的宽度
定义一个成员变量,十分简单
//全长 ITEM_WIDTH = getWidth();
获取删除按钮的宽度
这个不是太简单,因为这个控件在我们上面展示xml布局的FragmLayout里,所以需要先从当前控件,获取这个FrameLayout,再根据这个FrameLayout去获取这个删除按钮即可
//按钮宽度 //获取父亲,这是view类里的方法,即FrameLayout ViewGroup vg = (ViewGroup) getParent(); //由FrameLayout获取儿子,即那个删除按钮,为什么传入的是0,你可以看一下上面我们xml那个布局的代码先后顺序 View v = vg.getChildAt(0); DELETE_BUTTON_WIDTH = v.getWidth();
所以这样去限定范围
//水平滑动并且在一定范围内 if (getRight() + offsetX < ITEM_WIDTH && getRight() + offsetX >= ITEM_WIDTH - DELETE_BUTTON_WIDTH) { layout(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom()); }
我们这里为了测试,就简单限定了一下范围,300到500之间
效果图
好了,到此为止,已经可以说是做完了!其实并不难!接下来再抠一两个细节就完工了。
第一个:你可以看一下你的QQ,滑的过程中松手了,如果你的右边缘在删除按钮的右半部分,就会复位,如果你的右边缘在删除按钮的左半部分,就会自动完成剩下的位移。
怎么实现?
我们需要给手势添加一个
case MotionEvent.ACTION_UP:这个指代的是当你触摸完后,手指离开触摸屏的那一刹那,可以看看我之前几张gif,就是我松开鼠标的那一刻。
有了这个思路就清晰了,用我们之前的
getRight()
获取右边缘位置,判断在哪个区间就可以了,非常简单。
代码如下
case MotionEvent.ACTION_UP: //向右复位 if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) { ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start(); //向左复位 } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) { ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start(); } break;
上个坐标图
就是根据坐标来写if语句的。
然后if语句里面的
ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start();就是属性动画,非常简单好用,我来讲一下参数。
this:this就是当前这个类,就是这个自定义控件
translationX就代表动画执行的是X轴上平移的操作
0就是从你的当前坐标开始动,如果你设置另外的坐标可能不会太好
ITEM_WIDTH - getRight()就是你的移动距离 我们这里的是向右复位操作,这个距离很容易的出来。
setDuration(600)就代表动画执行时间是600ms
最后start启动动画
为什么我们要用 属性动画 实现位置的复原而不是用之前的layout来重新放置控件的位置?
因为自动复位不再是你的手拖着控件走了,而是他自己复原,如果你用layout,会瞬间回到原点,会造成非常不友好的用户体验。
(别人说的用Scroller也不是不可以,但是那个也是基于的动画而且相比属性动画较为复杂,所以属性动画要强大简单的多)
所以现在的效果图是这样的(不再是那个测试用例了,请读者注意,布局变成最开始的xml布局了)
这个搞定了以后,我们剩下了最后一个难点,如何实现同时只能滑动一个。
为什么难?
因为我们重写的是item,我们手哪里有这么长去管到外面的事情?我想了很多方法,观察者,接口回调,发消息,单例模式等等等。而且我看了网上别人的实现,大部分都写在viewholder里,我觉得耦合度太高了,所以我采用了一个简单的办法
内部维护一个当前自定义控件的实例。
听起来很高大上,其实就是在MainActivity中设置两个静态变量即可。
public static HorizontalSlideView current; public static boolean isDeleteIconShown;再发一下控件中增加的代码,仔细讲一讲这个还算不错的方法。
case up中增加了2行代码
case MotionEvent.ACTION_UP: //向右复位 if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) { ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start(); //向左复位 } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) { ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start();MainActivity.isDeleteIconShown = true;MainActivity.current = this; } break;
在完成向左复位的时候,我们是松开了手,未滑完的动画帮你完成了,这个时候自然是处于已显示状态了。
所以第一行:把已显示状态置为true
第二行:把你这个自定义控件类当前的实例传进去。是和MainActivity中的量是一一对应的的。
最后在case down里增加一个判断就完工了
case MotionEvent.ACTION_DOWN: if (isDeleteIconShown) { if (MainActivity.current == this) { ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start(); isDeleteIconShown = false; } return false; } lastX = x; lastY = y; break;如果当前状态是已显示状态,就要进入这个if。
再进行判断,我们的这个实例是不是和MainActivity中的实例一样(这样==的比较,实际在比较两个实例的空间地址是否一样,所以如果是不同的实例,自然是不一样的)
最后那个return false就是拦截你后面的操作,不让后面的操作生效。这是涉及到了Android事件分发机制,如果有兴趣的可以看看我的另外一篇文章http://blog.csdn.net/qq_36523667/article/details/78627595
也是一目了然。
好了,大功告成。掌握了方法步骤,接下来你们就可以随意地定制了!下面贴上全部代码和xml
自定义控件
public class HorizontalSlideView extends RelativeLayout { public HorizontalSlideView(Context context) { super(context); } public HorizontalSlideView(Context context, AttributeSet attrs) { super(context, attrs); } public HorizontalSlideView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public HorizontalSlideView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } int lastX; int lastY; int ITEM_WIDTH; int DELETE_BUTTON_WIDTH; @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isDeleteIconShown) { if (MainActivity.current == this) { ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start(); isDeleteIconShown = false; } return false; } lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX; int offsetY = y - lastY; //全长 ITEM_WIDTH = getWidth(); //按钮宽度 ViewGroup vg = (ViewGroup) getParent(); DELETE_BUTTON_WIDTH = ((ViewGroup) getParent()).getChildAt(0).getWidth(); if (getRight() + offsetX < ITEM_WIDTH && getRight() + offsetX >= ITEM_WIDTH - DELETE_BUTTON_WIDTH) { layout(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom()); } break; case MotionEvent.ACTION_UP: //向右复位 if (getRight() <= ITEM_WIDTH && getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH / 2) { ObjectAnimator.ofFloat(this, "translationX", 0, ITEM_WIDTH - getRight()).setDuration(600).start(); //向左复位 } else if (getRight() >= ITEM_WIDTH - DELETE_BUTTON_WIDTH && getRight() < ITEM_WIDTH - (DELETE_BUTTON_WIDTH / 2)) { ObjectAnimator.ofFloat(this, "translationX", 0, -(getRight() - (ITEM_WIDTH - DELETE_BUTTON_WIDTH))).setDuration(600).start(); isDeleteIconShown = true; MainActivity.current = this; } break; } return true;//这就是继续要使用触摸指令,如果false 将继续寻找要使用它的人 } }
MainActivity
public class MainActivity extends AppCompatActivity { @BindView(R.id.rv) RecyclerView mRv; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); LinearLayoutManager manager = new LinearLayoutManager(this); CommonAdapter<String> adapter = new CommonAdapter<String>( this, R.layout.item, initList("开源资讯", "推荐博客", "技术问答", "每日一博")) { @Override protected void convert(final ViewHolder holder, String s, int position) { TextView mTv = holder.getView(R.id.tv_main); mTv.setText(s); } }; mRv.setLayoutManager(manager); mRv.setAdapter(adapter); } public static HorizontalSlideView current; public static boolean isDeleteIconShown; private <T> List<T> initList(T... ts) { List<T> list = new ArrayList<>(); Collections.addAll(list, ts); return list; } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v7.widget.RecyclerView android:id="@+id/rv" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
item.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="80dp"> <TextView android:id="@+id/tv_delete" android:layout_width="80dp" android:layout_height="match_parent" android:layout_gravity="end" android:background="@color/colorAccent" android:text="删除"/> <com.example.test.HorizontalSlideView android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF"/> </com.example.test.HorizontalSlideView> </FrameLayout>