單詞拖動

不知道在哪裏看到過一個需求,具體的也記不清了,有點久了,現在抽空寫下效果。
簡單需求如下,給一個單詞,給一個句子,然後把單詞拖動到正確的位置。拖動結束,判斷下位置是否正確,正確的話單詞顯示綠色,不正確的話單詞顯示紅色。然後顯示正確的句子以及釋義。
注:如果用戶在無效範圍內鬆開手指,那麼還原即可,還可以重新拖動。

長按開始拖動。
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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章