作者:IAM四十二
鏈接:http://www.reibang.com/p/d06c1d10bf7f
著作權(quán)歸作者所有陡舅。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)日裙,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處镊掖。
天下無(wú)敵
前言
這幾天很多歐洲球隊(duì)來(lái)中國(guó)進(jìn)行熱身賽域慷,不知道喜歡足球的各位小伙伴們有沒(méi)有看球环形。喜歡足球的朋友可能知道懂球帝APP冲九,鄙人也經(jīng)常使用這個(gè)應(yīng)用谤草,里面有一個(gè)我是教練的功能挺好玩,就是可以模擬教練員的身份,排兵布陣莺奸;本著好奇心簡(jiǎn)單模仿了一下丑孩,在這里和大家分享。
效果圖
老規(guī)矩灭贷,先上效果圖看看模仿的像不温学。
add_player.gif
move_player
玩過(guò)我是教練這個(gè)功能的小伙伴可以對(duì)比一下。
總的來(lái)說(shuō)甚疟,這樣的一個(gè)效果仗岖,其實(shí)很簡(jiǎn)單,就是一個(gè)view隨著手指在屏幕上移動(dòng)的效果览妖,外加一個(gè)圖片替換的動(dòng)畫(huà)轧拄。但就是這些看似簡(jiǎn)單的效果,在實(shí)現(xiàn)的過(guò)程中也是遇到了很多坑讽膏,漲了許多新姿勢(shì)檩电。好了,廢話(huà)不說(shuō)府树,代碼走起(??ˇ?ˇ?)是嗜。
自定義View-BallGameView
整個(gè)內(nèi)容中最核心的就是一個(gè)自定義View-BallGameView,就是屏幕中綠色背景挺尾,有氣泡和球員圖片的整個(gè)view鹅搪。
說(shuō)到自定義View,老生常談遭铺,大家一直都在學(xué)習(xí)丽柿,卻永遠(yuǎn)都覺(jué)得自己沒(méi)有學(xué)會(huì),但是自定義View的知識(shí)本來(lái)就很多呀魂挂,想要熟練掌握甫题,必須假以時(shí)日。
既然是自定View就從大家最關(guān)心的兩個(gè)方法 onMeasure和onDraw 兩個(gè)方法說(shuō)起涂召。這里由于是純粹繼承自View坠非,就不考慮onLayout的實(shí)現(xiàn)了。
測(cè)量-onMeasure
這里onMeasure()方法的實(shí)現(xiàn)很簡(jiǎn)單果正,簡(jiǎn)單的用屏幕的寬度規(guī)定了整個(gè)View 的寬高炎码;至于1.3這個(gè)倍數(shù)盟迟,完全一個(gè)估算值,不必深究潦闲。
繪制-onDraw
onDraw()方法是整個(gè)View中最核心的方法攒菠。
可以看到,在onDraw方法里歉闰,我們主要使用了canvas.drawBitmap 方法辖众,繪制了很多圖片。下面就簡(jiǎn)單了解一下canvas.drawBitmap 里的兩個(gè)重載方法和敬。
drawBitmap(Bitmap bitmap,Rect src,Rect dst,Paint paint)
drawBitmap(Bitmap bitmap,Rect src,Rect dst,Paint paint),這個(gè)重載方法主要是通過(guò)兩個(gè)Rectangle 決定了bitmap以怎樣的形式繪制出來(lái)凹炸。簡(jiǎn)單來(lái)說(shuō),src 這個(gè)長(zhǎng)方形決定了“截取”bitmap的大小昼弟,dst 決定了最終繪制出來(lái)時(shí)Bitmap應(yīng)該占有的大小啤它。。就拿上面的代碼來(lái)說(shuō)
? ? ? ?
bitmapRect 是整個(gè)backgroundBitmap的大小私杜,mViewRect也就是我們?cè)趏nMeasure里規(guī)定的整個(gè)視圖的大小蚕键,這樣相當(dāng)于把battle_bg這張圖片,以scaleType="fitXY"的形式畫(huà)在了視圖大小的區(qū)域內(nèi)衰粹。這樣锣光,你應(yīng)該理解這個(gè)重載方法的含義了。
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
?
這個(gè)重載方法應(yīng)該很容易理解了铝耻,left誊爹,top 規(guī)定了繪制Bitmap的左上角的坐標(biāo),然后按照其大小正常繪制即可瓢捉。
這里我們所有的氣泡(球員位置)都是使用這個(gè)方法繪制的频丘。足球場(chǎng)上有11個(gè)球員,因此我們通過(guò)數(shù)組預(yù)先定義了11個(gè)氣泡的初始位置泡态,然后通過(guò)其坐標(biāo)位置搂漠,繪制他們。為了繪制精確某弦,需要減去每張圖片自身的寬高桐汤,這應(yīng)該是很傳統(tǒng)的做法了。
同時(shí)靶壮,在之后的觸摸反饋機(jī)制中怔毛,我們會(huì)根據(jù)手指的滑動(dòng),修改這些坐標(biāo)值腾降,這樣就可以隨意移動(dòng)球員在場(chǎng)上的位置了拣度;具體實(shí)現(xiàn),結(jié)合代碼中的注釋?xiě)?yīng)該很容易理解了,就不再贅述抗果;可以查看完整源碼BallGameView筋帖。
文字居中繪制
這里再說(shuō)一個(gè)在繪制過(guò)程中遇到一個(gè)小問(wèn)題,可以看到在整個(gè)視圖底部窖张,繪制了一個(gè)半透明的圓角矩形幕随,并在他上面繪制了一行黃色的文字蚁滋,這行文字在水平和垂直方向都是居中的宿接;使用TextPaint 繪制文字實(shí)現(xiàn)水平居中是很容易的事情,只需要設(shè)置mTipPaint.setTextAlign(Paint.Align.CENTER)即可辕录,但是在垂直方向?qū)崿F(xiàn)居中睦霎,就沒(méi)那么簡(jiǎn)單了,這里需要考慮一個(gè)文本繪制時(shí)基線(xiàn)的問(wèn)題走诞,具體細(xì)節(jié)可以參考這篇文章副女,分析的很詳細(xì)。
我們?cè)谶@里為了使文字在圓角矩形中居中蚣旱,如下實(shí)現(xiàn)碑幅。
? ? ?
圓角矩形的垂直中心點(diǎn)的基礎(chǔ)上,再一次做修正塞绿,確保實(shí)現(xiàn)真正的垂直居中沟涨。
好了,結(jié)合扔物線(xiàn)大神所總結(jié)的自定義View關(guān)鍵步驟异吻,以上兩點(diǎn)算是完成了繪制和布局的工作,下面就看看觸摸反饋的實(shí)現(xiàn)裹赴。
觸摸反饋-onTouchEvent
這里觸摸反饋機(jī)制,使用到了GestureDetector這個(gè)類(lèi)诀浪;這個(gè)類(lèi)可以用來(lái)進(jìn)行手勢(shì)檢測(cè)棋返,用于輔助檢測(cè)用戶(hù)的單擊、滑動(dòng)雷猪、長(zhǎng)按睛竣、雙擊等行為。內(nèi)部提供了OnGestureListener求摇、OnDoubleTapListener和OnContextClickListener三個(gè)接口射沟,并提供了一系列的方法,比如常見(jiàn)的
onSingleTapUp : 手指輕觸屏幕離開(kāi)
onScroll : 滑動(dòng)
onLongPress: 長(zhǎng)按
onFling: 按下后月帝,快速滑動(dòng)松開(kāi)(類(lèi)似切水果的手勢(shì))
onDoubleTap : 雙擊
可以看到躏惋,使用這個(gè)類(lèi)可以更加精確的處理手勢(shì)操作。
這里引入GestureDetector的原因是這樣的嚷辅,單獨(dú)在onTouchEvent處理所有事件時(shí)簿姨,在手指點(diǎn)擊屏幕的瞬間,很容易觸發(fā)MotionEvent.ACTION_MOVE事件,導(dǎo)致每次觸碰氣泡扁位,被點(diǎn)擊氣泡的位置都會(huì)稍微顫抖一下准潭,位置發(fā)生輕微的偏移,體驗(yàn)十分糟糕域仇。采用GestureDetector對(duì)手指滑動(dòng)的處理刑然,對(duì)點(diǎn)擊和滑動(dòng)的檢測(cè)顯得更加精確
這里m_gestureDetector.onTouchEvent(event),這樣就可以讓GestureDetector在他自己的回調(diào)方法OnGestureListener里暇务,處理觸摸事件泼掠。
上面的邏輯很簡(jiǎn)單,動(dòng)畫(huà)正在進(jìn)行是垦细,直接返回择镇。MotionEvent.ACTION_DOWN事件發(fā)生時(shí)的處理邏輯,通過(guò)注釋很容易理解括改,就不再贅述腻豌。
當(dāng)我們點(diǎn)擊到某個(gè)氣泡時(shí),就獲取到了當(dāng)前選中位置currentPos嘱能;下面看看GestureDetector的回調(diào)方法吝梅,是怎樣處理滑動(dòng)事件的。
SimpleOnGestureListener 默認(rèn)實(shí)現(xiàn)了OnGestureListener惹骂,OnDoubleTapListener, OnContextClickListener這三個(gè)接口中所有的方法苏携,因此非常方便我們使用GestureDetector進(jìn)行特定手勢(shì)的處理。
這里的處理很簡(jiǎn)單析苫,當(dāng)氣泡被選中時(shí)moveEnable=true兜叨,通過(guò)onScroll回調(diào)方法返回的距離,不斷更新當(dāng)前位置的坐標(biāo)衩侥,同時(shí)記得限制一下手勢(shì)滑動(dòng)的邊界国旷,總不能把球員移動(dòng)到場(chǎng)地外面吧o(╯□╰)o,最后的postInvalidate()是關(guān)鍵茫死,觸發(fā)onDraw方法跪但,實(shí)現(xiàn)重新繪制。
這里有一個(gè)細(xì)節(jié)峦萎,不知你發(fā)現(xiàn)沒(méi)有屡久,我們?cè)诟伦鴺?biāo)的時(shí)候,每次都是在當(dāng)前坐標(biāo)的位置爱榔,減去了滑動(dòng)距離(distanceX/distanceY)被环。這是為什么(⊙o⊙)?,為什么不是加呢详幽?
我們可以看看這個(gè)回調(diào)方法的定義
? ?
可以看到筛欢,這里特定強(qiáng)調(diào)了This is NOT the distance between {@code e1}and {@code e2}浸锨,就是說(shuō)這個(gè)距離并不是兩次事件e1和e2 之間的距離。那么這個(gè)距離又是什么呢版姑?那我們就找一找到底是在哪里觸發(fā)了這個(gè)回調(diào)方法.
最終在GestureDetector類(lèi)的onTouchEvent()方法里找到了觸發(fā)這個(gè)方法發(fā)生的地方:
這里還涉及到多指觸控的考慮柱搜,情況較為復(fù)雜;簡(jiǎn)單說(shuō)一下結(jié)論剥险,在ACTION_MOVE時(shí)聪蘸,會(huì)從上一次手指離開(kāi)的距離,減去此次手指觸碰的位置表制;這樣當(dāng)scrollX>0時(shí)健爬,就是在向右滑動(dòng),反之向左夫凸;scrollY > 0 時(shí)浑劳,是在向上滑動(dòng)阱持,反之向下夭拌;因此,這兩個(gè)距離和我們習(xí)以為常的方向恰好都是相反的衷咽,因此鸽扁,在更新坐標(biāo)時(shí),需要做相反的處理镶骗。
有興趣的同學(xué)桶现,可以把上面的“-”改成“+”,嘗試運(yùn)行一下代碼鼎姊,就會(huì)明白其中的道理了骡和。
好了,到了這里按照繪制相寇,布局慰于,觸摸反饋的順序我們已經(jīng)完成了BallGameView這個(gè)自定義View自己的內(nèi)容了,但是我們還看到在點(diǎn)擊下面的球員頭像時(shí)唤衫,還有一個(gè)簡(jiǎn)單的動(dòng)畫(huà)婆赠,下面就看看動(dòng)畫(huà)是如何實(shí)現(xiàn)的。
動(dòng)畫(huà)效果
首先說(shuō)明一下佳励,底部球員列表是一個(gè)橫向的RecyclerView休里,這樣一個(gè)橫向滑動(dòng)的雙列展示的RecyclerView 應(yīng)該很簡(jiǎn)單了,這里就不再詳述赃承。文末有源碼妙黍,最后可以查看。
這里看一下每一個(gè)RecyclerView中item的點(diǎn)擊事件
這里可以看到調(diào)用了GameView的updatePlayer方法:
這個(gè)動(dòng)畫(huà)瞧剖,簡(jiǎn)單來(lái)說(shuō)就是一個(gè)一階貝塞爾曲線(xiàn)拭嫁。根據(jù)RecyclerView中item在屏幕中的位置,構(gòu)造一個(gè)一模一樣的ImageView添加到根視圖中,然后通過(guò)一個(gè)屬性動(dòng)畫(huà)噩凹,在屬性值不斷更新時(shí)巴元,在回調(diào)方法中不斷調(diào)用setTranslation方法,改變這個(gè)ImageView的位置驮宴,呈現(xiàn)出動(dòng)畫(huà)的效果逮刨。動(dòng)畫(huà)結(jié)束后,將這個(gè)ImageView從視圖移除堵泽,同時(shí)氣泡中的數(shù)據(jù)即可修己,最后再次invalidate導(dǎo)致整個(gè)視圖重新繪制,這樣動(dòng)畫(huà)完成時(shí)迎罗,氣泡就被替換為真實(shí)的頭像了睬愤。
到這里,基本上所有功能纹安,都實(shí)現(xiàn)了尤辱。最后就是把自己排出來(lái)的陣型,保存為圖片分享給小伙伴了厢岂。這里主要說(shuō)一下保存圖片的實(shí)現(xiàn)光督;分享功能,就不作為重點(diǎn)討論了塔粒。
自定義View保存為Bitmap
一個(gè)典型的AsyncTask實(shí)現(xiàn)结借,文件流的輸出,沒(méi)什么多說(shuō)的卒茬。主要是存儲(chǔ)目錄的選擇船老,這里有個(gè)技巧,如果沒(méi)有特殊限制圃酵,平時(shí)我們做開(kāi)發(fā)的時(shí)候柳畔,可以 把一些存儲(chǔ)路徑做如下定義
mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES):代表/storage/emulated/0/Android/data/{packagname}/files/Pictures
mContext.getExternalCacheDir() 代表 /storage/emulated/0/Android/data/{packagname}/cache
對(duì)于mContext.getExternalFilesDir還可定義為Environment.DIRECTORY_DOWNLOADS,Environment.DIRECTORY_DOCUMENTS等目錄辜昵,對(duì)應(yīng)的文件夾名稱(chēng)也會(huì)變化荸镊。
這個(gè)目錄中的內(nèi)容會(huì)隨著用戶(hù)卸載應(yīng)用,一并刪除堪置。最重要的是躬存,讀寫(xiě)這個(gè)目錄是不需要權(quán)限的,因此省去了每次做權(quán)限判斷的麻煩舀锨,而且也避免了沒(méi)有權(quán)限時(shí)的窘境岭洲。
到這里,模仿功能坎匿,全部都實(shí)現(xiàn)了盾剩。下面稍微來(lái)一點(diǎn)額外的擴(kuò)展雷激。
我們希望圖片保存后可以在通知欄提示用戶(hù),點(diǎn)擊通知欄后可以通過(guò)手機(jī)相冊(cè)查看保存的圖片告私。
擴(kuò)展-Android ?Notification & FileProvider 的使用
Android 系統(tǒng)中的通知欄屎暇,隨著版本的升級(jí),已經(jīng)形成了固定了寫(xiě)法驻粟,在Builder模式的基礎(chǔ)上根悼,通過(guò)鏈?zhǔn)綄?xiě)法,可以非常方便的設(shè)置各種屬性蜀撑。這里重點(diǎn)說(shuō)一下PendingIntent的用法挤巡,我們知道這個(gè)PendingIntent 顧名思義,就是處于Pending狀態(tài)酷麦,當(dāng)我們點(diǎn)擊通知欄矿卑,就會(huì)觸發(fā)他所包含的Intent。
嚴(yán)格來(lái)說(shuō)沃饶,通過(guò)自己的應(yīng)用想用手機(jī)自帶相冊(cè)打開(kāi)一張圖片是無(wú)法實(shí)現(xiàn)的母廷,因?yàn)闊o(wú)法保證每一種手機(jī)上面相冊(cè)的包名是一樣的,因此這里我們創(chuàng)建ACTION=Intent.ACTION_VIEW的 Intent绍坝,去匹配系統(tǒng)所有符合這個(gè)Action 的Activity徘意,系統(tǒng)相冊(cè)一定是其中之一。
到這里轩褐,還有一定需要注意,Android 7.0 開(kāi)始玖详,無(wú)法以file://xxxx 形式向外部應(yīng)用提供內(nèi)容了把介,因此需要考慮使用FileProvider。當(dāng)然蟋座,對(duì)這個(gè)問(wèn)題拗踢,Google官方提供了完整的使用實(shí)例,實(shí)現(xiàn)起來(lái)都是套路向臀,沒(méi)有什么特別之處巢墅。
重點(diǎn)記住下面的對(duì)應(yīng)關(guān)系即可:
按照上面,我們存儲(chǔ)圖片的目錄券膀,我們?cè)趂ile_path.xml 做如下定義即可:
在AndroidManifest中完成如下配置 :
? ?
這樣君纫,當(dāng)Build.VERSION.SDK_INT大于等于24及Android7.0時(shí),可以安心的使用FileProvider來(lái)和外部應(yīng)用共享文件了芹彬。
最后
好了蓄髓,從一個(gè)簡(jiǎn)單的自定義View 出發(fā),又牽出了一大堆周邊的內(nèi)容舒帮。好在会喝,總算完整的說(shuō)完了陡叠。
特別申明
以上代碼中所用到的圖片資源,全部源自懂球帝APP內(nèi)肢执;此處對(duì)應(yīng)用解包枉阵,只是本著學(xué)習(xí)的目的,沒(méi)有其他任何用意预茄。
源碼地址: Github-AndroidAnimationExercise岭妖。
有興趣的同學(xué)歡迎 star & ?fork。
如果你有好的文章想和大家分享歡迎投稿反璃,直接向我投遞文章鏈接即可昵慌。