vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】DataBindingとかRecyclerViewとか

はじめに

前回までは主にカメラの映像をプレビューとして表示したり、画像として保存したりしていました。

今回は、映像にかけるフィルター(Mono、Negativeなど)を選択するための画面を、DataBindingやRecyclerViewを使って作成した時(libwvm.soが開けない

ちょっと脇道に逸れますが、Nexus5Xでデバッグをする際気になっていたのですが、以下のようなエラーメッセージが表示されていました。

    E/WVMExtractor: Failed to open libwvm.so: dlopen failed: library "libwvm.so" not found

動作としては特に問題なさそうだったのですが、やはり内容的には気になる…。
ということでググッてみたところ、端末固有の問題のようです。

libwvm.so自体はオーディオのドライバ関連のファイルのようなので、今回はシャッター音を鳴らすためのMediaActionSoundに絡んで発生したものと考えられます。

DataBindingを使う

前から気にはなっていたDataBinding。せっかくなので取り入れてみることにしました。

準備

Beta版の頃と違って、現在はDataBindingを使うには プロジェクト > app にあるbuild.gradleに以下を加えて、DataBindingを有効にしてやるだけです。

build.gradle

~省略~
android {
~省略~
    dataBinding {
        enabled = true
    }
}
~省略~

今回はまず手始めに、「findViewById」を置き換えることにします。

  1. 対象のLayoutファイルのLayout全体を、「layout」タグの子に入れます。
  2. 「data」タグを追加して、「variable」の「type」として、対象のクラス(Activity、Fragment)を指定します。

layout-land-v17/activity_camera2.xml

< ?xml version="1.0" encoding="utf-8"? >
< < data >
        < variable name="camera2" type="jp.cameratest.Camera2Activity" / >
    < /data >
    < RelativeLayout
        android:layout_width='match_parent'
        android:layout_height='match_parent' >
        < jp.cameratest.PreviewTextureView
            android:id='@+id/texture_preview_camera2'
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            / >
        < android.support.v7.widget.Toolbar
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/toolbar_camera2"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:minHeight="40dp"
            android:background="#0F000000" / >
        < Button
            android:layout_width='wrap_content'
            android:layout_height='wrap_content'
            android:text='@string/btn_taking_photo'
            android:id='@+id/btn_taking_photo_camera2'
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true" / >
    < /RelativeLayout >
< /~省略~
import jp.cameratest.databinding.ActivityCamera2Binding;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class Camera2Activity extends AppCompatActivity {
~省略~
    private ActivityCamera2Binding binding;
~省略~
    @Override
    protected void onCreate(Bundle savedInstanceState) {
~省略~
        binding = (ActivityCamera2Binding)DataBindingUtil.setContentView(this, R.layout.activity_camera2);
        binding.btnTakingPhotoCamera2 != null){
            binding.btnTakingPhotoCamera2.setOnClickListener(
                    (View v) ->{
                        takePicture();
                    }
            );
        }
~省略~

透明なToolbar

CameraのFilterなどを選択するボタンをToolbar上に設置しようと考えたのですが、 プレビュー用のTextureViewと重ねて表示するため透明にしたいと思います。

ということで調べてみたのですが、特別なことは何もなく、ただ背景色に透明度を与えるだけでしたw

toolbar_camera2.xml

< android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toolbar_camera2"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="50dp"
    android:title=""
    android:background="#0F000000" / >
  • あえてポイントがあるとすれば、色指定の並びがRGBAではなくARGBになっているため、頭の2桁が透明度の指定となります。

RecyclerViewを使う

Lollipopから追加されたRecyclerViewを使って、Filterが選択できるようにしてみました。
せっかくなので、↑で追加したDataBindingを使用します。

※以下などを読むと本当はListViewでできないかを検討すべきなのだとは思いますが、何よりRecyclerViewを使ってみたいという気持ちがあったのでこちらを選んでいます。

準備

RecyclerViewを使用するには、build.gradleに以下を追加します。

build.gradle

~省略~
dependencies {
~省略~
    compile 'com.android.support:recyclerview-v7:24.0.0-beta1'
~省略~
}
~省略~

Adapterの作成

表示する値のセットやUI部分を設定するAdapterを作成します。

recycler_item_filter.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android" >
    < data >
        < variable
            name="filterclass"
            type="jp.cameratest.FilterListAdapter.FilterClass" >
        < /variable >
    < /data >
    < LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
        < TextView
            android:id="@+id/listitem_view_filter"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="30dp"
            android:padding="20dp"
            android:layout_marginBottom="2dp"
            android:textColor="#FFFFFF"
            android:background="#3F000000"
            android:text="@{filterclass.filterName}"/ >
    < /LinearLayout >
< /layout >
  • Layout部分には表示するItemひとつ分(今回は一列分)のものを指定します。
  • 「data」タグでは、Itemが保持するFilter名、CaptureRequestのCONTROL_EFFECT_MODE_~を持つFilterClassを指定しています。表示する際はこのクラスのListを作成してViewにセットします。
  • android:text="@{filterclass.filterName}"」とすることで、Viewを表示するときにFilterClassのListが持つFilter名がそれぞれセットされます。

FilterListAdapter.java

import android.annotation.TargetApi;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class FilterListAdapter extends RecyclerView.Adapter{
    private final static FilterListAdapter filterListAdapter = new FilterListAdapter();
    private ArrayList filterClassList;

    public class FilterClass extends BaseObservable {
        // 各Itemが保持するデータを格納するClass.
        private String filterName;
        private int filterNum;

        @Bindable
        public String getFilterName(){
            return filterName;
        }
        public void setFilterName(String newValue){
            filterName = newValue;
            // 変更されたことを通知
            notifyPropertyChanged(jp.cameratest.BR.filterName);
        }
        @Bindable
        public int getFilterNum(){
            return filterNum;
        }
        public void setFilterNum(int newValue){
            filterNum = newValue;
            // 変更されたことを通知
            notifyPropertyChanged(jp.cameratest.BR.filterNum);
        }
    }
    public static class DataBindingHolder extends RecyclerView.ViewHolder {
        private final ViewDataBinding dataBinding;

        public DataBindingHolder(View v) {
            super(v);
            dataBinding = DataBindingUtil.bind(v);
            // ItemがClickされたら呼ばれる.
            dataBinding.getRoot().setOnClickListener((view) -> {
                Log.d("FilterListAdapter", "ClickedPosition:" + getAdapterPosition());
            });
        }
        public ViewDataBinding getBinding() {
            return dataBinding;
        }
    }
    public FilterListAdapter() {
        // Itemの保持するFilter名、CaptureRequestのCONTROL_EFFECT_MODE番号をセット.
        filterClassList = new ArrayList<>();

        addFilterItem("DEFAULT", CaptureRequest.CONTROL_EFFECT_MODE_OFF);
        addFilterItem("MONO", CaptureRequest.CONTROL_EFFECT_MODE_MONO);
        addFilterItem("NEGATIVE", CaptureRequest.CONTROL_EFFECT_MODE_NEGATIVE);
        addFilterItem("SEPIA", CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
        addFilterItem("AQUA", CaptureRequest.CONTROL_EFFECT_MODE_AQUA);
        addFilterItem("BLACKBOARD", CaptureRequest.CONTROL_EFFECT_MODE_BLACKBOARD);
        addFilterItem("WHITEBOARD", CaptureRequest.CONTROL_EFFECT_MODE_WHITEBOARD);
        addFilterItem("POSTERIZE", CaptureRequest.CONTROL_EFFECT_MODE_POSTERIZE);
        addFilterItem("SOLARIZE", CaptureRequest.CONTROL_EFFECT_MODE_SOLARIZE);
    }
    @Override
    public DataBindingHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        // ItemのViewを作成.
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.recycler_item_filter, viewGroup, false);

        return new DataBindingHolder(v);
    }
    @Override
    public void onBindViewHolder(DataBindingHolder viewHolder, final int position) {
        FilterClass filter = filterClassList.get(position);

        viewHolder.getBinding().setVariable(jp.cameratest.BR.filterclass, filter);
    }
    @Override
    public int getItemCount() {
        return filterClassList.size();
    }
    private void addFilterItem(@NonNull String filterName, int filterNum){
        // Filter名とCaptureRequestのCONTROL_EFFECT_MODEの番号からClassを作って配列に格納.
        FilterClass newClass = new FilterClass();
        newClass.filterName = filterName;
        newClass.filterNum = filterNum;
        filterClassList.add(newClass);
    }
}
  • RecyclerViewの各Itemの値を保持するクラス(FilterClass)には、BaseObservableを継承させます。
  • FilterClassにはget / setを持たせていますが、今回は特に使用していません。
  • Itemをクリックした時のイベントは、インナークラスとして作成しているViewHolderで取得できます。
  • Itemとしてセットする値は、ネット上のサンプルなどではRecyclerViewを表示するActivityやFragmentから渡していることが多かったのですが、今回はAdapter内で生成、セットしています。

Fragmentから呼び出して表示する

Adapterができれば、後は表示するだけです。

fragment_select_filter.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_select_filter_container" >
    < data >
        < variable name="selectfilter" type="jp.cameratest.SelectFilterFragment" / >
    < /data >
    < FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        < android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_selectfilter"
            android:scrollbars="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerVertical="true"
            android:padding="30dp"
            app:layoutManager="LinearLayoutManager"
            / >
    < /FrameLayout >
< /layout >

SelectFilterFragment.java

import android.os.Bundle;
import android.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import jp.cameratest.databinding.FragmentSelectFilterBinding;

public class SelectFilterFragment extends Fragment {
    protected FilterListAdapter adapter;

    public SelectFilterFragment() {
        // Required empty public constructor
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_select_filter, container, false);
        FragmentSelectFilterBinding binding = FragmentSelectFilterBinding.bind(rootView);

        int scrollPosition = 0;

        // スクロールした状態でActivityが破棄された場合はスクロール位置を直前の状態に戻す.
        if (binding.recyclerSelectfilter.getLayoutManager() != null) {
            scrollPosition = ((LinearLayoutManager) binding.recyclerSelectfilter.getLayoutManager())
                    .findFirstCompletelyVisibleItemPosition();
        }
        binding.recyclerSelectfilter.scrollToPosition(scrollPosition);

        adapter = new FilterListAdapter();

        // RecyclerViewにFilterListAdapterをセットする.
        binding.recyclerSelectfilter.setAdapter(adapter);

        return rootView;
    }
}

次回

だいぶサンプルコードを丸写しにしているところが多いですが、とりあえず表示できるようになりました。

f:id:mslGt:20160608005814p:plain

ただ、このままだとどのItemがClickされたかがわかるだけで、CameraへのFilterのセットができません。

次回はその辺りを扱う予定です。

参考

libwvm.so

DataBinding

Toolbar

RecyclerView