【需求解決系列之一】移動卡片實現答題功能

Roll圈圈發表於2018-06-01

前言

前兩天在改完APP的一些bug之後逛了一下貼吧,在Android開發吧中很驚喜的發現了一個朋友在尋求幫助。為什麼說驚喜呢?因為現在這個貼吧已經淪為了接畢設課設的重災區,少有人在這裡討論技術了。話說回來,這位朋友的問題是這樣的。

需求

需求說明

看到之後我覺得還是挺有意思的,加上工作也不是特別忙,就試著做了一下,下面是做成的效果。

最終效果圖

實現思路

每次得到一個新的需求的時候,要將一個大的需求進行劃分,劃分成主要的和次要的小需求,在這個大需求裡面,“請將卡片移動到正確位置”,“跳過此題”和最後正確答案的顯示都是非常容易實現的小需求,先不管,除此之外有三個重點: 一、流式佈局 二、“not”這個單詞隨手勢的移動——單詞塊 三、將單詞插入到原本的句子中 解決了這三點,基本也就實現了這個大需求了。

擼起袖子各個擊破

一、流式佈局

這個流式佈局主要是為了承載題乾的,當然使用RecyclerView+LayoutManager來實現是最簡單的,這裡我使用的是**xiangcman/LayoutManager-FlowLayout**,用這個LayoutManager可以很輕鬆的實現流式佈局來承載題乾的內容。

二、“not”這個單詞隨手勢的移動——單詞塊

相比較上面流式佈局的實現,這個就相對複雜多了。主要考察的點是View的事件處理和座標位置換算。我們主要監聽“單詞塊”的setOnTouchListener事件,然後處理MotionEvent.ACTION_MOVE事件,讓“單詞塊“隨著我們的手指移動。在這裡,我們需要介紹下幾個重要的概念: event.getRawX() //獲取相對於手機螢幕左上角的距離 event.getX() //獲取以被監聽事件控制元件為座標系的離控制元件左上角的距離 view.getX() //獲取view相對於其父控制元件的位置 具體如下圖所示:

說明

要想單詞塊能夠隨著我們的手指移動,我們需要獲取你當前手指指尖的位置,然後將單詞塊移動到你手指指尖的位置,然後通過view.setX(x)和view.setY(y)來設定view的位置,我們通過event.getRawX()和event.getRawY()來獲取我們當前手指在整個螢幕中的位置,view.setX(x)中的x是相對於他的父容器的,那麼座標的轉換就是一個大問題,如下圖所示:

圖片.png

所以最終view.setX(H.x)和view.setY(H.y),這樣就能實現”單詞塊“隨著手指指尖移動了,程式碼如下

flow_text.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
                    //記錄手指指尖的位置和代詞塊左上角的x和y的值
                    firstClickX = event.getX();
                    firstClickY = event.getY();
                    //記錄單詞塊父容器和手機螢幕左上角的x和y的值
                    tempX = event.getRawX() - event.getX() - v.getX();
                    tempY = event.getRawY() - event.getY() - v.getY();
                } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) {
                    //移動的時候
                    float positionX = event.getRawX() - firstClickX - tempX;
                    float positionY = event.getRawY() - firstClickY - tempY;
                    v.setX(positionX);
                    v.setY(positionY);
                }
                return false;
            }
        });
複製程式碼

三、將單詞插入到原本的句子中

這個是最難實現的,也是最複雜的。在這個模組中,我們需要實現以下邏輯。“單詞塊”移動到”題幹“附近的時候,要開始計算當前“單詞塊”的中心點和”題幹“中的哪兩個單詞的中間的”縫“最近,然後在這個”縫“所在的位子插入一個沒有內容的空格子,以提示使用者你將插入到這個位置,當“單詞塊”遠離題乾的時候,不再計算位置;然後在釋放”單詞塊“的時候,如果是在”題幹“附近釋放的時候(也就是有提示框出現的時候),將這個單詞插入到剛剛的那個”縫“的位置,然後給出答題的結果,是放對了還是放錯了,否則就是放棄本次答題,將“單詞塊”放回原來的位置。敘述起來很複雜,其實跟場景結合起來,還是很好理解的。

來,我們依然是各個擊破!

檢測“單詞塊”是否移動到”題幹“附近 我們可以計算出“單詞塊”的中心點,然後計算出當前”題幹“(也就是RecyclerView)的位置,如果“單詞塊”的中心點在”題幹“的範圍內,那麼就代表進入了要監聽的範圍了。程式碼如下:

flow_text.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
                    //記錄手指指尖的位置和代詞塊左上角的x和y的值
                    firstClickX = event.getX();
                    firstClickY = event.getY();
                    //記錄單詞塊父容器和手機螢幕左上角的x和y的值
                    tempX = event.getRawX() - event.getX() - v.getX();
                    tempY = event.getRawY() - event.getY() - v.getY();
                } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) {
                    //移動的時候
                    float positionX = event.getRawX() - firstClickX - tempX;
                    float positionY = event.getRawY() - firstClickY - tempY;
                    v.setX(positionX);
                    v.setY(positionY);

                    //被移動塊的中點
                    int centerX = ( int ) (positionX + mViewWidth / 2);
                    int centerY = ( int ) (positionY + mViewHeight / 2);
                    //rvY是RecyclerView距離頂部的距離rvHeight是RecyclerView的高度
                    if ( centerY > rvY && centerY < rvHeight + rvY ) {
                       //在範圍內了
                    } else {
                       //不在範圍內了
                    }
                } 
                return false;
            }
        });
複製程式碼

計算當前“單詞塊”的中心點和”題幹“中的哪兩個單詞的中間的”縫“最近 其實這裡有兩種思路,一種通過RecyclerView的介面卡獲取到每個item的位置資訊,然後計算出兩個item的中間位置,將所有的這些中間位置儲存起來,在分別計算“單詞塊”的中心點和這些中間位置的距離,然後再處理,不過用這種方式需要考慮item換行之後中心點計算的問題(由於我沒有使用這種方式,對這個預期會出現的問題也沒有多加思考);還有一種是在建立題乾的時候使用多型別的介面卡,在每個單詞中間插入一個佔位置的”空格“,這樣就可以直接獲取到這個”空格“的位置作為參照點,同時,這個空格還可以直接給使用者提示位置,一舉兩得。我這裡就是用的第二種方式。

找出最近的”縫“

    //找出最近的點 只找沒有內容的格子 就是佔位格子
    private ItemPositionModel findPoint() {
        //沒有資料直接返回
        if ( itemList.isEmpty() )
            return null;
        double distance = Math.sqrt(Math.pow((center.x - itemList.get(0).getCenter().x), 2) +
                Math.pow((center.y - itemList.get(0).getCenter().y), 2));
        int index = 0;
        for ( int i = 1; i < itemList.size(); i++ ) {
            if ( i % 2 == 0 ) {
                double temp = Math.sqrt(Math.pow((center.x - itemList.get(i).getCenter().x), 2) +
                        Math.pow((center.y - itemList.get(i).getCenter().y), 2));
                if ( temp <= distance ) {
                    distance = temp;
                    index = i;
                }
            }
        }
        return itemList.get(index);
    }
複製程式碼

找到這個”縫“之後,儲存這個”縫“的下標,重新整理介面卡,在”縫“這個下標處顯示那個用於提示的空格子。

@Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
            ShowItem showItem = list.get(position);
            if ( showItem != null )
                if ( showItem.getType() == 0 ) {
                    //正文內容
                    ......
                } else {
                   //currSelectIndex是縫的下標
                    if ( currSelectIndex == position ) {
                        (( MyHolderDivider ) holder).tv_divider.setVisibility(View.VISIBLE);
                    } else {
                        (( MyHolderDivider ) holder).tv_divider.setVisibility(View.GONE);
                    }
                }
        }
複製程式碼

釋放”單詞塊“的時候

在釋放”單詞塊“的時候,我們需要判斷當前是否還在範圍內,如果是在範圍內,就在”縫“的地方插入”單詞塊“內部的單詞值,然後隱藏掉”單詞塊“,否則,隱藏剛剛用於提示的空格子並將”單詞塊“移動到之前的位置。

//抬起手指的一瞬間
if ( event.getAction() == MotionEvent.ACTION_UP ) {
                    //如果在RecyclerView的範圍內才處理 否則回退到原地
                    if ( isInArea ) {
                        //新增成功 移除之前的檢視
                        v.setVisibility(View.GONE);
                        //檢查並設定結果 最好提取出來
                        ShowItem result = new ShowItem((( TextView ) v).getText().toString(), 0);
                        if ( rightIndex == currSelectIndex ) {
                            //正確
                            result.setIsRight(1);
                        } else {
                            //錯誤
                            result.setIsRight(2);
                        }
                        list.add(currSelectIndex + 1, result);
                        list.add(currSelectIndex + 2, new ShowItem("", 1)); 
                    } else {
                        //未成功新增抬起的時候迴歸原地
                        v.setX(firstX);
                        v.setY(firstY);
                    }
                    //重置位置
                    currSelectIndex = -1;
                    flowAdapter.notifyDataSetChanged();
                }
複製程式碼

下面整個是多型別的介面卡的程式碼,由於比較簡單就寫的比較隨意,沒有多去封裝啥的:

class FlowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        private List<ShowItem> list;

        public FlowAdapter(List<ShowItem> list) {
            this.list = list;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if ( viewType == 0 ) {
                //正文內容型別
                return new MyHolder(View.inflate(MainActivity.this, R.layout.flow_item, null));
            } else {
                //佔位符型別
                return new MyHolderDivider(View.inflate(MainActivity.this, R.layout.flow_divider, null));
            }
        }

        @Override
        public int getItemViewType(int position) {
            return list.get(position).getType();
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
            ShowItem showItem = list.get(position);
            if ( showItem != null )
                if ( showItem.getType() == 0 ) {
                    //正文內容
                    TextView textView = (( MyHolder ) holder).text;
                    textView.setText(list.get(position).des);
                } else {
                    //是否顯示空格子
                    if ( currSelectIndex == position ) {
                        (( MyHolderDivider ) holder).tv_divider.setVisibility(View.VISIBLE);
                    } else {
                        (( MyHolderDivider ) holder).tv_divider.setVisibility(View.GONE);
                    }
                }
        }

        @Override
        public int getItemCount() {
            return list.size();
        }

        class MyHolder extends RecyclerView.ViewHolder {

            private TextView text;

            public MyHolder(View itemView) {
                super(itemView);
                text = ( TextView ) itemView.findViewById(R.id.flow_text);
            }
        }

        class MyHolderDivider extends RecyclerView.ViewHolder {

            private TextView tv_divider;

            public MyHolderDivider(View itemView) {
                super(itemView);
                tv_divider = ( TextView ) itemView.findViewById(R.id.tv_divider);
            }
        }
    }
複製程式碼

最後就是處理使用者答案和正確答案的拼接與顯示工作已經對使用者的答案進行評判的過程,像什麼答案正確顯示綠色,錯誤顯示紅色,比較簡單,就不再贅述,為了減少篇幅,就不再貼出整個程式碼了,感興趣的可以檢視原始碼,我會將原始碼放到Github上,如果感覺有用,歡迎star,哈哈。

注:由於時間比較趕,所以有些地方的程式碼和命名不是很規範,敬請諒解。

專案地址和結語

Github地址: DragDemo

如果連線失效就直接點選這個連結吧!https://github.com/MZCretin/DragDemo

最後感謝 xiangcman/LayoutManager-FlowLayout

關於我的

我就是比較喜歡用程式碼解決生活中的問題,感覺很開心,哈哈哈。也喜歡大家關注我的簡書,掘金,Github和CSDN。

簡書首頁,連結是 https://www.jianshu.com/u/123f97613b86

掘金首頁,連結是 https://juejin.im/user/5838d57fac502e006c1708bc

Github首頁,連結是 https://github.com/MZCretin

CSDN首頁,連結是 http://blog.csdn.net/u010998327

我是Cretin,一個可愛的小男孩。

相關文章