摘要:二這個(gè)單詞隨手勢(shì)的移動(dòng)單詞塊相比較上面流式布局的實(shí)現(xiàn),這個(gè)就相對(duì)復(fù)雜多了。在這個(gè)模塊中,我們需要實(shí)現(xiàn)以下邏輯。
前言
前兩天在改完APP的一些bug之后逛了一下貼吧,在Android開(kāi)發(fā)吧中很驚喜的發(fā)現(xiàn)了一個(gè)朋友在尋求幫助。為什么說(shuō)驚喜呢?因?yàn)楝F(xiàn)在這個(gè)貼吧已經(jīng)淪為了接畢設(shè)課設(shè)的重災(zāi)區(qū),少有人在這里討論技術(shù)了。話說(shuō)回來(lái),這位朋友的問(wèn)題是這樣的。
看到之后我覺(jué)得還是挺有意思的,加上工作也不是特別忙,就試著做了一下,下面是做成的效果。
實(shí)現(xiàn)思路每次得到一個(gè)新的需求的時(shí)候,要將一個(gè)大的需求進(jìn)行劃分,劃分成主要的和次要的小需求,在這個(gè)大需求里面,“請(qǐng)將卡片移動(dòng)到正確位置”,“跳過(guò)此題”和最后正確答案的顯示都是非常容易實(shí)現(xiàn)的小需求,先不管,除此之外有三個(gè)重點(diǎn):
一、流式布局
二、“not”這個(gè)單詞隨手勢(shì)的移動(dòng)——單詞塊
三、將單詞插入到原本的句子中
解決了這三點(diǎn),基本也就實(shí)現(xiàn)了這個(gè)大需求了。
這個(gè)流式布局主要是為了承載題干的,當(dāng)然使用RecyclerView+LayoutManager來(lái)實(shí)現(xiàn)是最簡(jiǎn)單的,這里我使用的是xiangcman/LayoutManager-FlowLayout,用這個(gè)LayoutManager可以很輕松的實(shí)現(xiàn)流式布局來(lái)承載題干的內(nèi)容。
二、“not”這個(gè)單詞隨手勢(shì)的移動(dòng)——單詞塊相比較上面流式布局的實(shí)現(xiàn),這個(gè)就相對(duì)復(fù)雜多了。主要考察的點(diǎn)是View的事件處理和坐標(biāo)位置換算。我們主要監(jiān)聽(tīng)“單詞塊”的setOnTouchListener事件,然后處理MotionEvent.ACTION_MOVE事件,讓“單詞塊“隨著我們的手指移動(dòng)。在這里,我們需要介紹下幾個(gè)重要的概念:
event.getRawX() //獲取相對(duì)于手機(jī)屏幕左上角的距離
event.getX() //獲取以被監(jiān)聽(tīng)事件控件為坐標(biāo)系的離控件左上角的距離
view.getX() //獲取view相對(duì)于其父控件的位置
具體如下圖所示:
要想單詞塊能夠隨著我們的手指移動(dòng),我們需要獲取你當(dāng)前手指指尖的位置,然后將單詞塊移動(dòng)到你手指指尖的位置,然后通過(guò)view.setX(x)和view.setY(y)來(lái)設(shè)置view的位置,我們通過(guò)event.getRawX()和event.getRawY()來(lái)獲取我們當(dāng)前手指在整個(gè)屏幕中的位置,view.setX(x)中的x是相對(duì)于他的父容器的,那么坐標(biāo)的轉(zhuǎn)換就是一個(gè)大問(wèn)題,如下圖所示:
所以最終view.setX(H.x)和view.setY(H.y),這樣就能實(shí)現(xiàn)”單詞塊“隨著手指指尖移動(dòng)了,代碼如下
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(); //記錄單詞塊父容器和手機(jī)屏幕左上角的x和y的值 tempX = event.getRawX() - event.getX() - v.getX(); tempY = event.getRawY() - event.getY() - v.getY(); } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) { //移動(dòng)的時(shí)候 float positionX = event.getRawX() - firstClickX - tempX; float positionY = event.getRawY() - firstClickY - tempY; v.setX(positionX); v.setY(positionY); } return false; } });三、將單詞插入到原本的句子中
這個(gè)是最難實(shí)現(xiàn)的,也是最復(fù)雜的。在這個(gè)模塊中,我們需要實(shí)現(xiàn)以下邏輯?!皢卧~塊”移動(dòng)到”題干“附近的時(shí)候,要開(kāi)始計(jì)算當(dāng)前“單詞塊”的中心點(diǎn)和”題干“中的哪兩個(gè)單詞的中間的”縫“最近,然后在這個(gè)”縫“所在的位子插入一個(gè)沒(méi)有內(nèi)容的空格子,以提示用戶你將插入到這個(gè)位置,當(dāng)“單詞塊”遠(yuǎn)離題干的時(shí)候,不再計(jì)算位置;然后在釋放”單詞塊“的時(shí)候,如果是在”題干“附近釋放的時(shí)候(也就是有提示框出現(xiàn)的時(shí)候),將這個(gè)單詞插入到剛剛的那個(gè)”縫“的位置,然后給出答題的結(jié)果,是放對(duì)了還是放錯(cuò)了,否則就是放棄本次答題,將“單詞塊”放回原來(lái)的位置。敘述起來(lái)很復(fù)雜,其實(shí)跟場(chǎng)景結(jié)合起來(lái),還是很好理解的。
來(lái),我們依然是各個(gè)擊破!
檢測(cè)“單詞塊”是否移動(dòng)到”題干“附近
我們可以計(jì)算出“單詞塊”的中心點(diǎn),然后計(jì)算出當(dāng)前”題干“(也就是RecyclerView)的位置,如果“單詞塊”的中心點(diǎn)在”題干“的范圍內(nèi),那么就代表進(jìn)入了要監(jiān)聽(tīng)的范圍了。代碼如下:
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(); //記錄單詞塊父容器和手機(jī)屏幕左上角的x和y的值 tempX = event.getRawX() - event.getX() - v.getX(); tempY = event.getRawY() - event.getY() - v.getY(); } else if ( event.getAction() == MotionEvent.ACTION_MOVE ) { //移動(dòng)的時(shí)候 float positionX = event.getRawX() - firstClickX - tempX; float positionY = event.getRawY() - firstClickY - tempY; v.setX(positionX); v.setY(positionY); //被移動(dòng)塊的中點(diǎn) int centerX = ( int ) (positionX + mViewWidth / 2); int centerY = ( int ) (positionY + mViewHeight / 2); //rvY是RecyclerView距離頂部的距離rvHeight是RecyclerView的高度 if ( centerY > rvY && centerY < rvHeight + rvY ) { //在范圍內(nèi)了 } else { //不在范圍內(nèi)了 } } return false; } });
計(jì)算當(dāng)前“單詞塊”的中心點(diǎn)和”題干“中的哪兩個(gè)單詞的中間的”縫“最近
其實(shí)這里有兩種思路,一種通過(guò)RecyclerView的適配器獲取到每個(gè)item的位置信息,然后計(jì)算出兩個(gè)item的中間位置,將所有的這些中間位置保存起來(lái),在分別計(jì)算“單詞塊”的中心點(diǎn)和這些中間位置的距離,然后再處理,不過(guò)用這種方式需要考慮item換行之后中心點(diǎn)計(jì)算的問(wèn)題(由于我沒(méi)有使用這種方式,對(duì)這個(gè)預(yù)期會(huì)出現(xiàn)的問(wèn)題也沒(méi)有多加思考);還有一種是在創(chuàng)建題干的時(shí)候使用多類(lèi)型的適配器,在每個(gè)單詞中間插入一個(gè)占位置的”空格“,這樣就可以直接獲取到這個(gè)”空格“的位置作為參照點(diǎn),同時(shí),這個(gè)空格還可以直接給用戶提示位置,一舉兩得。我這里就是用的第二種方式。
找出最近的”縫“
//找出最近的點(diǎn) 只找沒(méi)有內(nèi)容的格子 就是占位格子 private ItemPositionModel findPoint() { //沒(méi)有數(shù)據(jù)直接返回 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); }
找到這個(gè)”縫“之后,保存這個(gè)”縫“的下標(biāo),刷新適配器,在”縫“這個(gè)下標(biāo)處顯示那個(gè)用于提示的空格子。
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { ShowItem showItem = list.get(position); if ( showItem != null ) if ( showItem.getType() == 0 ) { //正文內(nèi)容 ...... } else { //currSelectIndex是縫的下標(biāo) if ( currSelectIndex == position ) { (( MyHolderDivider ) holder).tv_divider.setVisibility(View.VISIBLE); } else { (( MyHolderDivider ) holder).tv_divider.setVisibility(View.GONE); } } }
釋放”單詞塊“的時(shí)候
在釋放”單詞塊“的時(shí)候,我們需要判斷當(dāng)前是否還在范圍內(nèi),如果是在范圍內(nèi),就在”縫“的地方插入”單詞塊“內(nèi)部的單詞值,然后隱藏掉”單詞塊“,否則,隱藏剛剛用于提示的空格子并將”單詞塊“移動(dòng)到之前的位置。
//抬起手指的一瞬間 if ( event.getAction() == MotionEvent.ACTION_UP ) { //如果在RecyclerView的范圍內(nèi)才處理 否則回退到原地 if ( isInArea ) { //添加成功 移除之前的視圖 v.setVisibility(View.GONE); //檢查并設(shè)置結(jié)果 最好提取出來(lái) ShowItem result = new ShowItem((( TextView ) v).getText().toString(), 0); if ( rightIndex == currSelectIndex ) { //正確 result.setIsRight(1); } else { //錯(cuò)誤 result.setIsRight(2); } list.add(currSelectIndex + 1, result); list.add(currSelectIndex + 2, new ShowItem("", 1)); } else { //未成功添加抬起的時(shí)候回歸原地 v.setX(firstX); v.setY(firstY); } //重置位置 currSelectIndex = -1; flowAdapter.notifyDataSetChanged(); }
下面整個(gè)是多類(lèi)型的適配器的代碼,由于比較簡(jiǎn)單就寫(xiě)的比較隨意,沒(méi)有多去封裝啥的:
class FlowAdapter extends RecyclerView.Adapter{ private List list; public FlowAdapter(List list) { this.list = list; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if ( viewType == 0 ) { //正文內(nèi)容類(lèi)型 return new MyHolder(View.inflate(MainActivity.this, R.layout.flow_item, null)); } else { //占位符類(lèi)型 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 ) { //正文內(nèi)容 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); } } }
最后就是處理用戶答案和正確答案的拼接與顯示工作已經(jīng)對(duì)用戶的答案進(jìn)行評(píng)判的過(guò)程,像什么答案正確顯示綠色,錯(cuò)誤顯示紅色,比較簡(jiǎn)單,就不再贅述,為了減少篇幅,就不再貼出整個(gè)代碼了,感興趣的可以查看源碼,我會(huì)將源碼放到Github上,如果感覺(jué)有用,歡迎star,哈哈。
注:由于時(shí)間比較趕,所以有些地方的代碼和命名不是很規(guī)范,敬請(qǐng)諒解。
項(xiàng)目地址和結(jié)語(yǔ)Github地址: DragDemo
如果連接失效就直接點(diǎn)擊這個(gè)鏈接吧!https://github.com/MZCretin/D...
最后感謝 xiangcman/LayoutManager-FlowLayout
我是Cretin,一個(gè)可愛(ài)的小男孩
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/68621.html
摘要:要快,但是我們的服務(wù)也必須萬(wàn)無(wú)一失,后續(xù)我會(huì)分享百度移動(dòng)端首頁(yè)的前端架構(gòu)設(shè)計(jì)那么這樣的優(yōu)化,是如何做到的呢,又如何兼顧穩(wěn)定性,架構(gòu)性,與速度呢別急,讓我們把這些優(yōu)化一一道來(lái)。百度移動(dòng)端首頁(yè)的很多就是這樣緩存在客戶端的。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog/fronte...
摘要:而這一次的項(xiàng)目,原本以為開(kāi)發(fā)挺順利的,但是開(kāi)發(fā)完了,才發(fā)現(xiàn)自己犯了一個(gè)低級(jí)而嚴(yán)重的錯(cuò),這樣的一個(gè)失誤,我一直耿耿于懷。但是監(jiān)聽(tīng)用戶退出頁(yè)面微信瀏覽器上面的那個(gè)返回或者關(guān)閉按鈕卻死活不行。也容易犯一些低級(jí)的錯(cuò)誤。 1.前言 前端從事了超過(guò)兩年,修復(fù)了無(wú)數(shù)的bug,寫(xiě)了無(wú)數(shù)的bug;挖了很多次坑,填了很多次坑;犯了很多次錯(cuò),彌補(bǔ)了很多次,學(xué)習(xí)了很多次。一般而言,對(duì)于bug、坑,都是修復(fù)完了...
摘要:一的直播答題時(shí)什么的直播答題主要發(fā)生在三個(gè)環(huán)境下端移動(dòng)瀏覽器和微信端,微信端包括微信瀏覽器和微信小程序。除了要注意部署邊緣節(jié)點(diǎn),轉(zhuǎn)碼和中繼也需要部署邊緣幾點(diǎn),所以微信端直播與答題的同步問(wèn)題會(huì)加重。 各大平臺(tái)為了給自家的直播答題爭(zhēng)搶流量,已經(jīng)絞盡腦汁,不斷在玩法上進(jìn)行創(chuàng)新。這場(chǎng)競(jìng)爭(zhēng)從 iOS 平臺(tái)蔓延至 Android 平臺(tái)。目前大多數(shù)平臺(tái)獲取用戶的方式還是通過(guò)分享邀請(qǐng)碼,下載 App ...
閱讀 784·2021-08-17 10:11
閱讀 1654·2019-08-30 11:15
閱讀 1090·2019-08-26 13:54
閱讀 3556·2019-08-26 11:47
閱讀 1289·2019-08-26 10:20
閱讀 2894·2019-08-23 18:35
閱讀 1264·2019-08-23 17:52
閱讀 1352·2019-08-23 16:19