仿微信朋友圈发表图片拖拽和删除功能

4,055 阅读10分钟

小窥朋友圈实现原理

我们使用Android Device Monitor来分析朋友圈发布图片的界面实现原理。如果需要分析其他应用的界面实现也是采用这种方法哦。

打开android Device Monitor,选择DDMS,连接上真机,区域2就会显示出当前手机正在运行的应用,再点击区域1,然后新窗口就会显示出当前页面分析的结果,点击区域3中的相应控件,区域4中就会选中对应的控件。区域4中以菜单的层级关系显示出各控件的关系。
这里写图片描述

区域4中各参数意义:
eg:(0)FrameLayout[0,0][720,1280]
对应意义:(序号)控件名[x位置,y位置][宽度,高度]

我们这里主要分析与图片拖拽相关的布局实现!通过4中的控件层级关系,我们可以画出这样一个大概布局图:
这里写图片描述
从图中可以看出,微信团队使用了自定义的 GridView 实现了图片的拖拽和删除功能。

准备

以上分析得出微信使用的是自定义 GridView 实现item拖拽和删除功能。xxx,现在都什么年代了,还使用 GridView 吗?当然是使用流行的 RecyclerView 实现啦。如果只是单纯地使用 RecyclerView 就能实现的话,那世界就和平。这里还需要使用ItemTouchHelper对拖拽进行处理。

ItemTouchHelper

官方解释:

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView 一个让 RecyclerView 支持滑动删除和拖拽的实用工具类

主要方法

//关联对应的 RecyclerView
public void attachToRecyclerView(RecyclerView recyclerView)

//viewHolder开始拖动
public void startDrag(RecyclerView.ViewHolder viewHolder)

//viewHolder开始滑动
public void startSwipe(RecyclerView.ViewHolder viewHolder)

使用

  1. 自定义一个类继承并实现ItemTouchHelper.Callback接口,以下方法必须实现:

    //设置item是否处理拖拽事件和滑动事件,以及拖拽和滑动操作的方向
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    }
    
    //当用户从item原来的位置拖动可以拖动的item到新位置的过程中调用
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
    }
    
    //滑动到消失后的调用
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    }
  2. 实现化ItemTouchHelper并关联RecyclerView

    itemTouchHelper = new ItemTouchHelper(myCallBack);
    itemTouchHelper.attachToRecyclerView(recyclerView);

ItemTouchHelper.Callback

ItemTouchHelper在拖拽和滑动删除的过程中会回调ItemTouchHelper.Callback的相关方法

主要方法

//设置item是否处理拖拽事件和滑动事件,以及拖拽和滑动操作的方向
public int getMovementFlags (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)

/**
*当用户从item原来的位置拖动可以拖动的item到新位置的过程中调用
*@recyclerView 
*@viewHolder 拖动的 item
*@target 目标 item
**/
public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)

/**
* RecyclerView调用onDraw时调用,如果想自定义item对用户互动的响应,可以重写该方法
* @dx item 滑动的距离
**/
public void onChildDraw (Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)

//设置是否可以长按拖拽
public boolean isLongPressDragEnabled ()

//设置手指离开后ViewHolder的动画时间,在用户手指离开后调用
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy)

//当长按选中item的时候(拖拽开始的时候)调用
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)

//当用户与item的交互结束并且item也完成了动画时调用
public void clearView (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)

实现过程

先放上最终效果图:
这里写图片描述

  1. 布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context="com.kuyue.wechatpublishimagesdrag.PostImagesActivity">
    
    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="@dimen/article_post_delete"
        android:layout_alignParentBottom="true"
        android:background="@android:color/holo_red_light"
        android:gravity="center"
        android:text="@string/post_delete_tv_d"
        android:textColor="@color/white"
        android:visibility="gone" />
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rcv_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/article_post_et_h"
        android:paddingLeft="15dp" />
    
    <EditText
        android:id="@+id/et_content"
        android:layout_width="match_parent"
        android:layout_height="@dimen/article_post_et_h"
        android:background="@null"
        android:gravity="top"
        android:hint="分享有趣的事"
        android:inputType="textMultiLine"
        android:maxLength="140"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:paddingTop="10dp"
        android:textSize="14sp" />
    </RelativeLayout>
  2. 自定义MyCallBack类继承并实现ItemTouchHelper.Callbackr的以下方法:

设置 item 只能处理拖拽事件,并能够向左、右、上、下拖拽

@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    //判断 recyclerView的布局管理器数据
    if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
        dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;//设置能拖拽的方向
        swipeFlags = 0;//0则不响应事件
    }
    return makeMovementFlags(dragFlags, swipeFlags);
}

刷新 item

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
    int fromPosition = viewHolder.getAdapterPosition();//得到item原来的position
    int toPosition = target.getAdapterPosition();//得到目标position
    if (fromPosition < toPosition) {
        for (int i = fromPosition; i < toPosition; i++) {
            Collections.swap(images, i, i + 1);
        }
    } else {
        for (int i = fromPosition; i > toPosition; i--) {
            Collections.swap(images, i, i - 1);
        }
    }
    adapter.notifyItemMoved(fromPosition, toPosition);
    return true;
}

3. 关联 RecyclerView

itemTouchHelper = new ItemTouchHelper(myCallBack);
itemTouchHelper.attachToRecyclerView(rcvImg);

4. 调整布局
现在运行程序,界面如下:
这里写图片描述
手动黑人问号??? 仔细研究布局,原来 recyclerView 的高度设置成了wrap_content,因此无论怎么拖动,超出recyclerView的边界的部分就是不会显示滴!那就把高度设置成match_parent咯,重新调整后的布局如下:

<android.support.v7.widget.RecyclerView
    android:id="@+id/rcv_img"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="@dimen/article_post_et_h"
    android:paddingLeft="15dp" />

再运行一次,我xx,还是不行。这是什么鬼,我不是已经设置了match_parent了吗?这一定有黑科技!经过一番思索,想起了一个不是经常使用的属性:clipToPadding;设置android:clipToPadding=”false”,果然可以。
这里写图片描述
居然还有这种操作!

android:clipToPadding

Defines whether the ViewGroup will clip its children and resize (but not clip) any EdgeEffect to its padding, if padding is not zero. This property is set to true by default 设置ViewGroup是否剪切其子View,通俗点讲就是:是否允许ViewGroup在padding中绘制子View。默认情况下,此属性设置为true,即不允许;

另外一个和clipToPadding比较相似的属性是:clipChildren;

android:clipChildren

Defines whether a child is limited to draw inside of its bounds or not. This is useful with animations that scale the size of the children to more than 100% for instance. In such a case, this property should be set to false to allow the children to draw outside of their bounds. The default value of this property is true 一句话概括就是:是否允许子View超出父View

使用场景有:比如说外卖里的购物车效果
这里写图片描述
很明显画红色的地方是超出黑色的View,可以将黑色View设置android:clipChildren=”false”,这样子View红色部分就可以超出父View大小。

还发现了一个比较诡异的问题:blog.csdn.net/u013231041/…

设置最后一个item不能移动

由以上分析可知,ItemTouchHelper.Callback的isLongPressDragEnabled()可以设置是否支持长按拖拽,默认是true,即支持长按拖拽。现在我们要自定义指定哪些item可以拖拽,哪些不可以,因此我们需要重写isLongPressDragEnabled():

@Override
public boolean isLongPressDragEnabled() {
    return false;
}

取消了支持长按拖拽,就要自己处理RecyclerView的触摸事件,具体实现请参考源代码。 在长按事件触发的时候调用以下代码:

    //事件监听
    rcvImg.addOnItemTouchListener(new OnRecyclerItemClickListener(rcvImg) {
        @Override
        public void onItemClick(RecyclerView.ViewHolder vh) {
        }

        @Override
        public void onItemLongClick(RecyclerView.ViewHolder vh) {
            //如果item不是最后一个,则执行拖拽
            if (vh.getLayoutPosition() != dragImages.size() - 1) {
                itemTouchHelper.startDrag(vh);
            }
        }
    });

最后还需要在ItemTouchHelper.Callback的onMove()中添加以下代码

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        int fromPosition = viewHolder.getAdapterPosition();//得到item原来的position
        int toPosition = target.getAdapterPosition();//得到目标position
        if (toPosition == images.size() - 1 || images.size() - 1 == fromPosition) {
            return true;
        }
        ···
        return true;
    }

还有要在clearView()方法里去notifyDataSetChanged,不然 item的position是没有交换的

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        adapter.notifyDataSetChanged();
    }

实现拖拽到底部放手删除功能

item从拖动到放手的主要处理流程图如下;
这里写图片描述
那么我们应该在哪个地方去判断item是否到达删除区域呢?在前面介绍的方法,有这么个方法:onChildDraw,这个方法就会在item拖拽的过程不断回调并且返回item的偏移量。有了偏移量之后我们就很容易去判断item是否到达删除区域了。
偏移量满足以下条件时,就到达删除区域:
item偏移量>=RecyclerView的高-item底部到RecyclerView顶边的距离-TextView的高
这里写图片描述
问题来了,我们怎样判断用户在拖动后放手呢?
我们用boolean up来标记,当up为true时手指抬起,false为初始状态。在getAnimationDuration()中设置其为true。记得需要在clearView()中恢复初始值false;
还需要在ItemTouchHelper.Callback中暴露个接口DragListener给外部,用来提示通知外部什么时候显示删除区域,以及item进入删除区域时的文字提示。
相关代码如下:

//自定义拖动与滑动交互
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (null == dragListener) {
        return;
    }
    dragListener.dragState(true);//显示删除区域

    if (dY >= (recyclerView.getHeight()
            - viewHolder.itemView.getBottom()//item底部距离recyclerView顶部高度
            - CommonUtils.getPixelById(R.dimen.article_post_delete))) {//拖到删除处
        dragListener.deleteState(true);
        if (up) {//在删除处放手,则删除item
            viewHolder.itemView.setVisibility(View.INVISIBLE);//先设置不可见,如果不设置的话,会看到viewHolder返回到原位置时才消失,因为remove会在viewHolder动画执行完成后才将viewHolder删除
            images.remove(viewHolder.getAdapterPosition());
            adapter.notifyItemRemoved(viewHolder.getAdapterPosition());
            initData();
            return;
        }
    } else {//没有到删除处
        if (View.INVISIBLE == viewHolder.itemView.getVisibility()) {//如果viewHolder不可见,则表示用户放手,重置删除区域状态
            dragListener.dragState(false);
        }
        dragListener.deleteState(false);
    }
    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

@Override
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
    //手指放开
    up = true;
    return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}


//当长按选中item的时候(拖拽开始的时候)调用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
    if (ItemTouchHelper.ACTION_STATE_DRAG == actionState && dragListener != null) {
        dragListener.dragState(true);
    }
    super.onSelectedChanged(viewHolder, actionState);
}

@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    super.clearView(recyclerView, viewHolder);
    adapter.notifyDataSetChanged();
    initData();
}

/**
 * 重置
 */
private void initData() {
    if (dragListener != null) {
        dragListener.deleteState(false);
        dragListener.dragState(false);
    }
    up = false;
}

interface DragListener {
    /**
     * 用户是否将 item拖动到删除处,根据状态改变颜色
     *
     * @param delete
     */
    void deleteState(boolean delete);

    /**
     * 是否于拖拽状态
     *
     * @param start
     */
    void dragState(boolean start);
}

图片相关

一看到图片或者说看到Bitmap,会联想到什么?没错就是OOM!!!对于Bitmap导致的OOM要解决它是很有套路可言的。我们都知道图片压缩分为两种:质量压缩和尺寸压缩。可能有些人对这两个概念不是很清楚!他们的区别是质量压缩并不会改变图片的尺寸,而尺寸压缩则会改变图片的尺寸。当把以文件形式存在在硬盘上的图片,以Bitmap的形式加载到内存中的时候,我就必须进行尺寸压缩,尺寸压缩可以减少内存占用,这样就解决了OOM。详细原理参考文章: blog.csdn.net/tyk0910/art…。对于质量压缩?主要用在图片传输上,用于提高传输速率,就像我们朋友圈发布图片时,它会对图片质量进行压缩,然后再传到后台,所以你在朋友圈看到的图片都不是原图片,都是经过压缩过的。这里推荐一个质量压缩效果和微信差不多的开源库github.com/Curzibn/Lub…。文末demo已实现图片尺寸压缩,即解决了OOM,也解决了拖拽不流畅问题。

总结

有时候我们看到一个觉得酷炫的功能,很想去实现他。这时候不必匆匆下手。我们自己可以先想一下要是我自己会怎样去实现,然后再用下Android Device Monitor大概分析下,更深一步可反编译一下看下源码,看他是怎样实现的,分析两者的优劣。对于这种情形:一个View1根据其他View2操作的手势或状态的改变而改变的,可以根据这样的套路去想:这个View2是否有提供这种状态反馈的方法什么的,我是否能把这个反馈传到View1等。在不确定这个 View2是否有你想要的方法时,请查看官方文档,请查看官方文档,请查看官方文档,重要的事件说三遍!比如我的删除区域是根据item的拖拽状态和距离去改变的,那么你就要去找ItemTouchHelper.Callback给我提供了什么回调啊,哪些方法可以返回这些东东啊,最后我就写个接口给通知外部。

demo地址:github.com/kuyue/WeCha…

感谢阅读!欢迎拍砖。