单词拖动

作者: 有点健忘 | 来源:发表于2018-09-06 13:48 被阅读23次

    不知道在哪里看到过一个需求,具体的也记不清了,有点久了,现在抽空写下效果。
    简单需求如下,给一个单词,给一个句子,然后把单词拖动到正确的位置。拖动结束,判断下位置是否正确,正确的话单词显示绿色,不正确的话单词显示红色。然后显示正确的句子以及释义。
    注:如果用户在无效范围内松开手指,那么还原即可,还可以重新拖动。

    长按开始拖动。
    20180906_132653.gif

    最开始的思路

    最开始我是把选项单词和下边的句子分开弄做2个控件的,单独处理了上边的触摸事件,完事修改下边的控件,可发现如果对下边的控件进行invalidate的话,上边拖动的view的触摸事件就没了。

    新的思路

    平时用recyclerview,这玩意感觉就是万能的啊,那何不用这个来写,大家就是一个view了,事件也好处理。

    第一步,先自定义一个LayoutManager 把第一幅图的效果弄出来

    这个还是比较简单的。第一个item单独一行居中,上下留点间距,完事从第二个item开始大家就按顺序一排一排的添加即可。
    暂时不考虑滑动,复用的问题,因为这里的需求基本就是一个页面显示的,也没有滚动和复用的必要。

    代码如下WordsLayoutManager.kt

    import android.support.v7.widget.RecyclerView
    import android.view.ViewGroup
    
    class WordsLayoutManager:RecyclerView.LayoutManager(){
        override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
            return RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    
        override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
            super.onLayoutChildren(recycler, state)
            if(state.itemCount==0){
                removeAndRecycleAllViews(recycler)
            }
            if (childCount == 0 && state.isPreLayout) {
                return
            }
            detachAndScrapAttachedViews(recycler)
            var left=paddingLeft
            var top=paddingTop
            repeat(state.itemCount){
                val child=recycler.getViewForPosition(it)
                addView(child)
                measureChildWithMargins(child,0,0)
                val childWidth=getDecoratedMeasuredWidth(child)
                val childHeight=getDecoratedMeasuredHeight(child)
                if(it==0){
                    left=(width-childWidth)/2//第一个item居中显示
                    top=paddingTop+childHeight*2//默认第一个item上下的间距都是item高度的2倍
                }else if(it==1){
                    left=paddingLeft
                    top=paddingTop+childHeight*5
                }else{
                    if(left+childWidth>width-paddingRight){//开始添加之前得先判断下添加以后是否跑到屏幕外边了,如果超出了,那就换行展示
                        left=paddingLeft
                        top+=childHeight //我们这里文字是单行的,所以一行多个元素,高度是一样的,就不处理了。
                    }
                }
                layoutDecoratedWithMargins(child,left,top,left+childWidth,top+childHeight)
                if(it>0){
                    left+=childWidth //添加完child以后,计算新的left位置
                }
            }
    
        }
    
    }
    

    使用起来也就比较简单了,adapter自己写,item弄个textview就完事了。

     layoutManager=WordsLayoutManager()
     addItemDecoration(ItemDecorationSpace())
    //decoration就简单写了个,我手机1dp=1px,所以偷懒不考虑换算了,直接数字就上了。
     outRect.left=5
            outRect.right=5
            outRect.bottom=20
    

    好了,第一步展示就完工了,下边考虑拖动

    拖动,就用系统的ItemTouchHelper

    注释都很清楚了,简单说下数据,我是假设把所有单词一次都给到一个数组里,第一个单词就是可以拖动的选项,然后还得给正确答案应该插在哪个位置,这是按照给的单词来讲的。
    比如给了单词 a,b,c,d,e 其中第一个a就是要拖动的,答案如果是3的话,那么就是插入c和d之间。具体逻辑实际情况修改。拖动逻辑都在ItemTouchHelper的callback里了。

    最后的代码如下

    单词实体类,就一个word和一个boolean值【用来判断是否是插入的数据】。

    data class WordBean(var word:String,var inserted:Boolean=false)
    
    import android.graphics.Canvas
    import android.graphics.Color
    import android.os.Bundle
    import android.support.v7.widget.RecyclerView
    import android.support.v7.widget.helper.ItemTouchHelper
    import android.view.View
    import android.widget.TextView
    import com.charliesong.demo0327.base.BaseActivity
    import com.charliesong.demo0327.base.BaseRvAdapter
    import com.charliesong.demo0327.base.BaseRvHolder
    import com.charliesong.demo0327.R
    import kotlinx.android.synthetic.main.activity_words.*
    import java.util.*
    
    /**
     * Created by charlie.song on 2018/4/28.
     */
    class ActivityWords : BaseActivity() {
        var rightPosition = 3;//正确答案的位置,假设是这个。
        var choice = -1;//这是松开手指的时候最终选择的位置索引
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_words)
    
    
            var wordsOriginal = arrayListOf<WordBean>()
            wordsOriginal.add(WordBean("He"))
            wordsOriginal.add(WordBean("must"))
            wordsOriginal.add(WordBean("been"))
            wordsOriginal.add(WordBean("single"))
            wordsOriginal.add(WordBean("activity"))
            wordsOriginal.add(WordBean("fragment"))
            wordsOriginal.add(WordBean("honey"))
            wordsOriginal.add(WordBean("restaurant"))
            rv_words.apply {
                layoutManager = WordsLayoutManager()
                addItemDecoration(ItemDecorationSpace())
                adapter = object : BaseRvAdapter<WordBean>(wordsOriginal) {
                    override fun getLayoutID(viewType: Int): Int {
                        return R.layout.item_simple_word
                    }
    
                    override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
                        holder.itemView.layoutParams = RecyclerView.LayoutParams(-2, -2)
                        var bean = getItemData(position)
                        holder.getView<TextView>(R.id.tv_word).apply {
                            text = bean.word
                            visibility = if (bean.inserted) View.INVISIBLE else View.VISIBLE
                            if (choice == position) {//choice是我们最终选择的位置,这里处理下颜色,对的话为绿色,错的话为红色,
                                visibility = View.VISIBLE
                                setBackgroundColor(if (rightPosition == position) Color.GREEN else Color.RED)
                            }
                        }
                    }
                }
            }
    
            var helper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
                var temp = -1
                override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
    
                }
    
                //这里用来判断处理哪些情况
                override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
                    var position = viewHolder.adapterPosition
                    if (position != 0 || wordsOriginal[position].inserted) {//
                        return 0   //只有第一个可以拖动,另外拖动过之后也不能再次拖动了,其实第一个也就隐藏了
                    }
                    val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END  //这个是拖动的flag
                    return makeMovementFlags(dragFlags, 0)
                }
    
                //拖拽的时候会不停的回掉这个方法,我们在这里做的就是交换对应的数据
                override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                    var oldPositioon = viewHolder.adapterPosition
                    var newPosition = target.adapterPosition
                    if (temp < 0) {//首次拖动到有效范围的时候temp肯定是-1拉,这时候我们在拖动的位置添加一条选项数据,也就是第一条数据。
                        wordsOriginal.add(newPosition, wordsOriginal.get(0).apply { inserted = true })//为啥inserted=true,是为了使这个item不可见,只用来占位
                        recyclerView.adapter.notifyItemInserted(newPosition)
                    }
                    if (temp == newPosition) {//在同一个位置来回晃悠,啥也不干
                        return false
                    }
                    if (temp > 0) {//这个temp其实就是我们添加的那条数据,用他来代替第一条数据来移动
                        Collections.swap(wordsOriginal, temp, newPosition)
                        recyclerView.adapter.notifyItemMoved(temp, newPosition)
                    }
                    temp = newPosition
                    return true
                }
    
                //使用kotlin的时候得注意,这个viewHolder是可能为空的,当state为Idle的时候
                override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
                    super.onSelectedChanged(viewHolder, actionState)
                    viewHolder?.run {
                        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
                            itemView.setBackgroundColor(Color.RED); //正在拖动的item弄成红色
                        }
                    }
                }
    
                override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
                    super.clearView(recyclerView, viewHolder)
                    viewHolder.itemView.setBackgroundColor(0);//正在拖动的item颜色还原
                    if(!wordsOriginal[0].inserted){
                        recyclerView.adapter.notifyItemChanged(0)
                    }
                }
    
                //手指拖动的时候isCurrentlyActive是true,松开手指的时候成为false,可以在false的时候判断下当前item的位置是否在有效位置,dY就是y轴方向移动的距离,往下是正的
                override fun onChildDraw(c: Canvas?, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
                    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
                    //我们只判断y的范围是否在有效范围内,temp大于0表示曾经移动到有效范围内
                    if (temp > 0 && !isCurrentlyActive) {
                        val firstChild = recyclerView.getChildAt(0)
                        val secondTop = recyclerView.getChildAt(1).top
                        val lastBottom = recyclerView.getChildAt(recyclerView.childCount - 1).bottom
                        if (firstChild.bottom + dY < secondTop || firstChild.top + dY > lastBottom) {
                            //有效范围之外松手,那么还原数据,不做判断
                            wordsOriginal.removeAt(temp)
                            recyclerView.adapter.notifyItemRemoved(temp)
                            wordsOriginal[0].inserted = false;
                            //更新操作放到clearView里,因为这时候第一个view正在进行回到原始位置的动画。
                        } else {
                            choice = temp
                            recyclerView.adapter.notifyDataSetChanged()
                            //最终的choice和正确的rightPosition比较下,对错之后要感谢啥,自己处理
                            showToast("选择${if (choice == rightPosition) "正确" else "错误"}")
                        }
                        temp = -1
                    }
                }
    
            })
            helper.attachToRecyclerView(rv_words)
    
        }
    
    }
    

    最后附上有效位置判断图解

    第一个item的bottom加上移动的距离 小于第二个item的top,很明显就是在句子上边了。
    第一个item的top加上移动的距离 大于 最后一个item的bottom,很明显跑到句子最下边去了。


    image.png

    相关文章

      网友评论

        本文标题:单词拖动

        本文链接:https://www.haomeiwen.com/subject/britgftx.html