Androidバージョンの違いをCustomViewで吸収する


新し目のバージョンのAndroidにしか無いViewを使いたいが、xmlをバージョンで分けたりしたくない
そこで、そういったViewをラップするCustomViewを作る

今回、ICS以上で`TextureView`、それ以下で`SurfaceView`を使ったカメラを実装した

# リポジトリ

https://github.com/petitviolet/MultiCamera
記事中のコードはこのリポジトリの一部抜粋となっています

## ライブラリとして使用

Githubにaarをアップロードしたので、以下のように記述するとライブラリとして使用できます

```groovy:build.gradle
repositories {
    maven {
        url "https://raw.githubusercontent.com/petitviolet/MultiCamera/master/repository/"
    }
}

dependencies {
    compile "net.petitviolet:multicamera:0.1.0"
}
```

# 実装方法

Androidバージョンに関係なくlayoutファイルでCamera領域を指定できるように親となるCustomViewを作る

layoutでは普通のViewと同様に指定する

```xml
<net.petitviolet.viewsample.view.CameraView
    android:id="@+id/camera"
    android:layout_width="match_parent"
    android:layout_height="500dp"/>
```

## CustomViewの実装

`CameraView`は以下の様な感じ

```java:CameraView.java

public class CameraView extends FrameLayout {
    private static final String TAG = CameraView.class.getSimpleName();
    protected Context mContext;
    protected View mView;
    protected Camera mCamera;

    public CameraView(Context context) {
        super(context);
        mContext = context;
    }

    public CameraView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
    }

    public CameraView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mContext = context;
    }


    protected void setView() {
        // Must be Override
    }

    public void show() {
        Log.d(TAG, "show");
        // Must be Override
    }
    
    public CameraView initView() {
        ViewGroup parentView = (ViewGroup) getParent();
        int position = parentView.indexOfChild(this);
        CameraView newView;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            newView = new CameraSurfaceView(mContext);
        } else {
            newView = new CameraTextureView(mContext);
        } 
        newView.setLayoutParams(getLayoutParams());
        newView.setId(getId());
        newView.setView();

        parentView.removeViewAt(position);
        parentView.addView(newView, position);
        newView.setView();
        return newView;
    }
}
```

このViewの肝は`initView`で、Androidバージョンに合わせたView、この場合は`CameraSurfaceView`と`CameraTextureView`のインスタンスを新しく作成し、layoutの中で自分が置かれた場所から自分自身と入れ替えるようにしている
`setView`および`show`は、`CameraView`では何も実装がないが、abstract classではlayoutに配置できないため、空のメソッドを置いて継承したクラスで実装することとなる

継承した`CameraSurfaceView`は以下

```java:CameraSurfaceView
public class CameraSurfaceView extends CameraView {
    private static final String TAG = CameraSurfaceView.class.getSimpleName();
    private SurfaceView mSurfaceView;

    public CameraSurfaceView(Context context) {
        super(context);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void setView() {
        mView = LayoutInflater.from(mContext).inflate(R.layout.camera_surface, this, false);
        addView(mView);
        mSurfaceView = (SurfaceView) mView.findViewById(R.id.camera_surface);
    }

    @Override
    public void show() {
        // カメラの準備
        SurfaceHolder holder = mSurfaceView.getHolder();
        holder.addCallback(callback);
        Log.d(TAG, "show");
    }
    
    public void takePicture(TakePictureCallback callback) {
        // ...
    }
    
    public interface TakePictureCallback {
        void onSuccess(byte[] data);
        void onFail();
    }
}
```

`setView`でlayoutを`inflate`して作ったViewを`addView`して自分自身のViewを構築している
`inflate`するxmlは`SurfaceView`を置いてあるだけの簡単なものにしている

```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <SurfaceView
        android:id="@+id/camera_surface"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
```

また、`CameraTexureView`における`setView`と`show`は`TextureView`を初期化している

```java:CameraTextureView
    @Override
    protected void setView() {
        mView = LayoutInflater.from(mContext).inflate(R.layout.camera_texture, this, false);
        addView(mView);
        mTextureView = (TextureView) mView.findViewById(R.id.camera_texture);
    }

    @Override
    public void show() {
        Log.d(TAG, "show");
        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    }
```

## 使う側の実装

`findViewById`で取得した`CameraView`の`initView`をcallして、`CamereSurfaceView`か`CameraTextureView`の適切な方を生成する
この場合はその後に`Camera`の起動などを行う`show`メソッドを呼ぶことでカメラとして使用できるようになる

```java:MainActivity
public class MainActivity extends AppCompatActivity {
    CameraView cameraView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cameraView = ((CameraView) findViewById(R.id.camera)).initView();

        findViewById(R.id.take_picture).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cameraView.takePicture(new CameraView.TakePictureCallback() {
                    @Override
                    public void onSuccess(byte[] data) {
                        Log.d("MainActivity", "onSuccess");        
                    }

                    @Override
                    public void onFail() {
                        Log.d("MainActivity", "onFail");
                    }
                });
            }
        });
        cameraView.show();
    }    
}    
```

# 所感

- バージョンによる分岐処理をViewの中に隠蔽できるため、使う側で考えなくて良いというのが個人的には気に入っている
	- ライブラリとしてCustomViewを提供する場合に便利だと思う
- layoutに使用するViewとしてabstract classを使えるようになると、空メソッドを置かずにabstractメソッドとして用意できるためもう少し綺麗に書けそうだが、実現はしない気がする
- Lollipop以上で`Camera2`を使った実装をしたかったが、`Camera2`が複雑で面倒だったので途中で断念してしまった