资讯专栏INFORMATION COLUMN

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

Mr_zhang / 935人阅读

摘要:二这个单词随手势的移动单词块相比较上面流式布局的实现,这个就相对复杂多了。在这个模块中,我们需要实现以下逻辑。

前言

前两天在改完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是相对于他的父容器的,那么坐标的转换就是一个大问题,如下图所示:

所以最终view.setX(H.x)和view.setY(H.y),这样就能实现”单词块“随着手指指尖移动了,代码如下

</>复制代码

  1. flow_text.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
  5. //记录手指指尖的位置和代词块左上角的x和y的值
  6. firstClickX = event.getX();
  7. firstClickY = event.getY();
  8. //记录单词块父容器和手机屏幕左上角的x和y的值
  9. tempX = event.getRawX() - event.getX() - v.getX();
  10. tempY = event.getRawY() - event.getY() - v.getY();
  11. } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) {
  12. //移动的时候
  13. float positionX = event.getRawX() - firstClickX - tempX;
  14. float positionY = event.getRawY() - firstClickY - tempY;
  15. v.setX(positionX);
  16. v.setY(positionY);
  17. }
  18. return false;
  19. }
  20. });
三、将单词插入到原本的句子中

这个是最难实现的,也是最复杂的。在这个模块中,我们需要实现以下逻辑。“单词块”移动到”题干“附近的时候,要开始计算当前“单词块”的中心点和”题干“中的哪两个单词的中间的”缝“最近,然后在这个”缝“所在的位子插入一个没有内容的空格子,以提示用户你将插入到这个位置,当“单词块”远离题干的时候,不再计算位置;然后在释放”单词块“的时候,如果是在”题干“附近释放的时候(也就是有提示框出现的时候),将这个单词插入到刚刚的那个”缝“的位置,然后给出答题的结果,是放对了还是放错了,否则就是放弃本次答题,将“单词块”放回原来的位置。叙述起来很复杂,其实跟场景结合起来,还是很好理解的。

来,我们依然是各个击破!

检测“单词块”是否移动到”题干“附近
我们可以计算出“单词块”的中心点,然后计算出当前”题干“(也就是RecyclerView)的位置,如果“单词块”的中心点在”题干“的范围内,那么就代表进入了要监听的范围了。代码如下:

</>复制代码

  1. flow_text.setOnTouchListener(new View.OnTouchListener() {
  2. @Override
  3. public boolean onTouch(View v, MotionEvent event) {
  4. if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
  5. //记录手指指尖的位置和代词块左上角的x和y的值
  6. firstClickX = event.getX();
  7. firstClickY = event.getY();
  8. //记录单词块父容器和手机屏幕左上角的x和y的值
  9. tempX = event.getRawX() - event.getX() - v.getX();
  10. tempY = event.getRawY() - event.getY() - v.getY();
  11. } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) {
  12. //移动的时候
  13. float positionX = event.getRawX() - firstClickX - tempX;
  14. float positionY = event.getRawY() - firstClickY - tempY;
  15. v.setX(positionX);
  16. v.setY(positionY);
  17. //被移动块的中点
  18. int centerX = ( int ) (positionX + mViewWidth / 2);
  19. int centerY = ( int ) (positionY + mViewHeight / 2);
  20. //rvY是RecyclerView距离顶部的距离rvHeight是RecyclerView的高度
  21. if ( centerY > rvY && centerY < rvHeight + rvY ) {
  22. //在范围内了
  23. } else {
  24. //不在范围内了
  25. }
  26. }
  27. return false;
  28. }
  29. });

计算当前“单词块”的中心点和”题干“中的哪两个单词的中间的”缝“最近
其实这里有两种思路,一种通过RecyclerView的适配器获取到每个item的位置信息,然后计算出两个item的中间位置,将所有的这些中间位置保存起来,在分别计算“单词块”的中心点和这些中间位置的距离,然后再处理,不过用这种方式需要考虑item换行之后中心点计算的问题(由于我没有使用这种方式,对这个预期会出现的问题也没有多加思考);还有一种是在创建题干的时候使用多类型的适配器,在每个单词中间插入一个占位置的”空格“,这样就可以直接获取到这个”空格“的位置作为参照点,同时,这个空格还可以直接给用户提示位置,一举两得。我这里就是用的第二种方式。

找出最近的”缝“

</>复制代码

  1. //找出最近的点 只找没有内容的格子 就是占位格子
  2. private ItemPositionModel findPoint() {
  3. //没有数据直接返回
  4. if ( itemList.isEmpty() )
  5. return null;
  6. double distance = Math.sqrt(Math.pow((center.x - itemList.get(0).getCenter().x), 2) +
  7. Math.pow((center.y - itemList.get(0).getCenter().y), 2));
  8. int index = 0;
  9. for ( int i = 1; i < itemList.size(); i++ ) {
  10. if ( i % 2 == 0 ) {
  11. double temp = Math.sqrt(Math.pow((center.x - itemList.get(i).getCenter().x), 2) +
  12. Math.pow((center.y - itemList.get(i).getCenter().y), 2));
  13. if ( temp <= distance ) {
  14. distance = temp;
  15. index = i;
  16. }
  17. }
  18. }
  19. return itemList.get(index);
  20. }

找到这个”缝“之后,保存这个”缝“的下标,刷新适配器,在”缝“这个下标处显示那个用于提示的空格子。

</>复制代码

  1. @Override
  2. public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
  3. ShowItem showItem = list.get(position);
  4. if ( showItem != null )
  5. if ( showItem.getType() == 0 ) {
  6. //正文内容
  7. ......
  8. } else {
  9. //currSelectIndex是缝的下标
  10. if ( currSelectIndex == position ) {
  11. (( MyHolderDivider ) holder).tv_divider.setVisibility(View.VISIBLE);
  12. } else {
  13. (( MyHolderDivider ) holder).tv_divider.setVisibility(View.GONE);
  14. }
  15. }
  16. }

释放”单词块“的时候

在释放”单词块“的时候,我们需要判断当前是否还在范围内,如果是在范围内,就在”缝“的地方插入”单词块“内部的单词值,然后隐藏掉”单词块“,否则,隐藏刚刚用于提示的空格子并将”单词块“移动到之前的位置。

</>复制代码

  1. //抬起手指的一瞬间
  2. if ( event.getAction() == MotionEvent.ACTION_UP ) {
  3. //如果在RecyclerView的范围内才处理 否则回退到原地
  4. if ( isInArea ) {
  5. //添加成功 移除之前的视图
  6. v.setVisibility(View.GONE);
  7. //检查并设置结果 最好提取出来
  8. ShowItem result = new ShowItem((( TextView ) v).getText().toString(), 0);
  9. if ( rightIndex == currSelectIndex ) {
  10. //正确
  11. result.setIsRight(1);
  12. } else {
  13. //错误
  14. result.setIsRight(2);
  15. }
  16. list.add(currSelectIndex + 1, result);
  17. list.add(currSelectIndex + 2, new ShowItem("", 1));
  18. } else {
  19. //未成功添加抬起的时候回归原地
  20. v.setX(firstX);
  21. v.setY(firstY);
  22. }
  23. //重置位置
  24. currSelectIndex = -1;
  25. flowAdapter.notifyDataSetChanged();
  26. }

下面整个是多类型的适配器的代码,由于比较简单就写的比较随意,没有多去封装啥的:

</>复制代码

  1. class FlowAdapter extends RecyclerView.Adapter {
  2. private List list;
  3. public FlowAdapter(List list) {
  4. this.list = list;
  5. }
  6. @Override
  7. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  8. if ( viewType == 0 ) {
  9. //正文内容类型
  10. return new MyHolder(View.inflate(MainActivity.this, R.layout.flow_item, null));
  11. } else {
  12. //占位符类型
  13. return new MyHolderDivider(View.inflate(MainActivity.this, R.layout.flow_divider, null));
  14. }
  15. }
  16. @Override
  17. public int getItemViewType(int position) {
  18. return list.get(position).getType();
  19. }
  20. @Override
  21. public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
  22. ShowItem showItem = list.get(position);
  23. if ( showItem != null )
  24. if ( showItem.getType() == 0 ) {
  25. //正文内容
  26. TextView textView = (( MyHolder ) holder).text;
  27. textView.setText(list.get(position).des);
  28. } else {
  29. //是否显示空格子
  30. if ( currSelectIndex == position ) {
  31. (( MyHolderDivider ) holder).tv_divider.setVisibility(View.VISIBLE);
  32. } else {
  33. (( MyHolderDivider ) holder).tv_divider.setVisibility(View.GONE);
  34. }
  35. }
  36. }
  37. @Override
  38. public int getItemCount() {
  39. return list.size();
  40. }
  41. class MyHolder extends RecyclerView.ViewHolder {
  42. private TextView text;
  43. public MyHolder(View itemView) {
  44. super(itemView);
  45. text = ( TextView ) itemView.findViewById(R.id.flow_text);
  46. }
  47. }
  48. class MyHolderDivider extends RecyclerView.ViewHolder {
  49. private TextView tv_divider;
  50. public MyHolderDivider(View itemView) {
  51. super(itemView);
  52. tv_divider = ( TextView ) itemView.findViewById(R.id.tv_divider);
  53. }
  54. }
  55. }

最后就是处理用户答案和正确答案的拼接与显示工作已经对用户的答案进行评判的过程,像什么答案正确显示绿色,错误显示红色,比较简单,就不再赘述,为了减少篇幅,就不再贴出整个代码了,感兴趣的可以查看源码,我会将源码放到Github上,如果感觉有用,欢迎star,哈哈。

注:由于时间比较赶,所以有些地方的代码和命名不是很规范,敬请谅解。

项目地址和结语

Github地址: DragDemo

如果连接失效就直接点击这个链接吧!https://github.com/MZCretin/D...

最后感谢 xiangcman/LayoutManager-FlowLayout

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68621.html

相关文章

  • [聊一聊系列]聊一聊百度移动端首页前端速度那些事儿

    摘要:要快,但是我们的服务也必须万无一失,后续我会分享百度移动端首页的前端架构设计那么这样的优化,是如何做到的呢,又如何兼顾稳定性,架构性,与速度呢别急,让我们把这些优化一一道来。百度移动端首页的很多就是这样缓存在客户端的。 欢迎大家收看聊一聊系列,这一套系列文章,可以帮助前端工程师们了解前端的方方面面(不仅仅是代码):https://segmentfault.com/blog/fronte...

    The question 评论0 收藏0
  • 记一次低级并严重的开发失误

    摘要:而这一次的项目,原本以为开发挺顺利的,但是开发完了,才发现自己犯了一个低级而严重的错,这样的一个失误,我一直耿耿于怀。但是监听用户退出页面微信浏览器上面的那个返回或者关闭按钮却死活不行。也容易犯一些低级的错误。 1.前言 前端从事了超过两年,修复了无数的bug,写了无数的bug;挖了很多次坑,填了很多次坑;犯了很多次错,弥补了很多次,学习了很多次。一般而言,对于bug、坑,都是修复完了...

    wudengzan 评论0 收藏0
  • H5直播答题并不难,看完这篇你也会

    摘要:一的直播答题时什么的直播答题主要发生在三个环境下端移动浏览器和微信端,微信端包括微信浏览器和微信小程序。除了要注意部署边缘节点,转码和中继也需要部署边缘几点,所以微信端直播与答题的同步问题会加重。 各大平台为了给自家的直播答题争抢流量,已经绞尽脑汁,不断在玩法上进行创新。这场竞争从 iOS 平台蔓延至 Android 平台。目前大多数平台获取用户的方式还是通过分享邀请码,下载 App ...

    hiyang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<