InstaMaterial 概念设计(第九部分) - 图片的发布

这篇文章是演示如何实现Material风格的INSTAGRAM系列文章的最后一部分。今天我们将创建最后两个元素 - PublishActivity 和 SendingProgressView,然后结束这篇文章。这个功能在视频的41 - 49秒之间。

根据今天所讲的的源码编译的apk在 这里 。是的这是最后一个版本了。

下面是本文实现的最终效果(演示整个项目效果的视频将在下一章的总结总放出):

介绍

这篇文章不同于以往。我不会关注所改变的每一行代码,许多都是公式化的简单代码,有些部分已经在前面的一篇文章中讲述过了,这就是为什么我只关注一个细节,而不是所有的代码。

上传进度

我们先来讲讲这个版本改动最大的部分 - SendingProgressView。这个元素可能是本项目自定义程度最高的view了。很好,我们有机会练习下view绘制方面的技术了。

这个view有四种状态:

  • STATE_NOT_STARTED - 此时无需绘制

  • STATE_PROGRESS_STARTED - 绘制代表当前进度的扇形

  • STATE_DONE_STARTED - 完成元素的动画 (背景和选中标记正在进入)

  • STATE_FINISHED - 具有完整的进度圆圈以及背景的静态视图

下面是不同阶段的实际效果:

Progress states

在实现SendingProgressView之前,你需要知道一个关于绘制的重要规则。如果你想达到最佳的性能,必须将绘制和工具的初始化飞开。因为onDraw()会被频繁的调用,我们不应该在里面做任何内存分配的事情。内存分配过程可能导致垃圾回收从而导致卡顿。

初始化的最佳地方是onSizeChanged()方法中()。本人一般是在构造方法中,shit、、、 - 译者注。

ProgressBar

Progress

先从进度条和STATE_PROGRESS_STARTED状态开始。就是个简单的扇形线条。只需准备一个STROKE 样式(我们只是想绘制轮廓,不需要填充)的, 抗锯齿(因为这是一个圆形)的带颜色和线条宽度的paint:

SendingProgressView1.java

private void setupProgressPaint() {
    progressPaint = new Paint();
    progressPaint.setAntiAlias(true);
    progressPaint.setStyle(Paint.Style.STROKE);
    progressPaint.setColor(0xffffffff);
    progressPaint.setStrokeWidth(PROGRESS_STROKE_SIZE);
}

这个方法在SendingProgressView的构造方法中被调用(paint只初始化一次)。

绘制进度更简单,一行代码:

SendingProgressView2.java

private void drawArcForCurrentProgress() {
    tempCanvas.drawArc(progressBounds, -90f, 360 * currentProgress / 100, false, progressPaint);
}

progressBounds参数是一个可以填满整个view的矩形(在onSizeChanged()方法中测量的),其余的参数都很简单。这个方法在下面的onDraw()中被调用:

SendingProgressView3.java

@Override
protected void onDraw(Canvas canvas) {
    if (state == STATE_PROGRESS_STARTED) {
        drawArcForCurrentProgress();
    }
 
    //..
 
    canvas.drawBitmap(tempBitmap, 0, 0, null);
}

你可能会疑惑为什么我们用tempCanvas/tempBitmap,而不是用onDraw()中给与的canvas参数。这是有意的,用于遮罩层进度,我们稍后会回头解释这个问题。

因为我们的项目只是一个UI壳子,因此需要模拟一个进度效果,我们这里用animator来做:

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状态中):

setCurrentProgress(0);
simulateProgressAnimator.start();

Done animation

注:这一小节感觉我翻译不好,吃力的地方英汉对照。

现在我们要准备“完成”动画,它需要在进度更新到100%的时候立即被触发。看看我们想到达到的效果:

Done animation

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。这个模式的实际效果是将我们的图像裁剪成采用的层,下面是效果:

Masking

为了实现这种效果,我们先配置画笔:

SendingProgressView6.java

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()方法中):

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:

@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()以分配下一帧的动画。背景和标记的当前位置是从两个动画中得到的:

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的完整代码。

最终效果:
Progress view

Activities 栈的管理

根据概念视频中的演示,用户点击完成按钮之后,MainActivity被带到了前台(而不是打开一个新的Activity),内容滚动到了第一个item,下面是通过Intent flags实现这种效果的代码片段:

// 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
    的这个方法将被调用:

// 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一样会从栈中去掉,就如刚刚我说的,那是因为没有它会重建。

下面是其工作机制:

Backstack

实际上,这就是今天的全部内容了。当然 最近的一次提交  要比我今天所讲的代码要多的多。但是所用的技术都在前面已经讲过。

我们终于把Emmanuel Pacamalan的概念视频 Instagram with Material Design concept  实现了。同时我们也证明了视频中的所有效果都是可以在老版本上实现的。谢谢大家的阅读与分享,希望今后的项目中依然能在这里和大家交流!

源代码


项目的完整代码:repository

作者: Miroslaw Stanek

写于  2015.3.9

来自:InstaMaterial概念设计