InstaMaterial 概念设计(第八部分) - 拍照功能

英文原文 InstaMaterial concept (part 8) - Capturing photo

转载译文须注明出处

今天,我们将实现照相功能,这个功能在演示在概念视频的38 到 41秒之间。因为相机的实现略为复杂,我们将忽略一些细节(比如FAB 按钮的动画,颜色,图标)。在下一章(也可能是最后一章)中回头再讲他们。

我必须提醒的是因为只是产权的原因,InstaMaterial已经从Google Play Store下架。这点我完全理解,在也许会准备一个布局和Instagram完全不一样的版本(但是概念视频中的所有效果都将保留)。

今天的代码所编译的apk文件可以在 这里 下载。

回到我们的文章内容- 下面是今天要实现的最终效果:

介绍

今天所描述的几乎所有动画和解决办法都在前面的文章中已经提到了,这也是为什么我不再把这些细节作为重点的原因。但是,有一个完全新的东西- 相机。接触过相机编程的同学应该知道这个组建实现起来是很让人为难的,为什么呢?简单的来说就是:安卓是一个开放的平台,需要适配许多硬件完全不同(尤其是相机)的设备。因此如果你和照相打交道,你需要关注以下事情:

  • 不同的预览和捕获到的图像尺寸

    每一个设备都只支持有限的尺寸(不管是预览还是拍照的图像)。记住,有些设备不能处理1:1的比率(squared image) - 实际上 Nexus 5 就是这样的设备之一。

  • 后置/前置摄像头
    每个设备都应该有前置和后置摄像头,当然有些只有后置摄像头,不过记住,有些设备只有前置摄像头(比如2012年的Nexus 7),这些设备的拍出来的像素大小都是不一样的。

  • 相机所支持的功能也不一样

    有些设备不支持自动对焦,还有一些不支持变焦,在使用这些功能之前先应该先检查它们是否存在。

  • 相机的操作开销很大

    始终记住维护图像(尤其是高分辨率的图像)是开销很大的一件事。图像处理不轻松,你需要关注内存的管理并且在UI主线程之外计算。另外相机被调用之后并不会立即可以使用,有时候你需要等待它做一些初始化工作。

考虑到以上描述的几点,我决定使用第三方的库来处理相机操作,而不是自己写代码实现。我选择了CWAC-Camera 库 ,虽然实现起来还是比较复杂,但是它处理好了很多比较偏的问题。即使作者计划完全重写它,但是在我看来,也很难找到更好的方法来处理图像和视频了。

最后提一点,InstaMaterial app中所实现的相机功能非常有限。没有图片文件的处理同时显示捕获照片也是用的比较取巧的方法。在用在产品级应用之前还需要做很多工作(尤其是图片裁剪,图片的合适尺寸等)。不过我相信这些代码作为更复杂项目的基本代码框架还是非常不错的。

准备

如往常一样,先添加资源做一些琐碎的准备工作吧。也有一些库的更新,下面是新的代码结构:

Project structure

别把它看成是最终的参考,但是在实际项目中这种结构是我的最爱,UI,逻辑,数据被分开(另一种是根据功能分,但是在小项目中使用会觉得比较累赘)。

下面是提交到东西:

CWAC-Camera 配置

Camera库的安装很简单。只需在 /build.gradle中添加新的maven repository:

repositories {
    maven {
        url "https://repo.commonsware.com.s3.amazonaws.com"
    }
}

在 /app/build.gradle中添加新的依赖:

dependencies {
    //...
    compile 'com.commonsware.cwac:camera:0.6.12'
}

最后需要在 AndroidManifest.xml 文件中添加新的权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.github.froger.instamaterial">
 
    <!--...-->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.camera.front"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera.autofocus"
        android:required="false" />
 
    <application
        android:name=".InstaMaterialApplication"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        
        <!--...-->
        
    </application>
 
</manifest>

照相界面

现在可以开始实现捕获的屏幕了。我考虑了两种实现方式 - 用两个Activity分别用于照相和编辑 或者 两者都在一个Activity中完成。第一种选择也许更合理些(在我们的正式项目中也也许就是采用这种方案),但是我还是选择了第二种办法。为什么?因为没有一种简单的方法使从相机预览过渡到图片预览不会卡顿。默认情况相机将拍摄的照片保存在内存卡中,另外一个Activity需要从那里读取,不幸的是这会产生一点延迟,因此我们需要想办法让bitmap在两个Activity之间传递(这个过程要比通过Intent传递bitmap或者将bitmap作为静态变量保存要复杂得多)。

让我们从入场动画开始。Activity 过渡我们可以使用 上篇文章 中介绍的RevealBackgroundView。我们需要再一次将起始位置(FAB按钮的位置)传递过去,唯一不同的是这次我们需要添加新的样式来隐藏TakePhotoActivity中的状态栏:

<?xml version="1.0" encoding="utf-8"?>
<!-- styles.xml-->
<resources>
 
    <!--...-->
 
    <style name="AppTheme.TransparentActivity.FullScreen">
        <item name="android:windowFullscreen">true</item>
    </style>
 
    <!--...-->
 
</resources>

在AndroidManifest中设置

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.github.froger.instamaterial">
 
    <!--...-->
    
        <activity
            android:name=".ui.activity.TakePhotoActivity"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.TransparentActivity.FullScreen" />
    
    <!--...-->
 
</manifest>

所有的改动在这里

TakePhotoActivity 的布局

现在我们来准备拍照界面布局。正如我提到的,它需要处理两种状态 - 拍照和修改照片参数。这就是我们为什么要实现元素的过渡。这个问题我们使用ViewSwitcher来完成(在我们的项目中曾使用过继承自ViewSwitcher的TextSwitcher来实现like计数的动画)。

在开始布局的实现之前,先为拍摄按钮准备一个背景:

Capture button

还有拍摄选项的背景:

Capture options button

第一个我们可以在xml中创建。可以使用带有两个圆圈和圈圈边框的layer-list:

<?xml version="1.0" encoding="utf-8"?>
<!--btn_capture.xml-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="oval">
            <solid android:color="#458dca" />
        </shape>
    </item>
    <item
        android:bottom="16dp"
        android:left="16dp"
        android:right="16dp"
        android:top="16dp">
        <shape android:shape="oval">
            <solid android:color="#529bd8" />
        </shape>
    </item>
    <item>
        <shape android:shape="oval">
            <stroke
                android:width="2dp"
                android:color="#ffffff"
                android:dashWidth="0dp" />
        </shape>
    </item>
</layer-list>

第二个更简单 - 就是一个圆圈边框(里面的图片是项目的资源文件提供的)

<?xml version="1.0" encoding="utf-8"?>
<!--btn_capture_options.xml-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <stroke
        android:width="1dp"
        android:color="#ffffff"
        android:dashWidth="0dp" />
</shape>

记住这两者都没有处理onClick以及enable/disable状态,你需要自己去做。

现在我们有了TakePhotoActivity布局所需的所有元素。实现有点稍微复杂,这也是为什么我没有展示代码的原因。相反我们先看看布局的视觉效果:

Take photo layout

顶部和底部面板从屏幕的右侧滑入(多亏了ViewSwitcher)。拍摄的照片(右边的白色区域)显示在CameraView(CWAC-Camera 库的一个类)的上面。

这里是xml文件的 完整代码 。(代码很长就不贴在这里了-译者注)。

拍照视图的实现

终于可以开始实现最重要的部分了- 拍照。简单描述需求如下:

.入场动画(在reveal动画结束之后立刻滑入顶部和底部面板)

.从camera捕获照片

.在拍照与图像编辑两个状态之间动画切换

第一点很简单,只须在Acitivity启动的时候隐藏顶部和底部面板然后在背景的reveal动画完成之后显示它们。

TakePhotoActivity_intro.java

public class TakePhotoActivity extends BaseActivity implements RevealBackgroundView.OnStateChangeListener {
 
    @InjectView(R.id.vUpperPanel)
    ViewSwitcher vUpperPanel;
    @InjectView(R.id.vLowerPanel)
    ViewSwitcher vLowerPanel;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //...
        
        vUpperPanel.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                vUpperPanel.getViewTreeObserver().removeOnPreDrawListener(this);
                vUpperPanel.setTranslationY(-vUpperPanel.getHeight());
                vLowerPanel.setTranslationY(vLowerPanel.getHeight());
                return true;
            }
        });
    }
 
    @Override
    public void onStateChange(int state) {
        if (RevealBackgroundView.STATE_FINISHED == state) {
            vTakePhotoRoot.setVisibility(View.VISIBLE);
            startIntroAnimation();
        } else {
            vTakePhotoRoot.setVisibility(View.INVISIBLE);
        }
    }
 
    private void startIntroAnimation() {
        vUpperPanel.animate().translationY(0).setDuration(400).setInterpolator(DECELERATE_INTERPOLATOR);
        vLowerPanel.animate().translationY(0).setDuration(400).setInterpolator(DECELERATE_INTERPOLATOR).start();
    }
 
    //...
}

Photo capturing

现在我们需要配置CameraView,根据 CWAC-Camera 的文档 我们需要将这个view和Activity的声明周期整合(onResume() 和 onPause()方法):

@Override
protected void onResume() {
    super.onResume();
    cameraView.onResume();
}
@Override
protected void onPause() {
    super.onPause();
    cameraView.onPause();
}

如果你要在布局中使用CameraView (而不是使用CameraFragment),那么Activity必须实现CameraHostProvider 接口。它提供返回CameraHost的getCameraHost()方法。这个接口负责camera的配置。为了满足我们的需求,我们创建了一个非常简单的MyCameraHost 内部类:

MyCameraHost.java

class MyCameraHost extends SimpleCameraHost {
 
    private Camera.Size previewSize;
 
    public MyCameraHost(Context ctxt) {
        super(ctxt);
    }
 
    @Override
    public boolean useFullBleedPreview() {
        return true;
    }
 
    @Override
    public Camera.Size getPictureSize(PictureTransaction xact, Camera.Parameters parameters) {
        return previewSize;
    }
 
    @Override
    public Camera.Parameters adjustPreviewParameters(Camera.Parameters parameters) {
        Camera.Parameters parameters1 = super.adjustPreviewParameters(parameters);
        previewSize = parameters1.getPreviewSize();
        return parameters1;
    }
 
    @Override
    public void saveImage(PictureTransaction xact, final Bitmap bitmap) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                showTakenPicture(bitmap);
            }
        });
    }
}

以上代码简单的说就是让预览和捕获图像的大小一致(这样捕获图像和预览图像相比就不会显得有拉伸)。useFullBleedPreview()的意思和ImageView中的缩放类型centerCrop差不多。因此如果CameraView
的预览和view的布局参数(layout params)不一致,那么预览图像将被裁剪然后填充满可用区域。

saveImage() 方法在捕获操作的最后被调用()(因此即使我们修改了bitmap也不会影响到保存在手机内存中的文件),不过要记住saveImage()方法只有在takePicture()的参数合适的时候才会被调用

我不会深入讲解CameraHost剩余的其他代码,你直接去参考 CWAC-Camera 文档查看更多信息吧。

现在我们要做的剩下的事情是调用takePicture() 方法。它有两个布尔类型的参数needBitmap 和needByteArray。如果第一个是true, 我们的CameraHost实现中saveImage(PictureTransaction xact, final Bitmap bitmap)将会被调用。第二个(为true?)是调用saveImage(PictureTransaction xact, byte[] image)。

当然如果我们想要看到快门效果就需要自己去实现了,这里是本项目中的大致样子:

TakePhotoActivity_take_photo.java

public class TakePhotoActivity extends BaseActivity implements RevealBackgroundView.OnStateChangeListener,
        CameraHostProvider {
 
    //...
    
    @OnClick(R.id.btnTakePhoto)
    public void onTakePhotoClick() {
        btnTakePhoto.setEnabled(false);
        cameraView.takePicture(true, false);
        animateShutter();
    }
 
    private void animateShutter() {
        vShutter.setVisibility(View.VISIBLE);
        vShutter.setAlpha(0.f);
 
        ObjectAnimator alphaInAnim = ObjectAnimator.ofFloat(vShutter, "alpha", 0f, 0.8f);
        alphaInAnim.setDuration(100);
        alphaInAnim.setStartDelay(100);
        alphaInAnim.setInterpolator(ACCELERATE_INTERPOLATOR);
 
        ObjectAnimator alphaOutAnim = ObjectAnimator.ofFloat(vShutter, "alpha", 0.8f, 0f);
        alphaOutAnim.setDuration(200);
        alphaOutAnim.setInterpolator(DECELERATE_INTERPOLATOR);
 
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playSequentially(alphaInAnim, alphaOutAnim);
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                vShutter.setVisibility(View.GONE);
            }
        });
        animatorSet.start();
    }
 
    //...
}

最后一件事 - 我们需要处理拍摄的照片并显示。本例中是这样工作的:在CameraView之上我们有一个隐藏的ImageView,只要从 saveImage()方法获得一个新的bitmap,我们就将它放到ImageView中,并在CameraView的上面显示。按下返回之后ImageView将隐藏,用户又可以看到CameraView了,下面是代码的大致样子:

public class TakePhotoActivity extends BaseActivity implements RevealBackgroundView.OnStateChangeListener,
        CameraHostProvider {
    
    //...
 
    private void showTakenPicture(Bitmap bitmap) {
        vUpperPanel.showNext();
        vLowerPanel.showNext();
        ivTakenPhoto.setImageBitmap(bitmap);
        updateState(STATE_SETUP_PHOTO);
    }
 
    @Override
    public void onBackPressed() {
        if (currentState == STATE_SETUP_PHOTO) {
            btnTakePhoto.setEnabled(true);
            vUpperPanel.showNext();
            vLowerPanel.showNext();
            updateState(STATE_TAKE_PHOTO);
        } else {
            super.onBackPressed();
        }
    }
 
    private void updateState(int state) {
        currentState = state;
        if (currentState == STATE_TAKE_PHOTO) {
            vUpperPanel.setInAnimation(this, R.anim.slide_in_from_right);
            vLowerPanel.setInAnimation(this, R.anim.slide_in_from_right);
            vUpperPanel.setOutAnimation(this, R.anim.slide_out_to_left);
            vLowerPanel.setOutAnimation(this, R.anim.slide_out_to_left);
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    ivTakenPhoto.setVisibility(View.GONE);
                }
            }, 400);
        } else if (currentState == STATE_SETUP_PHOTO) {
            vUpperPanel.setInAnimation(this, R.anim.slide_in_from_left);
            vLowerPanel.setInAnimation(this, R.anim.slide_in_from_left);
            vUpperPanel.setOutAnimation(this, R.anim.slide_out_to_right);
            vLowerPanel.setOutAnimation(this, R.anim.slide_out_to_right);
            ivTakenPhoto.setVisibility(View.VISIBLE);
        }
    }
}

这里是TakePhotoActivity的完整代码。

这就是今天的全部内容了,谢谢阅读!

源代码

项目的完整代码:repository

作者: Miroslaw Stanek

写于 2015年2月14

来自:InstaMaterial概念设计