ケルベロスさんのプログラミング\nけるぷろ

ネオプログラマのけるさんことケルベロスです

Androidで動画プレイヤーアプリを作る

Androidで動画プレイヤーアプリを作る

VideoViewというクラスを使えば簡単な動画プレイヤーを作ることはできるのですが
今回は勉強のためにあえてVideoViewの内部で使われているSurfaceViewとMediaPlayerを使用して動画プレイヤーを作っていきます

主に使用するパッケージやクラス

開発環境

Android 5.0 ~
Android Studio 2.3.3

動画再生までの流れ

  • SurfaceViewを有効にする
  • MediaPlayerを初期化する
  • SurfaceViewにPlayerを紐づける
  • 再生中の時間表示等のためのスレッドを開始させる
  • そのスレッドでPlayerの状態を監視して画面表示を更新していく

準備

Manifestに追加する

<uses-permission android:name="android.permission.INTERNET"/>

MoviePlayerFragmentの実装

Fragmentに動画URIを渡す

MoviePlayerFragmentにイニシャライズメソッドを定義してFragment生成時に動画のURIを渡す処理をする

.. 省略

private String mPlayingUri;
private static final String PLAYING_URI_KEY = "PLAYING_URI";

.. 省略

public static MoviePlayerFragment newInstance(String uri) {

    Bundle args = new Bundle();

    MoviePlayerFragment fragment = new MoviePlayerFragment();
    args.putString(PLAYING_URI_KEY,uri);
    fragment.setArguments(args);
    return fragment;
}

.. 省略

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_movie_player, container, false);

    mPlayingUri = getArguments().getString(PLAYING_URI_KEY);

.. 省略

SurfaceViewの準備をする

動画を描画するView

.. 省略

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_movie_player, container, false);

        mPlayingUri = getArguments().getString(PLAYING_URI_KEY);

        mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
        mSurfaceHolder = mSurfaceView.getHolder();                             // ->(1)
        mSurfaceHolder.addCallback(mSurfaceCallback);

.. 省略

    SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {              // ->(2)
            mPlayPauseButton.setAlpha(1);                                      // ->(3)
            mLoopButton.setAlpha(1);
            mSeekBar.setAlpha(1);
        }

        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { // ->(4)
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {                 // ->(5)
        }
    };

.. 省略

(1) SurfaceHolderを取得してコールバックを登録
(2) SurfaceViewの準備が完了したタイミング
(3) 再生コントロールのUIを使用可能にする
(4) SurfaceViewの状態が変更された
(5) SurfaceViewが破棄された

MediaPlayerの準備をする

.. 省略

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { // ->(6)
    @Override
    public void onVideoSizeChanged(MediaPlayer mediaPlayer, int w, int h) {
        int winWidth = getView().getWidth();
        int winHeight = getView().getHeight();
        if (w > h || (w == h && winWidth < winHeight)) {
            int width = winWidth;
            float p = (float) h / (float) w;
            int height = (int) ((float) width * p);
            mSurfaceHolder.setFixedSize(width, height);
        } else  {
            int height = winHeight;
            float p = (float) w / (float) h;
            int width = (int) ((float) height * p);
            mSurfaceHolder.setFixedSize(width, height);
        }
    }
});
mMediaPlayer.setDataSource(mPlayingUri);
mMediaPlayer.setDisplay(mSurfaceHolder);                                                // ->(7)
mMediaPlayer.prepare();                                                                 // ->(8)
mMediaPlayer.start();
mSeekBar.setMax(mMediaPlayer.getDuration());                                            // ->(9)

.. 省略

(6) 動画のサイズに応じてSurfaceViewのサイズを変更させるためのコールバック追加
動画のサイズの倍率に合わせてFragment内で最大化する
(7) SurfaceViewと紐づける
(8) 再生準備 開始
(9) シークバーの最大値を動画のサイズに設定する

再生中動画の状態を取得してUIを更新していく処理を実装する

別スレッドを立てて監視をする

.. 省略

public class MoviePlayerFragment extends Fragment implements Runnable {  // ->(10)

.. 省略

private boolean mRunning;
private Thread mPlayerThread;
private final DecimalFormat mFormatterMin = new DecimalFormat("#");
private final DecimalFormat mFormatterSec = new DecimalFormat("00");agment.java

.. 省略

    mRunning = true;                                                         // ->(11)
    mPlayerThread = new Thread(this);
    mPlayerThread.start();

.. 省略

public void run() {                                                         
    while(mRunning) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mHandler.sendMessage(Message.obtain());                            
    }
}

public void stopRunning() {
    mRunning = false;
}

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (mMediaPlayer == null) {
            return;
        }
        mSeekBar.setProgress(mMediaPlayer.getCurrentPosition());          // ->(12)

        String playButtonText = mMediaPlayer.isPlaying() ? "停止" : "再生"; // ->(13)
        mPlayPauseButton.setText(playButtonText);

        String loopButtonText = mMediaPlayer.isLooping() ? "連続" : "一回";
        mLoopButton.setText(loopButtonText);

        int leftTime = mMediaPlayer.getCurrentPosition()/1000;            // ->(14)
        String leftMinText = mFormatterMin.format((int)(leftTime / 60));
        String leftSecText = mFormatterSec.format((int)(leftTime % 60));
        int rightTime = mMediaPlayer.getDuration()/1000 - leftTime;
        String rightMinText = mFormatterMin.format((int)(rightTime / 60));
        String rightSecText = mFormatterSec.format((int)(rightTime % 60));
        mLeftTimeText.setText(leftMinText +":"+ leftSecText);
        mRightTimeText.setText("-"+rightMinText +":"+ rightSecText);
    }
};


.. 省略

(10) Runnableをimplementしておく
(11) 別スレッドをスタートさせる
(12) シークバーの表示更新
(13) 再生ボタンの表示更新
(14) 再生時間の表示更新

シークバー操作の実装

.. 省略

mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int i, boolean b) {  // ->(15)
        if (null != mMediaPlayer) {
            if (!mMediaPlayer.isPlaying()) {
                mMediaPlayer.seekTo(i);
            }
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {              // ->(16)
        if (null != mMediaPlayer) {
            mMediaPlayer.pause();
        }
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {               // ->(17)
        if (null != mMediaPlayer) {
            mMediaPlayer.start();
        }
    }
});

.. 省略

ラッキング開始でMediaPlayerを一時停止してシークを開始して、トラッキングから指を話した時点で再生を再開する
一時停止中にトラッキングをすると再生開始してしまうがフラグ等で状態を管理する実装をするつもり

(15) プログレスが変更されたときの通知
(16) トラッキング開始時
(17) トラッキング終了時

サンプルコード

上記アプリ全体のサンブルコードはgithubにありますので参考にしていただければと思います。
https://github.com/rdme/MoviePlayerSample

Androidでカメラアプリを作成する(Android5.0以降に対応) (2)

Androidでカメラアプリを作成する(Android5.0以降に対応) (2)

撮影機能を実装する

カメラを準備するメソッドの修正

.. 省略

private Handler mBackgroundHandler; // ->(1)
private ImageReader mImageReader;

.. 省略

private void prepareCameraView() {
    CameraManager cameraManager 
        = (CameraManager) mParentActivity.getSystemService(Context.CAMERA_SERVICE);
    try {
        String backCameraId = null;
        for (String cameraId : cameraManager.getCameraIdList()) {
            CameraCharacteristics characteristics 
                = cameraManager.getCameraCharacteristics(cameraId);
            if (characteristics.get(CameraCharacteristics.LENS_FACING)
                    == CameraCharacteristics.LENS_FACING_BACK) {
                backCameraId = cameraId;

                StreamConfigurationMap map
                    = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0];

                Size jpegSize = getJPEGSize(characteristics); // ->(2)

                mImageReader = ImageReader.newInstance(jpegSize.getWidth(), // ->(3)
                        jpegSize.getHeight(),
                        ImageFormat.JPEG, 1);
                mImageReader.setOnImageAvailableListener(mReaderListener, mBackgroundHandler); // ->(4)
            }
        }

.. 省略

(1) バックグラウンドハンドラをメンバ変数として定義する onResumeでスレッドを開始させておく(コードは省略)
(2) ストリーム設定からJPEGの出力サイズを取得する
(3) 画像を取得するためのImageReaderの初期化
(4) Image取得リスナーをセットする 実装内容は後述

ImageReaderとは
SurfaceTextureの画像にアクセスする機能を提供するクラス

撮影メソッドの実装

.. 省略

    private void takePicture() {
        if(null == mCameraDevice) {
            return;
        }
        try {
            mCaptureRequestBuilder.addTarget(mImageReader.getSurface());            // ->(5)
            mCaptureRequestBuilder.set(                                             // ->(6)
                CaptureRequest.CONTROL_MODE, 
                CameraMetadata.CONTROL_MODE_AUTO);

            int rotation
                = mParentActivity.getWindowManager().getDefaultDisplay().getRotation(); // ->(7)
            mCaptureRequestBuilder.set(
                CaptureRequest.JPEG_ORIENTATION,
                ORIENTATIONS.get(rotation));

            mCaptureSession.capture(mCaptureRequestBuilder.build(),                // ->(8)
                    new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session,  // ->(9)
                                                       @NonNull CaptureRequest request,
                                                       @NonNull TotalCaptureResult result) {
                            super.onCaptureCompleted(session, request, result);
                        }
                    },
                    mBackgroundHandler);

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

.. 省略

(5) 生成したImageReaderからSurfaceを取得する
(6) 自動モードをセットする
(7) 画像の縦横を設定する
(8) キャプチャを開始する
(9) キャプチャ成功時のコールバック

ImageReaderのコールバック

キャプチャが成功するとImageReadr.OnImageAvailableListenerに結果がかえってくる

.. 省略

private ImageReader.OnImageAvailableListener mReaderListener
        = new ImageReader.OnImageAvailableListener() {

    public void onImageAvailable(ImageReader imageReader) {               // ->(10)

        Image image = imageReader.acquireNextImage();                     // ->(11)
        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        OutputStream output = null;
        String saveDirectory = Environment.getExternalStorageDirectory().toString();
        String saveFileName = "pic_" + System.currentTimeMillis() + ".jpg";
        try {
            File file = new File(saveDirectory,saveFileName);
            output = new FileOutputStream(file);
            output.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (image != null) {
                image.close();
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        String[] paths = {saveDirectory + "/" + saveFileName};         // ->(12)
        String[] mimeTypes = {"image/jpeg"};
        MediaScannerConnection.scanFile(mParentActivity.getApplicationContext(),
                paths,
                mimeTypes,
                new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {
                        mParentActivity.runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(mParentActivity,"saved",Toast.LENGTH_LONG).show();
                                createCameraCaptureSession();         // ->(13)
                            }
                        });
                    }
                }
        );
    }

};

.. 省略

(10) 画像を取得可能になったときのコールバック
(11) バッファから画像をファイルとして保存していく処理
(12) MediaScannerConnectionでファイルをスキャンする これはメディアライブラリで使用可能にするための処理
(13) 再度プレビューを開始する

サンプルコード

githubに置いてありますので、参考にしていただければと思います。

Androidでカメラアプリを作成する(Android5.0以降に対応) (1)

Androidでカメラアプリを作成する(Android5.0以降に対応) (1)

主に使用するパッケージやクラス

  • android.hardware.camera2
    Android5以上でカメラを使用するためのパッケージ

  • android.view.TextureView
    カメラから取得した画像を連続描画するView

  • android.hardware.camera2.CameraManager
    カメラデバイス全体を管理するクラス

  • android.hardware.camera2.CameraDevice
    端末毎のカメラオデバイス(前面カメラや背面カメラそれぞれ)

  • android.hardware.camera2.CameraCaptureSession
    カメラデバイスの画像を取得するセッション

開発環境

Android 5.0 ~
Android Studio 2.3.3

カメラを使用する流れ

  • TextureViewをアクティブにする
  • CameraManagerをContextから取得してCameraDeviceを起動する
  • CameraDeviceからCameraCaptureSessionを作成する
  • CameraCaptureSessionから取得した画像をTextureViewに描画していく

準備

Manifestに追加する

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera2.full" />

CameraFragmentの実装

Viewの準備

.. 省略

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_camera, container, false);
    mTextureView = (TextureView) view.findViewById(R.id.texture_view);
    mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);      // ->(1)

    Button button = (Button) view.findViewById(R.id.button_take_picture);
    button.setOnClickListener(mButtonOnCickListener);                     // ->(2)

    return view;
}

.. 省略

(1) TextureViewのステータス変更のコールバックを登録する(後述)
(2) 撮影ボタンのリスナ登録 (省略)

TextureViewのステータス変更のコールバック

.. 省略

private TextureView.SurfaceTextureListener mSurfaceTextureListener 
    = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {     // ->(3)
            prepareCameraView();                                                                  // ->(4)
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) { } // ->(3)
        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { return false; } // ->(6)
        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { }                    // ->(7)
    };

.. 省略

TextureView.SurfaceTextureListenerを使用する
(3) TexutureViewの準備が完了
(4) CameraDeviceの準備を行う
(5) サイズ変更通知 今回は無視
(6) 破棄 今回は無視
(7) 更新 今回は無視

CameraDeviceの準備

.. 省略

private CameraDevice mCameraDevice; // ->(8)
private Size mPreviewSize;

.. 省略

private void prepareCameraView() {
    CameraManager cameraManager 
        = (CameraManager) mParentActivity.getSystemService(Context.CAMERA_SERVICE); // ->(9)
    try {
        String backCameraId = null;
        for (String cameraId : cameraManager.getCameraIdList()) {                   // ->(10)
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            if (characteristics.get(CameraCharacteristics.LENS_FACING)
                    == CameraCharacteristics.LENS_FACING_BACK) {                    // ->(11)
                backCameraId = cameraId;

                StreamConfigurationMap map 
                    = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); // ->(12)
                mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0];
            }
        }

        if (backCameraId == null) {
            return;
        }

        if (ActivityCompat.checkSelfPermission(mParentActivity, Manifest.permission.CAMERA) // ->(13)
                != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        cameraManager.openCamera(backCameraId, new CameraDevice.StateCallback() { // ->(14)
                    @Override
                    public void onOpened(@NonNull CameraDevice cameraDevice) {    // ->(15)
                        mCameraDevice = cameraDevice;
                        createCameraCaptureSession();
                    }

                    @Override
                    public void onDisconnected(@NonNull CameraDevice cameraDevice) { // ->(16)

                    }

                    @Override
                    public void onError(@NonNull CameraDevice cameraDevice, int i) { // ->(17)

                    }
        }, null);

    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

.. 省略

CameraManagerからカメラ情報を取得してカメラの準備をする
(8) メンバ変数にCameraDeviceを定義
(9) システムサービスのCameraManagerを取得する
(10) getCameraIdListで配列で端末の使用するカメラのIDを取得できる
(11) 背面カメラのIDを取得
(12) 背面カメラのプレビュー画面サイズを取得
(13) パーミッションの確認
(14) 背面カメラのIDを使用してカメラをアクティブにする
(15) 成功のコールバック CameraCaptureSessionを有効化する
(16) 接続終了
(17) 失敗

CameraCaptureSessionの有効化

.. 省略

private void createCameraCaptureSession() {
    if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
        return;
    }

    SurfaceTexture texture =  mTextureView.getSurfaceTexture();                     // ->(18)
    if (null == texture) {
        return;
    }
    texture.setDefaultBufferSize(mPreviewSize.getWidth(),mPreviewSize.getHeight());
    Surface surface = new Surface(texture);

    try {
        mCaptureRequestBuilder 
            = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);  // ->(19)
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
    mCaptureRequestBuilder.addTarget(surface);

    try {
        mCameraDevice.createCaptureSession(Arrays.asList(surface), 
        new CameraCaptureSession.StateCallback() { // ->(20)
            @Override
            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { // ->(21)
                mCaptureSession = cameraCaptureSession;
                updatePreview();
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { // ->(22)
                Toast.makeText(mParentActivity, "onConfigureFailed", Toast.LENGTH_LONG).show();
            }
        }, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

.. 省略

CameraDeviceからキャプチャを取得するためのセッションを確立する
(18) サイズを設定してTextureViewからSurfaceViewを初期化する
(19) CameraDeviceからCaptureRequestを生成してプレビュー画面をターゲットに設定する
(20) SurfaceViewを使用してCameraDeviceとのセッションを確立する
(21) 確立成功 画面の更新を開始する
(22) 失敗

CaptureSessionから画像を繰り返し取得してTextureViewに表示する

.. 省略

private void updatePreview() {
    if (null == mCameraDevice) {
        return;
    }
    mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);                 // ->(23)

    HandlerThread thread = new HandlerThread("CameraCapture");              // ->(24)
    thread.start();
    Handler backgroundHandler = new Handler(thread.getLooper()); 

    try {
        mCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(),null,backgroundHandler); // ->(25)
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

.. 省略

CaptureSessionから画像を繰り返し取得してTextureViewに表示する
(23)オートフォーカスを有効にするための設定
(24)バックグラウンドで実行するためのスレッドを作成
(25)リクエストを開始する

以上がカメラを起動するためのコードです。
このフラグメントを表示する前にカメラのパーミッションをユーザに要求する必要があります。

カメラのパーミッションをリクエストする

今回は呼び出し元のMainActivityに実装します

.. 省略

private final int MY_CAMERA_REQUEST_CODE = 1001;

.. 省略

private void showCameraFragment() {
    if (checkSelfPermission(Manifest.permission.CAMERA) 
        != PackageManager.PERMISSION_GRANTED) {                                            // ->(26)
        requestPermissions(new String[]{Manifest.permission.CAMERA},MY_CAMERA_REQUEST_CODE);     // ->(27)
        return;
    }

    CameraFragment f = new CameraFragment();                                                   // ->(28)
    this.setFragment(f);
}

@Override
public void onRequestPermissionsResult(int requestCode, 
                                        @NonNull String[] permissions,
                                        @NonNull int[] grantResults) {                     // ->(29)
    if (requestCode != MY_CAMERA_REQUEST_CODE) {
        return;
    }
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {                             // ->(30)

        CameraFragment f = new CameraFragment();
        this.setFragment(f);

    } else {                                                                                 // ->(31)

    }
}

.. 省略

(26) すでに許可済みか判定する
(27) リクエスト開始
(28) 許可済みであればそのまま起動 (this.setFragmentの内容は省略)
(29) パーミッションのリクエスト結果のコールバックをOverrideしておく
(30) 成功 CameraFragmentを起動する
(31) 失敗

まとめ

以上でカメラを起動して画像を画面に写すという処理が完了しました。
実際に撮影して画像を保存する処理は後ほど実装していきます。
あと、画面の回転に対応していないのでそちらも後ほど実装していきます。

サンプルコード

githubにありますので参考にしていただければと思います
https://github.com/rdme/PictureSample

AndroidでWebブラウザアプリを作る

AndroidでWebブラウザアプリを作る

主に使用するクラス
android.webkit.WebView

WebView実装

...省略

private WebView mWebView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // webView
    mWebView = (WebView) findViewById(R.id.web_view);
    mWebView.setWebViewClient(new MyWebViewClient());  // ->(1)
    mWebView.loadUrl("https://www.yahoo.co.jp");       // ->(2)
    mWebView.getSettings().setJavaScriptEnabled(true); // ->(3)
}

...省略

(1) setWebViewClientはWebViewでのイベントをハンドリングするために実装してsetする
(2) URLを読み込む
(3) WebView内でJavaScriptを有効にする

WebViewClientでWebView内のイベントをハンドリングする

...省略

private class MyWebViewClient extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { // ->(4)
        return false;
    }

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) { // ->(5)
        super.onPageStarted(view, url, favicon);
    }

    @Override
    public void onPageFinished(WebView view, String url) { // ->(6)
        super.onPageFinished(view, url);
    }

    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { // ->(7)
        super.onReceivedError(view, request, error);
    }
}

...省略

(4) リクエスト開始前にハンドリングされる trueを返すと他アプリやActivityが起動するデフォルトの挙動になるのでfalseを返す
(5) 読み込み開始処理
(6) 読み込み終了
ブラウザであればこの時点でURLのstringを取得してURLバーに表示させることができる
(7) エラーハンドリング

端末の戻るボタンでブラウザバックを実装

...省略

@Override
public void onBackPressed() {
    if (mWebView.canGoBack()) { // ->(7)
        mWebView.goBack();      // ->(8)
    } else {
        super.onBackPressed();  // ->(9)
    }
}

...省略

(7) canGoBackがtrueならばブラウザバックをすることができる
(8) goBackでブラウザバック
(9) canGoBackがfalseならば端末の戻る機能をそのまま使用する

検索を実装する

android.support.v7.widget.SearchView
を利用して検索バーを実装する

...省略

private SearchView mSearchView;

...省略

@Override
protected void onCreate(Bundle savedInstanceState) {

...省略

    // search view
    mSearchView = (SearchView) findViewById(R.id.search_view); 
    mSearchView.setOnQueryTextListener(new MySearchViewTextListener()); // ->(10)

...省略

(10) イベントリスナをセットする

...省略

private class MySearchViewTextListener implements SearchView.OnQueryTextListener { // ->(11)
    @Override
    public boolean onQueryTextSubmit(String query) { // ->(12)
        searchTextInWebView(query);                  // ->(13)
        return false;                                // ->(14)
    }

    @Override
    public boolean onQueryTextChange(String newText) { // ->(15)
        return false;                                  // ->(16) 
    }
}

...省略

(11) SearchView.OnQueryTextListenerをimplementsしたクラスを定義する
(12) キーボードの送信を押したときのコールバック
(13) WebViewを使用して検索するためのメソッド(後述)
(14) falseを返すとハンドリングする trueを返すとデフォルトの挙動になる(Intentの発生など)
(15) テキストが変更されたときのコールバック
(16) falseを返すとハンドリングする trueを返すとデフォルトの挙動になる(Intentの発生など)

...省略

/** WebView内でYahoo検索をするメソッド */
private void searchTextInWebView(String text) {
    String p = "";                                                                  // ->(17)
    try {
        p = URLEncoder.encode(text,"UTF-8");                                        // ->(18)
    } catch (UnsupportedEncodingException e) { 
        e.printStackTrace();
    }

    String url = "https://kids.yahoo.co.jp/search/bin/search?ei=UTF-8&fr=ush&p="+p; // ->(19)
    mWebView.loadUrl(url);

    mSearchView.clearFocus();                                                       // ->(20)
    mWebView.requestFocus();
}

...省略

(17) 検索クエリの初期化
(18) java.net.URLEncoderを使用して検索クエリをエンコードする
(19) Yahoo検索のGETパラメータに検索クエリをセットする
(20) SearchViewのフォーカスを外してキーボードを隠す

まとめ

以上で簡単なブラウザアプリを実装することができます

サンプルコード

githubにありますので参考にしていただければと思います https://github.com/rdme/WebViewSample

Android端末のセンサーを扱う

Android端末のセンサーを扱う

使用可能なセンサーリストを取得する

SensorManagerを使用する

/* ActivityのインスタンスからSensorManagerを取得 */
SensorManager manager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);

/* getSensorListの引数にSensor.TYPE_ALLでリストを取得 */
List<Sensor> sensors = manager.getSensorList(Sensor.TYPE_ALL);

for (Sensor sensor : sensors) {
    Log.d("Sensor","name:" + sensor.getName() + " type:" + String.valueOf(sensor.getType()));
}

↓実行結果 (Nexsus9 Android7.0)

name:Accelerometer Sensor type:1
name:Magnetic field Sensor type:2
name:Gyroscope Sensor type:4
name:CM32181 Light sensor type:5
name:Pressure Sensor type:6
name:CWGD Orientation Sensor type:3
name:Rotation Vector type:11
name:Linear Acceleration type:10
name:Gravity type:9
name:Magnetic Uncalibrated type:14
name:Gyroscope Uncalibrated type:16
name:Game Rotation Vector type:15
name:Geomagnetic Rotation Vector type:20
name:Significant Motion type:17
name:Step Detector type:18
name:Step Counter type:19
name:Accelerometer Sensor (WAKE_UP) type:1
name:Magnetic field Sensor (WAKE_UP) type:2
name:Gyroscope Sensor (WAKE_UP) type:4
name:Pressure Sensor (WAKE_UP) type:6
name:CWGD Orientation Sensor (WAKE_UP) type:3
name:Rotation Vector (WAKE_UP) type:11
name:Linear Acceleration (WAKE_UP) type:10
name:Gravity (WAKE_UP) type:9
name:Magnetic Uncalibrated (WAKE_UP) type:14
name:Gyroscope Uncalibrated (WAKE_UP) type:16
name:Game Rotation Vector (WAKE_UP) type:15
name:Geomagnetic Rotation Vector (WAKE_UP) type:20
name:Step Detector (WAKE_UP) type:18
name:Step Counter (WAKE_UP) type:19

Android7.0のNexsus9で検証
加速度(Acceleromenter)、磁場(Magnetic field)、ジャイロスコープ(Gyroscope)、光(CM32181 Light)、気圧(Pressure)、等のセンサーが使用できる
WAKE_UPというのはセンサー取得時にgetDefaultSensorの引数wakeUpのところにtrueを入れれはwake_upセンサーを取得できる

Sensorクラスについて

/*タイプ SensorManagerのgetDefaultSensorの引数に使用する*/
public int getType(){}  
/*センサーの名前を取得 */
public String getName(){}  

getTypeで取得できるタイプはSensorクラスに定義されている
public static final int TYPE_ACCELEROMETER = 1;
public static final int TYPE_MAGNETIC_FIELD = 2;
public static final int TYPE_GYROSCOPE = 4;

センサーの名前は英語で取得できる(上記実行結果)

加速度センサーを使用して値を取得する流れ (Accelerometer Sensor)

/* ActivityのインスタンスからSensorManagerを取得 */
SensorManager manager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
/* 加速度センサーを取得 */
Sensor accelerometerSensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
/* SensorManagerにリスナー、加速度センサーインスタンス、サンプリング回数を引数として、センサーを登録する */
/*(thisのクラスにSensorEventListenerをimplementsしておく)*/
manager.registerListener(this,accelerometerSensor,SensorManager.SENSOR_DELAY_NORMAL);

/* センサーの値が更新されると以下メソッドに値が入ってくる */
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
    float[] values = sensorEvent.values;
    float x = values[0]; // values配列の0番目の値はX軸の加速度 単位(m/s^2)
    float y = values[1]; // values配列の1番目の値はY軸の加速度 単位(m/s^2)
    float z = values[2]; // values配列の2番目の値はZ軸の加速度 単位(m/s^2)
}

加速度センサーのTYPEの値はSensor.TYPE_ACCELEROMETER
サンプリング回数はSensorManagerクラスに
SENSOR_DELAY_FASTEST = 0; (3-30ms)
SENSOR_DELAY_GAME = 1; (3-30ms)
SENSOR_DELAY_NORMAL = 3; (約200ms)
SENSOR_DELAY_UI = 2; (約60ms)
と定義されている

参考: http://masterka.seesaa.net/article/180418713.html

SensorManagerクラスについて

/*センサーリストを取得する*/
public List<Sensor> getSensorList(int type){}

/*センサーインスタンスを取得する*/
public Sensor getDefaultSensor(int type){}

/*センサーを登録する */
public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs){}

/*センサーを登録解除する sensor引数なしならばlistenerの全てのセンサーの登録解除*/
public void unregisterListener(SensorEventListener listener, Sensor sensor){}

/*リスナの定義 */
public interface SensorEventListener {
    /*センサー値変更時*/
    /*var1.valuesで値の配列を取得*/
    void onSensorChanged(SensorEvent var1);
    /*センサーの正確さ変更時*/
    void onAccuracyChanged(Sensor var1, int var2);
}

getSensorListの引数にはSensorクラスで定義されているSensor.TYPE_ALLを入れれば全センサーを取得できる
基本的にはActivityのonPauseでunregisterListenerを呼び出してセンサーの処理を止める
しかし、止めなければアプリがバックグラウンド状態でも値の取得が可能(端末を傾けたらアプリ内で処理を実行する等が可能)

サンプルプロジェクト

センサーを使用するサンプルプロジェクトをgithubに置いたので参考にしていただければと思います。
https://github.com/rdme/AndroidSensorSample

Androidアニメーションの基礎

Androidアニメーションの基礎

使用する主なクラス

android.animation.Animator
android.animation.ValueAnimator
android.animation.ObjectAnimator

ObjectAnimatorはValueAnimatorのサブクラス
ValueAnimatorはObjectAnimatorのサブクラス

+ Animator  
    ↓  
    + ValueAnimator  
            ↓  
          * ObjectAnimator  

Animator

ValueAnimatorやObjectAnimatorの基底クラスで状態変化のリスナなどの機能を持つ

ValueAnimator

アニメーション化された値を計算し、ターゲットオブジェクト上に設定するアニメーションを実行するための簡単なタイミングエンジンを提供するクラス

ObjectAnimator

ターゲットオブジェクトと値と実際にアニメーションプロパティをコンストラクタで受け取り、内部的に適切な関数を実行してアニメーションを実行するクラス

アニメーションに使用するプロパティ

相対的な位置変更: translationX & translationY
絶対的な位置変更: x, y
回転 :rotation, rotationX, rotationY
サイズ変更 :scaleX, scaleY
支点 :pivotX, pivotY
透明度 :alpha

実装してみる

透明度

透明からだんだん表示されるアニメーション

private Animator alphaAnimator(View target) {
    float fromAlpha = 0f;
    float toAlpha = 1f;
    ObjectAnimator animator = ObjectAnimator.ofFloat(target,"alpha",fromAlpha,toAlpha);  // ->(1)
    animator.setDuration(1000);                                                          // ->(2)
    return animator;
}

(1) ObjectAnimator.ofFloatの引数は
+ アニメーションするターゲットオブジェクト
+ アニメーション前の値
+ アニメーション後の値

(2) animator.setDurationで1000msecかけてアニメーションするように設定している

回転

回転し続けるアニメーション

private Animator rotateAnimator(View target) {
    ObjectAnimator  animator = ObjectAnimator.ofFloat(target, "rotation", 0f,360f);
    animator.setDuration(1000);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    return animator;
}

animator.setRepeatCountにINFINITEを指定して回転し続けるように指定している

移動

移動するアニメーション

private Animator translateAnimator(View target, float distance, float degree) {
    float fromX = 0f;
    float fromY = 0f;
    float toX = (float) (distance * Math.cos(Math.toRadians(degree)));
    float toY = (float) (distance * Math.sin(Math.toRadians(degree)));
    
    PropertyValuesHolder xHolder = PropertyValuesHolder.ofFloat("translationX",fromX,toX); // ->(3)
    PropertyValuesHolder yHolder = PropertyValuesHolder.ofFloat("translationY",fromY,toY);

    ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target,xHolder,yHolder);
    animator.setDuration(1000);
    animator.setInterpolator(new BounceInterpolator());                                    // ->(4)

    return animator
}

(3) Y軸の変更とX軸の変更を同時に行なっている
その場合はPropertyValuesHolderを使用して、ObjectAnimator.ofPropertyValuesHolderの引数に入れてAnimatorを初期化する
(4) animator.setInterpolatorでアニメーションカーブを設定することができる
+ AccelerateDecelerateInterpolator
なにも設定しなかった場合のデフォルトの挙動 加速したのちに減速してアニメーションを終える
+ DecelerateInterpolator
最初に加速してだんだん減速する
+ AccelerateInterpolator
だんだん加速する
+ BounceInterpolator
バウンドするように加速減速逆方向移動を繰り返して目標の値に到達する
+ OvershootInterpolator
目標の値を一旦オーバーしてから目的の値に到達する

全て組み合わせる

透明な状態から回転しながら目的の位置に移動する

private void alphaTranslateRotateAnimation(View target, float distance, float degree, long delay) {

    Animator alpha = alphaAnimator(target);

    Animator translate = translateAnimator(target,distance,degree);

    Animator rotate = rotateAnimator(target);

    AnimatorSet set = new AnimatorSet();                     // ->(5)
    set.setStartDelay(delay);                                // ->(6)
    set.setInterpolator(new OvershootInterpolator());
    set.playTogether(alpha,translate,rotate);                // ->(7)

    set.start();

}

(5) AnimatorSetは複数のAnimatorを束ねて同時に実行したり順番に実行したりすることができるいわゆる管理クラス
Animatorを継承しているので上記説明した設定も行える
(6) setStartDelayでアニメーション実行開始時間の遅延時間を設定する
(7) playTogetherは引数に入れたAnimatorを同時に実行する設定
playSequentiallyで順番に実行させることもできる

サンプルプロジェクト

上記アニメーションを使用するサンプルプロジェクトをgithubに置いたので参考にしていただければと思います。
実装内容は、ボタンを押すと放射線状に他のボタンが現れるメニューボタンのようなアニメーションです
rdme/AnimationSample: Android Sample Application

参考

ObjectAnimator | Android Developers
素人からAndroidアニメーションの面白さを発見しよう - Qiita
AndroidでもiPhoneに負けないようなアニメーションを実装してみよう - Yahoo! JAPAN Tech Blog

SQLiteについて

SQLite

普段の開発ではCoreDataしかほとんど触ることがありません なのでSQLiteそのものについて調べてみる

  • 特徴

    • 小中規模アプリケーション向けの軽量RDBMS(リレーショナルデータベースマネジメントシステム)
    • アプリケーションに組み込んで利用することができる
    • ユーザ認証がない
    • データベースファイルが1つにまとめられている
    • コマンドラインツールsqlite3を使って直接データベースを操作する
  • コマンドラインで使ってみる

    $ sqlite3 ~/Desktop/mydb.sqlite3  #データベースファイルを指定して起動する 存在しない場合  
    sqlite> create table human(name, age, perfect); #テーブル作成  
    splite> .tables #テーブル確認
    human
    splite> insert into human values('Suzuki',20,0); #インサート
    splite> insert into human values('Sato',24,0); #インサート
    splite> insert into human values('Nakata',29,1); #インサート
    splite> insert into human values('Hasegawa',24,0); #インサート
    splite> .headers on #データ表示時にヘッダが追加される
    splite> .mode column #カラムの長さを合わせる
    splite> select * from human; #データ取得
    name        age         perfect
    ----------  ----------  ----------
    Suzuki      20          0
    Sato        24          0
    Nakata      29          1
    Hasegawa    24          0
    sqlite> select name from human where perfect = '1'; #データ取得
    name      
    ----------
    Nakata
    sqlite> create index nameindex on human(name); #インデックス作成
    sqlite> select perfect from human where name = 'Hasegawa'; #データ取得
    perfect      
    ----------
    0
    sqlite> select perfect from human where name = 'Nakata'; #データ取得
    perfect      
    ----------
    1
  • .importを使ってみる
    insertdata.txtというファイルを用意しておく
    inesrtdata.txtの中身
    Suzuki|28|0
    Sato|48|0
    Nakata|19|1
    Hasegawa|84|0
    sqlite> drop table human; #いったんテーブルを削除
    sqlite> create table human(name,age,perfect); #再度作成する
    sqlite> .import ./insertdata.txt human #ファイルからデータをインポート
    sqlite> select * from human; #データ取得
    name        age         perfect
    ----------  ----------  ----------
    Suzuki      28          0
    Sato        48          0
    Nakata      19          1
    Hasegawa    84          0
    splite> .exit #終了

感想

お手軽でした 次はRealmについて調べようと思います

参考

SQLiteの公式ホームベージ https://www.sqlite.org/ RealmとSQLiteのパフォーマンス検証を行った話 - Qiita

TCPとUDP

TCPUDP

TCPとは

  • Transmission Control Protocol
  • インターネットにおいて標準的に利用されるプロトコル
  • UDPと比較して信頼性の高い通信を実現するために使用される

    • シーケンス制御、再送制御
  • ポート番号とは

    • 通信先のアプリケーションを指定するための番号
  • TCPはコネクション型プロトコルである

    • 相手の応答があって初めて通信を開始する
    • データ転送の前にコネクションの確立を行う
    • TCPコネクションの確立に使用されるのが3ウェイハンドシェイクである
  • 3ウェイハンドシェイク

    • 3回のやりとりでコネクションを確立する
      • 1- AからBへの接続確認
      • 2- BからAへの確認応答と接続確認
      • 3- AからBへの確認応答
  • 信頼性確保の方法

    • シーケンス制御
      • データを分割して送信する際にシーケンス番号を付与することで受信側で番号を元にデータを正しく組み立てることができる
    • 再送制御
      • 分割されたデータが到着したときに受信側から確認応答をすることで、時間内に確認応答がなかった場合にその番号のデータを再送信する
  • 通信効率向上

    • ウィンドウ制御
      • 受信側でウィンドウサイズを指定してそのサイズの分は確認応答を待つことなくデータを送信していく
    • フロー制御
      • 受信側で負荷が高まったときにウィンドウサイズを小さくして負荷がおさまるまで一定時間データ送信量を制御する

UDPとは

  • User Datagram Protocol
  • 信頼性は高くないが、速さやリアルタイム性を求める通信に使用されるプロトコルである
    • オーバーヘッドが少ない
    • パケットロスしても再送をしない
  • 用途
    • 音声や映像などのリアルタイム性のあるデータの送受信
      • 音声通話、ストリーミング
    • 複数の相手に同じデータを送信する場合
    • 信頼性が必要なく少量のデータ転送
      • DNSサーバとのデータ通信など

まとめ

通常の通信ではTCPを使用する
速さやリアルタイム性を求める場合にはUDPを使用する

参考
http://www.infraexpert.com/

CoronaSDK - 非同期で画像をダウンロードして表示する

CoronaSDK - 非同期で画像をダウンロードして表示する

display.loadRemoteImageを使います
Corona Docs — API | Libraries | display | loadRemoteImage

local halfW, halfH = display.contentCenterX,display.contentCenterY

local function load_image_listener(ev) -- 1
    if ev.isError then
        print 'load image error' 
    end
end -- load_image_listener

local url = 'https://developer.coronalabs.com/demo/hello.png' 
local imageName = 'tmp.png'
display.loadRemoteImage( url, -- 2
'GET',  -- 3
load_image_listener,  -- 4
imageName,  -- 5
system.TemporaryDirectory,  -- 6
halfW, -- 7
halfH )
  1. コールバックメソッド
  2. ダウンロードするurl
  3. HTTP Methodを指定
  4. コールバックメソッドを指定
  5. 保存する際の名前を指定
  6. 保存するフォルダを指定
  7. 画像の中心位置を指定 ここではdisplayの中心

これで画像が非同期でダウンロードされて、画面の中心に画像が表示されます
f:id:popeyekn:20151211191615p:plain
リストビュー(テーブルビュー)のセル一つ一つに画像を表示するには一手間かかります
詳しくは後ほど記事を書きますが、以下になんとなくの考え方だけ示しておきます

  1. 画像名をあらかじめinsertRowの時点でparamsにいれておく
  2. セルの表示タイミングでloadRemoteImageをする
  3. loadRemoteImageが完了したタイミングでtableView全体の再描画を行う
  4. それぞれのセルはtableView.view.rows[i]で取得することができる
  5. 再描画の際にrow.paramsにいれておいた画像名を使用してdisplay.newImageで画像をロードしてセルに表示する

Corona SDKでシンプルなTableView(List View)を実装する

Corona SDKでシンプルなTableView(List View)を実装する

2015-12-11
チュートリアル形式でコードを書いていきます

local widget = require 'widget' -- 1

local myList = widget.newTableView { -- 2
    width = display.contentWidth, -- 3 
    height = display.contentHeight, -- 3
    onRowRender = onRowRender, -- 4
    onRowTouch = onRowTouch, -- 5
    listener = scrollListener -- 6
}
  1. widgetライブラリをインポートしてインスタンス
  2. widgetライブラリのnewTableViewでTableViewインスタンスを作成してディスプレイに表示
  3. widthとheightはdisplayサイズに合わせる
  4. Cellがレンダーされた時のリスナ登録 - 後述
  5. Cellがタップされた時のリスナ登録 - 後述
  6. TableViewがスクロールされた時のリスナ登録 - 後述

まだこれでは真っ白で何も表示されません

さらに以下を追記

local myData = {} -- 1
myData[1] = { name="Fred",    phone="555-555-1234" }
myData[2] = { name="Barney",  phone="555-555-1235" }
myData[3] = { name="Wilma",   phone="555-555-1236" }
myData[4] = { name="Betty",   phone="555-555-1237" }
myData[5] = { name="Pebbles", phone="555-555-1238" }
myData[6] = { name="BamBam",  phone="555-555-1239" }
myData[7] = { name="Dino",    phone="555-555-1240" }

for i = 1, #myData do
    myList:insertRow{ -- 2
        rowHeight = 60,
        isCategory = false,
        rowColor = { 1, 1, 1 },
        lineColor = { 0.90, 0.90, 0.90 }
    }
end
  1. myDataというtableインスタンスを作成(tableviewとは関係ないよ) -> https://docs.coronalabs.com/daily/api/library/table/index.html
  2. TableViewのメソッドTableView:insertRowでセルを挿入 isCategoryはiOSでいうそのセルをtableHeaderにするかどうか

これでセルの間の線が表示されてセルが作成されたことがわかります
しかしまだセルの内容がありません
そこでさっき登録したonRowRenderを実装します
その前に少し上のコードを修正します

for i = 1, #myData do
    myList:insertRow{ -- 2
        rowHeight = 60,
        isCategory = false,
        rowColor = { 1, 1, 1 },
        lineColor = { 0.90, 0.90, 0.90 },
        params = {                           -- 1
            name = myData[i].name,
            phone = myData[i].phone
        }
    }
end
  1. insertRowの際にparamsというtableを設定することができます。そこにnameとphoneにパラメータをいれておきます

onRowRenderの実装です
注意する点としては先ほどのwidget.newTableviewでonRowRenderを登録するよりも上に下記コードを書いてください

local function onRowRender( event )
   local row = event.row
   local id = row.index

   row.bg = display.newRect( 0, 0, display.contentWidth, 59 )
   row.bg.anchorX = 0
   row.bg.anchorY = 0
   row.bg:setFillColor( 1 )
   row:insert( row.bg )

   if event.row.params then
       local name = event.row.params.name
       local phone = event.row.params.phone

       row.nameText = display.newText(name, 12, 0, native.systemFontBold, 18 )
       row.nameText.anchorX = 0
       row.nameText.anchorY = 0.5
       row.nameText:setFillColor( 0 )
       row.nameText.y = 20
       row.nameText.x = 42

       row.phoneText = display.newText(phone, 12, 0, native.systemFont, 18 )
       row.phoneText.anchorX = 0
       row.phoneText.anchorY = 0.5
       row.phoneText:setFillColor( 0.5 )
       row.phoneText.y = 40
       row.phoneText.x = 42

       row:insert( row.nameText )
       row:insert( row.phoneText )
   end

   return true
end -- onRowRender
  1. event.rowでどのセルがレンダーに入ったかを取得できます
  2. event.row.paramsでinsertRowした際のparamsを取得できます

これで一通りテーブルビュー(リストビュー)の表示ができたと思います
f:id:popeyekn:20151211161140p:plain
最後に一通りの実装したコードを載せておきます

local widget = require 'widget'

local onRowTouch
local scrollListener

local myData = {}
myData[1] = { name="Fred",    phone="555-555-1234" }
myData[2] = { name="Barney",  phone="555-555-1235" }
myData[3] = { name="Wilma",   phone="555-555-1236" }
myData[4] = { name="Betty",   phone="555-555-1237" }
myData[5] = { name="Pebbles", phone="555-555-1238" }
myData[6] = { name="BamBam",  phone="555-555-1239" }
myData[7] = { name="Dino",    phone="555-555-1240" }


local function onRowRender( event )
   local row = event.row
   local id = row.index

   row.bg = display.newRect( 0, 0, display.contentWidth, 59 )
   row.bg.anchorX = 0
   row.bg.anchorY = 0
   row.bg:setFillColor( 1 )
   row:insert( row.bg )

   if event.row.params then
       local name = event.row.params.name
       local phone = event.row.params.phone

       row.nameText = display.newText(name, 12, 0, native.systemFontBold, 18 )
       row.nameText.anchorX = 0
       row.nameText.anchorY = 0.5
       row.nameText:setFillColor( 0 )
       row.nameText.y = 20
       row.nameText.x = 42

       row.phoneText = display.newText(phone, 12, 0, native.systemFont, 18 )
       row.phoneText.anchorX = 0
       row.phoneText.anchorY = 0.5
       row.phoneText:setFillColor( 0.5 )
       row.phoneText.y = 40
       row.phoneText.x = 42

       row:insert( row.nameText )
       row:insert( row.phoneText )
   end

   return true
end -- onRowRender

local myList = widget.newTableView {
    width = display.contentWidth, 
    height = display.contentHeight,
    onRowRender = onRowRender,
    onRowTouch = onRowTouch,
    listener = scrollListener
}

for i = 1, #myData do
    myList:insertRow{
        rowHeight = 60,
        isCategory = false,
        rowColor = { 1, 1, 1 },
        lineColor = { 0.90, 0.90, 0.90 },
        params = {
            name = myData[i].name,
            phone = myData[i].phone
        },
    }
end

参照

Tutorial: Advanced TableView tactics | Corona Labs

CoronaSDKでSocket IO

CoronaSDKでSocket IO

2015-12-08

もともとSocket.IOでチャットアプリを作っていたのですが、iOSAndroidのクライアントは用意されていて簡単にできます
が、CoronaSDKやluaには専用のクライアントフレームワークみたいなのがなかったのでSocket.IOのコードとか読んで実装してみました

※Socket通信などの理解レベルが多少曖昧なので以下の記述で通信できることはできましたが、申し訳ないですがかなり中途半端で危うい情報であるということを念頭に置いてお読みください

SocketIOとは

Socket.IO
リアルタイムウェブアプリケーションフレームワーク
様々なリアルタイムWeb技術をラップしてシンプルなインターフェースでリアルタイムWebを使用できるようにしてくれてるやつです

CoronaSDKのOSSにWebSocketができるものがあったのでWebSocketを使用してSockt.IOに対応しようと思います

WebSocketとは

WebSocket - Wikipedia
コンピュータネットワーク用の通信規格(プロトコル)の1つ
低コストでリアルタイム双方向通信ができるプロトコル

クライアントからハンドシェイクと呼ばれる要求をサーバに送ると、WebSocket専用コネクションを確立してその時点で双方向通信を開始することができます

Home | DMC Documentation
このライブラリを使用して実装していきます

実装

  • ライブラリ組み込み dmccuskey/dmc-websockets

    1. 上記をgithubからダウンロード
    2. coronaのプロジェクトディレクトリ(main.luaとかある場所)に下記を突っ込む
      • dmc_coronaディレクトリ
      • dmc_corona.cfg
      • dmc_corona_boot.lua
    3. 準備完了
  • コネクションの確立

    1. handshake urlを作成してサーバーへリクエスト handshake url example : http://example.com:80/socket.io/1/?t=1234567890112
    2. レスボンスからsidを取り出してwsプロトコルurl作成 example : ws://example.com:80/socket.io/1/websocket/9adubaadfadf09andf0
    3. 上記をwebsocetライブラリにつっこんで通信開始
  • 通信

    1. Messageの処理
      • Messageを受信すると下記のようなStringになっています
    '5::{name:honyarara,args:[{honya:honey}}'

5::という数字はMessage送信という意味なので5::の部分は切り取ってからjsonライブラリでdecodeすることでtableとして扱います
送信をするときにも下記の形式で送信します

  • WebSocketライブラリによるソケット通信の概要のコード例
local WebSockets = require 'dmc_corona.dmc_websockets' -- dmc-websocketsライブラリインポート
local function socket_handler(event)  -- websocketsのイベントを受け取るfunction
    local ev_type = event.type
    if ev_type == ws.ONOPEN then
        print 'WebSocket on open'
    elseif ev_type == ws.ONMESSAGE then
        print 'WebSocket on message'

        local mes = event.message
        local mesType = mes.data:sub(1,1)

        if mesType == '5' then -- got message
            local mes_json_str = mes.data:sub(5) -- 5::の部分の切り取り
            local json_table = json.decode(mes_json_str) -- jsondecodeしてtableとして扱う
            local ev_name = json_table.name or 'unknown' -- event name
            local args = json_table.args[1] or {} -- args
            print ('name : '.. ev_name)
            print ('args : '.. tostring(args))
        end -- mestype == 5

    elseif ev_type == ws.ONCLOSE then
        print 'WebSocket on close'
    elseif ev_type == ws.ONERROR then
        print 'websockets on error' 
    end -- if ev_type 
end -- socket_handler

ws = WebSockets:new{ -- 通信開始
    uri = s_url,
}
ws:addEventListener( ws.EVENT, socket_handler) --  リスナー登録

以上

CoronaSDKでもSocket.IOの通信ができました、わあい

参考

Client handshake returns error message "Transport unknown" · Issue #1577 · socketio/socket.io websocket example doesn't work once uploaded on mobile phone (iPhone6) · Issue #28 · dmccuskey/DMC-Corona-Library

Corona SDK(lua) の開発環境構築 Vim編

Corona SDK(lua) の開発環境構築 Vim

2015-12-07

Vim編と書いていますが、Vim編しか書きません
Vimmerなんで
しかし他のエディタでの環境構築方法にも通じるものはあると思います(適当)
とにかく自分がやったことの忘備録的なことを書くぞ書きます
あ、ちなみに私のマシンはMacです

最近のvimは最初からlua対応されていると思います (:versionってすると+lua/dynみたいのが入ってる)

上記を参考に構築しました

vimrcに追加した部分

(Syntastic,OpenBrowser,SurroundVim,NeoSnippetのPluginを使用)

"以下directoryにluaのsnippetファイルを入れておく
let g:neosnippet#snippets_directory='~/.vim/mysnippets'
"Syntasticでluaのsyntaxチェック
let g:syntastic_enable_lua_checker = 1
autocmd vimrc FileType lua call s:lua_my_settings()
    function! s:lua_my_settings()
    " CoronaSimulatorをF3で起動する 
    map <buffer><F3> :!open -a /Applications/CoronaSDK/Corona\ Simulator.app %:h/main.lua <CR><CR>

    " Openbrowserを使用して カーソル下の単語をCoronaDocks検索する
    nnoremap <buffer>gl :OpenBrowser https://cse.google.com/cse?cx=009283852522218786394%3Ag40gqt2m6rq&q=<c-r><c-w>+docs<cr> 
    " Openbrowserを使用してカーソル下の単語をググる
    nnoremap <buffer>gs :OpenBrowserSearch<Space><c-r><c-w><space>corona<cr> 
    " SurroundVimを使用してコードをコメントアウト
    let g:surround_{char2nr("-")} = "--[[ \r --]]"
    " 選択中に-でコメントアウト
    vmap <buffer>- S-
endfunction
  • vimrc補足
"以下directoryにluaのsnippetファイルを入れておく
let g:neosnippet#snippets_directory='~/.vim/mysnippets'

これについては
http://cutemachine.com/corona-sdk-tutorial/vim-snippets-for-corona-sdk-and-lua-development/Corona-SDK-Lua-Vim-Snippets.9c96a9bf.snippets
これをダウンロードしてlua.snipにリネームして使いました

" CoronaSimulatorをF3で起動する   
map <buffer><F3> :!open -a /Applications/CoronaSDK/Corona\ Simulator.app %:h/main.lua <CR><CR>  

これですが
Plain Old Blogumentation: VimからCorona SDKのシミュレーターを起動する方法
の方法だとvimの中でshellが起動してうざかったんで直接シミュレータでmain.luaを起動するようにしちゃってます

" SurroundVimを使用してコードをコメントアウト
let g:surround_{char2nr("-")} = "--[[ \r --]]"
" 選択中に-でコメントアウト
vmap <buffer>- S-

ここらへんについては

    --[[
    print('へいへいへい')
    --]]

とやるとコメントアウトがされて

    ---[[
    print('へいへいへい')
    --]]

と上のやつに-を追加するだけでコメントが外されます

・他にもUniteVimやVimfilerを駆使すればかなり便利に開発することができます
特にUniteVimは使いこなしましょう

その他補足

  • Androidで実機ビルドしようとすると、JavaをインストールしてくれとかPATHを通してくれとか言われますが、Errorメッセージの指示通りにやっていればできます

Corona SDKとは

Corona SDKとは

2015-12-07

最近Corona SDKにはまっているんで紹介します

  • 公式サイト
    Corona SDK | Corona Labs
  • マルチプラットフォームで動くアプリを開発できるSDK
  • 基本無料 ストアアップロードも無料 課金すればObj-CやJavaのネイティブAPIにアクセスできたりオフラインビルドできるようになったりする
  • Lua言語
  • ビルドしてから超瞬間的にシュミレータで確認できる(実機ビルドは少し遅いcoronaのサーバに問い合わせたりしてる)
  • 描画は基本的にOpenGLでされているのでひと昔前の激遅Android端末でもなかなか速い
  • ゲーム開発用の機能が十分に揃っている
  • ただ
    • ググっても日本語での解説がほとんどない

ちょっとすごいと思ったのは以下の画面
f:id:popeyekn:20151207180241p:plainf:id:popeyekn:20151207180247p:plain
サンプルプロジェクトをシュミレータでちょちょいとビルドしてみた画面なんですが
なんとiOS標準のUIパーツとかAndroid標準のUIパーツが再現されているんです
もちろんGL上で描画されているんで、AndroidiOSの画面を再現できたり、その逆もできそう

Corona SDKでHTTP通信

Corona SDKでHTTP通信


  • シンタックス

    request( url, method, listener [, params] )

    • 戻り値
      • requestId
        network.cancel(requestId)とすることでその通信をキャンセルします
    • 引数
      • url
        通信するURL ・String
      • method
        GETやPOSTやPUTなどメソッド名を指定 ・String
      • listener
        通信中(progress)、通信終了後のコールバック ・function
      • param
        パラメータの指定 ・table
        • headers
          • ヘッダ ・table
        • body
          ・String
        • その他のパラメータ
          bodytype,progress,response,timeout,handleRedirects

というのが基本です


  • 使用例
local function network_listener(event) --コールバックの関数  
    if event.isError then  
        print('network error')  
    else   
        print ( "RESPONSE: " .. event.response )  
    end  
end  

lcocal url = 'https://www.google.com'   
local method = 'GET'  

local params = {  
    headers = {  
        ['Accept-Language'] = 'ja-JP'  
    }  
    timeout = 15  
}  

network.request(url,method,network_listener,params)  
  • POST
    JsonでパラメータをPOSTしたい場合
local function network_listener(event) --コールバックの関数
    if event.isError then
        print('network error')
    else 
        print ( "RESPONSE: " .. event.response )
    end
end

local url = 'https://www.google.com' 
local method = 'POST'

local params = {
    headers = {
        ['Content-Type'] = 'applicatdon/x-www-form-urlencoded'
    },
    body = 'color=red&size=small'
}

network.request(url,method,network_listener,params)
  • headersにContent-Type = 'applicatdon/x-www-form-urlencoded'を指定して、bodyのパラメータは文字列型でkey=valueで&つなぎで指定する必要があります