petitviolet blog

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

    2015-06-01

    QiitaAndroid

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

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

    リポジトリ

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

    ライブラリとして使用

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

    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 と同様に指定する

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

    CustomView の実装

    CameraViewは以下の様な感じ

    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、この場合はCameraSurfaceViewCameraTextureViewのインスタンスを新しく作成し、layout の中で自分が置かれた場所から自分自身と入れ替えるようにしている setViewおよびshowは、CameraViewでは何も実装がないが、abstract class では layout に配置できないため、空のメソッドを置いて継承したクラスで実装することとなる

    継承したCameraSurfaceViewは以下

    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 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におけるsetViewshowTextureViewを初期化している

    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で取得したCameraViewinitViewを call して、CamereSurfaceViewCameraTextureViewの適切な方を生成する この場合はその後にCameraの起動などを行うshowメソッドを呼ぶことでカメラとして使用できるようになる

    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が複雑で面倒だったので途中で断念してしまった

    from: https://qiita.com/petitviolet/items/bd0d594a7ade64bfa9cd