ROLL

登    录

注    册

重置密码

博主-ROLL圈圈 博主-ROLL圈圈 2020年10月24 10590 字

【需求解决系列之一】移动卡片实现答题功能

前言

前两天在改完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/1099167356171918

**Github首页**,链接是 https://github.com/MZCretin

**CSDN首页**,链接是 http://blog.csdn.net/u010998327

我是Cretin,一个可爱的小男孩。