這篇文章是演示如何實現Material風格的INSTAGRAM系列文章的最後一部分。今天我們將建立最後兩個元素 – PublishActivity 和 SendingProgressView,然後結束這篇文章。這個功能在視訊的41 – 49秒之間。
根據今天所講的的原始碼編譯的apk在 這裡 。是的這是最後一個版本了。
下面是本文實現的最終效果(演示整個專案效果的視訊將在下一章的總結總放出):
介紹
這篇文章不同於以往。我不會關注所改變的每一行程式碼,許多都是公式化的簡單程式碼,有些部分已經在前面的一篇文章中講述過了,這就是為什麼我只關注一個細節,而不是所有的程式碼。
上傳進度
我們先來講講這個版本改動最大的部分 – SendingProgressView。這個元素可能是本專案自定義程度最高的view了。很好,我們有機會練習下view繪製方面的技術了。
這個view有四種狀態:
- STATE_NOT_STARTED – 此時無需繪製
- STATE_PROGRESS_STARTED – 繪製代表當前進度的扇形
- STATE_DONE_STARTED – 完成元素的動畫 (背景和選中標記正在進入)
- STATE_FINISHED – 具有完整的進度圓圈以及背景的靜態檢視
下面是不同階段的實際效果:
在實現SendingProgressView之前,你需要知道一個關於繪製的重要規則。如果你想達到最佳的效能,必須將繪製和工具的初始化飛開。因為onDraw()會被頻繁的呼叫,我們不應該在裡面做任何記憶體分配的事情。記憶體分配過程可能導致垃圾回收從而導致卡頓。
初始化的最佳地方是onSizeChanged()方法中()。本人一般是在構造方法中,shit、、、 – 譯者注。
ProgressBar
先從進度條和STATE_PROGRESS_STARTED狀態開始。就是個簡單的扇形線條。只需準備一個STROKE 樣式(我們只是想繪製輪廓,不需要填充)的, 抗鋸齒(因為這是一個圓形)的帶顏色和線條寬度的paint:
1 2 3 4 5 6 7 |
private void setupProgressPaint() { progressPaint = new Paint(); progressPaint.setAntiAlias(true); progressPaint.setStyle(Paint.Style.STROKE); progressPaint.setColor(0xffffffff); progressPaint.setStrokeWidth(PROGRESS_STROKE_SIZE); } |
這個方法在SendingProgressView的構造方法中被呼叫(paint只初始化一次)。
繪製進度更簡單,一行程式碼:
1 2 3 |
private void drawArcForCurrentProgress() { tempCanvas.drawArc(progressBounds, -90f, 360 * currentProgress / 100, false, progressPaint); } |
progressBounds引數是一個可以填滿整個view的矩形(在onSizeChanged()方法中測量的),其餘的引數都很簡單。這個方法在下面的onDraw()中被呼叫:
1 2 3 4 5 6 7 8 9 10 |
@Override protected void onDraw(Canvas canvas) { if (state == STATE_PROGRESS_STARTED) { drawArcForCurrentProgress(); } //.. canvas.drawBitmap(tempBitmap, 0, 0, null); } |
你可能會疑惑為什麼我們用tempCanvas/tempBitmap,而不是用onDraw()中給與的canvas引數。這是有意的,用於遮罩層進度,我們稍後會回頭解釋這個問題。
因為我們的專案只是一個UI殼子,因此需要模擬一個進度效果,我們這裡用animator來做:
1 2 3 4 5 6 7 8 9 10 |
private void setupSimulateProgressAnimator() { simulateProgressAnimator = ObjectAnimator.ofFloat(this, "currentProgress", 0, 100).setDuration(2000); simulateProgressAnimator.setInterpolator(new AccelerateInterpolator()); simulateProgressAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { changeState(STATE_DONE_STARTED); } }); } |
這也是在建構函式中初始化的。simulateProgressAnimator將在2000毫秒內讓浮點值從0.f到100.f動畫變換。進度的更新是使用setCurrentProgress(float)方法。
現在我們需要呼叫(在changeState的STATE_PROGRESS_STARTED狀態中):
1 2 |
setCurrentProgress(0); simulateProgressAnimator.start(); |
Done animation
注:這一小節感覺我翻譯不好,吃力的地方英漢對照。
現在我們要準備“完成”動畫,它需要在進度更新到100%的時候立即被觸發。看看我們想到達到的效果:
Pretty simple, right? Just circular background with checkmark image. But here is one important detail. In animation time both, background and checkmark are coming form the bottom. In this time we should clip views in circular shape to avoid crossing them with progress circle. That’s why we have to play with masking process. In short, we have to provide circle mask which will cut animated views in intended way.
To do this we have two recomended ways – by using a shaders or by playing with Porter-Duff blending modes. For the first one, take a look on Romain Guy’s recipe.
In our project we’ll try blending modes way.
很簡單,是吧?只是圓形的背景和一個標記圖片。但是有一個重要的細節。在動畫期間,背景和標記都是從底部過來的。這次我們需要裁剪一個圓形來避免和進度圓的交叉。這就是我們為什麼要使用遮罩的原因。簡而言之,我們需要提供圓形的遮罩層使得我們按照期望的效果來裁剪view。
為了做到這點,有兩種推薦的方法 – 使用shaders或者使用Porter-Duff blending模式。對於第一種,可以看看n Romain Guy的文章。
我們的專案中將使用blending模式的方法。
Porter/Duff Compositing and Blend Modes
In short this process is all about combining two images. PorterDuff defines how to compose images based on the alpha value (Alpha compositing). Just check this article: Porter/Duff Compositing and Blend Modes to see what possibilities of blending we’ve got.
In our view we want to apply the alpha mask and that’s why we use PorterDuff.Mode.DST_IN. In practice this mode will clip our image to exact shape of applied mask. Here is visualisation of it:
這個過程其實就是關於兩張圖片的合成。PorterDuff定義瞭如何根據alpha合成影象(Alpha compositing)。檢視blending可以做到的效果可以參考這篇文章Porter/Duff Compositing and Blend Modes 。
我們的view中我想採用alpha mask 這也是為什麼我使用PorterDuff.Mode.DST_IN。這個模式的實際效果是將我們的影象裁剪成採用的層,下面是效果:
為了實現這種效果,我們先配置畫筆:
1 2 3 4 5 6 7 8 9 10 11 |
private void setupDonePaints() { doneBgPaint = new Paint(); doneBgPaint.setAntiAlias(true); doneBgPaint.setStyle(Paint.Style.FILL); doneBgPaint.setColor(0xff39cb72); checkmarkPaint = new Paint(); maskPaint = new Paint(); maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); } |
First one is used for done background, second for drawing checkmark bitmap and the third for masking. This method is called in our View’s counstructor.
Now let’s prepare our mask (called from onSizeChanged() method):
第一個是用於“完成”的背景,第二個是繪製編輯的bitmap,第三個是用於mask。這個方法在view的構造方法中被呼叫。
現在讓我們準備mask(在onSizeChanged()方法中):
1 2 3 4 5 |
private void setupDoneMaskBitmap() { innerCircleMaskBitmap = Bitmap.createBitmap(getWidth(), getWidth(), Bitmap.Config.ARGB_8888); Canvas srcCanvas = new Canvas(innerCircleMaskBitmap); srcCanvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2 - INNER_CIRCLE_PADDING, new Paint()); } |
最後我們繪製 done animation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Override protected void onDraw(Canvas canvas) { //... } else if (state == STATE_DONE_STARTED) { drawFrameForDoneAnimation(); postInvalidate(); } else if (state == STATE_FINISHED) { drawFinishedState(); } //... canvas.drawBitmap(tempBitmap, 0, 0, null); } private void drawFrameForDoneAnimation() { tempCanvas.drawCircle(getWidth() / 2, getWidth() / 2 + currentDoneBgOffset, getWidth() / 2 - INNER_CIRCLE_PADDING, doneBgPaint); tempCanvas.drawBitmap(checkmarkBitmap, checkmarkXPosition, checkmarkYPosition + currentCheckmarkOffset, checkmarkPaint); tempCanvas.drawBitmap(innerCircleMaskBitmap, 0, 0, maskPaint); tempCanvas.drawArc(progressBounds, 0, 360f, false, progressPaint); } |
As you can see first we’re drawing background and checkmark, then the mask. postInvalidate() called in onDraw()method schedules next frame of animation. Current position for background and checkmark is taken from two animators:
正如你看到的首先我們繪製背景和編輯,然後是mask。在onDraw()方法中呼叫postInvalidate()以分配下一幀的動畫。背景和標記的當前位置是從兩個動畫中得到的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private void setupDoneAnimators() { doneBgAnimator = ObjectAnimator.ofFloat(this, "currentDoneBgOffset", MAX_DONE_BG_OFFSET, 0).setDuration(300); doneBgAnimator.setInterpolator(new DecelerateInterpolator()); checkmarkAnimator = ObjectAnimator.ofFloat(this, "currentCheckmarkOffset", MAX_DONE_IMG_OFFSET, 0).setDuration(300); checkmarkAnimator.setInterpolator(new OvershootInterpolator()); checkmarkAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { changeState(STATE_FINISHED); } }); } |
Now a couple words about tempCanvas. As I said, we use it intentionally. By using PorterDuff and blending modes we play with alpha channel. And this method works only in a Bitmaps filled by transparency. When we draw directly on canvas which is given as onDraw() parameter, the destination is already filled by the window background. That’s why you can see black color instead of nothing (transparency).
The rest of code is pretty straightforward. Instead of reading about it just check the full source code of SendingProgressView.
Final result:
現在我們講幾句tempCanvas。我們已經講過,我們是故意使用的。alpha通道的處理中使用了PorterDuff和blending模式,而這個方法,只有在被透明色填充的bitmap中才會起作用。如果我們直接使用onDraw()方法中給的canvas引數。目標bitmap已經被window的背景填充,那樣你就可以看見黑色額不是透明的。其餘的程式碼doing很簡單。直接檢視 SendingProgressView的完整程式碼。
最終效果:
Activities 棧的管理
根據概念視訊中的演示,使用者點選完成按鈕之後,MainActivity被帶到了前臺(而不是開啟一個新的Activity),內容滾動到了第一個item,下面是通過Intent flags實現這種效果的程式碼片段:
1 2 3 4 5 6 7 8 |
// PublishActivity.java private void bringMainActivityToTop() { Intent intent = new Intent(this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setAction(MainActivity.ACTION_SHOW_LOADING_ITEM); startActivity(intent); } |
- Intent.FLAG_ACTIVITY_SINGLE_TOP – 在已經執行在頂棧的時候不會再次重新啟動。沒有這個flag,第二個會將原來的除掉,然後再重建一個。
- Intent.FLAG_ACTIVITY_CLEAR_TOP 如果被呼叫的Activity已經在執行,所有在它之上的Activity都將關閉,不會啟動這個Activity的新例項,而是將這個intent傳遞給原來的activity。在我們的場景中,MainActivity
的這個方法將被呼叫:
1 2 3 4 5 6 7 8 9 |
// MainActivity.java @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (ACTION_SHOW_LOADING_ITEM.equals(intent.getAction())) { showFeedLoadingItemDelayed(); } } |
那麼為什麼我們需要FLAG_ACTIVITY_SINGLE_TOP呢?沒有它MainActivity一樣會從棧中去掉,就如剛剛我說的,那是因為沒有它會重建。
下面是其工作機制:
實際上,這就是今天的全部內容了。當然 最近的一次提交 要比我今天所講的程式碼要多的多。但是所用的技術都在前面已經講過。
我們終於把Emmanuel Pacamalan的概念視訊 Instagram with Material Design concept 實現了。同時我們也證明了視訊中的所有效果都是可以在老版本上實現的。謝謝大家的閱讀與分享,希望今後的專案中依然能在這裡和大家交流!
原始碼
專案的完整程式碼:repository。