Androidでcolspan

Android

Androidでcolspan

タイトルは、だいぶはしょりましたが、要はHTMLで書くテーブルのセルの連結、colspanをAndroidのレイアウトファイルでどうやるか、という話です。
いつもググっているので、書いておきます。

colspan=”2″はtablerowの中身に

 android:layout_span="2"

とつけるだけです。

 <?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" >
 
 <TableLayout
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:layout_gravity="center_horizontal"
     android:orientation="vertical"
     android:padding="1dp" >
 
 <!-- 普通の2列のテーブルの行 -->
 <TableRow
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:layout_marginBottom="1dp"
     android:background="@color/white"
     android:gravity="center_vertical"
     android:padding="5dp" >
 
       <CheckBox
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/check_box"
             />
 
       <Button
            android:id="@+id/bt1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:text="@string/button"
            " >
       </Button>
 </TableRow>
 
 <!-- 1列に連結された行 -->
 <TableRow
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:layout_marginBottom="1dp"
     android:background="@color/white"
     android:gravity="center_vertical"
     android:padding="5dp" >
 
        <CheckBox
             android:id="@+id/push_notification"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:layout_span="2"
             android:text="@string/push_notification"
             android:textColor="@color/black" />
 </TableRow>
 </TableLayout>
 </LinearLayout>

Android 開発時にGPS情報を設定

Android

Android 開発時にGPS情報を設定

開発している時のエミュレーターに、特定の場所の座標を表示したい場合などです。
座標はあらかじめ、取得しておきましょう

  1. Window→Open Perspective→DDMSでDDMSを表示します
  2. Emulator Controlパネルで、下の方にLocation Controlsという部分があるので、ここに10進法の緯度経度を登録したい場合はDecimalにチェックをつけて下のLongtideに経度、Latitudeに緯度を登録します。時・分・秒での緯度経度の場合は、Sexagesimalにチェックを入れて登録します。

Android レイアウト 中身が違っても均等な幅のボックスを作る

Android

Android レイアウト 中身が違っても均等な幅のボックスを作る

中々言葉で表現するのは難しいので、下記を見てほしいのですが、(弊社で開発しているSmart動態管理というソフトの一画面です)

device-smart_location-before.png

下の部分の「到着」「休憩」は同じ文字数だからよいのですが、上の「空車」「積込済」というのは字数が違いますよね。
なので、ボタンの幅がそろわず、見た目がよくないです。

均等な幅を実現するために android:layout_weight というパラメーターがありますが、これを指定していても、android:layout_width=”wrap_content”などがあると、android:layout_widthの方が優先されてしまうんですね。

この部分のソースコードはこんな感じです。

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:paddingTop="10dp"
            android:paddingBottom="5dp" >
           <Button
	            android:id="@+id/vacant_button"
	            android:layout_width="wrap_content"
 	            android:layout_height="60dp"
                    android:layout_weight="0.5"
 	            android:text="@string/vacant"
	            android:textSize="24sp"
	            android:background = "@drawable/vacant_button"
	            android:layout_marginRight="5dp"
                     />
            <Button
	            android:id="@+id/occupied_button"
	            android:layout_width="wrap_content"
	            android:layout_height="60dp"
                    android:layout_weight="0.5"
	            android:text="@string/occupied"
	            android:textSize="24sp"
	            android:background = "@drawable/occupied_button"
                 />
         </LinearLayout>

android:layout_widthをなくすと、NullPointerEcceptionでエラーになります。
かといって、可変にしたいので、絶対的な大きさにもしたくない…
テーブルレイアウトは面倒…
そんな場合は、android:layout_width=”0dp”として回避しています。

    //変更後のソースコード
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:paddingTop="10dp"
            android:paddingBottom="5dp" >
 
           <Button
 	            android:id="@+id/vacant_button"
 	            android:layout_width="0dp"
 	            android:layout_height="60dp"
                    android:layout_weight="0.5"
                    android:text="@string/vacant"
 	            android:textSize="24sp"
 	            android:background = "@drawable/vacant_button"
 	            android:layout_marginRight="5dp"
 	             />
            <Button
 	            android:id="@+id/occupied_button"
 	            android:layout_width="0dp"
 	            android:layout_height="60dp"
                    android:layout_weight="0.5"
 	            android:text="@string/occupied"
 	            android:textSize="24sp"
 	            android:background = "@drawable/occupied_button"
                 />
  	    
        </LinearLayout>

修正後の画面はこんな感じになります。

device-smart_location.png

Android リリースしたアプリの月額課金をテストする

Android

Android リリースしたアプリの月額課金をテストする

Androidアプリで月額課金を行う方法は
Androidアプリの月額課金の方法!
で紹介しておりますが、悩ましいのがテストです。
リリースする前のアプリでしたら、お試しができますが、一旦リリースしてしまうと、テストができません。
証明書でサインされ、Google Playから配布されたアプリでないと、課金については
「このバージョンのアプリには、Google Playを通じたお支払いはご利用になれません。」
というメッセージが返ってきてしまうからです。

仕方ないので、Google Playに用意されている「アルファ版」のテスト機能を使います。
他によい方法があれば、ぜひ教えてください。m(_ _)m

アルファ版の使い方で、わかりづらいこともあったので、下記のページに書いておきます。

Google Play アルファ版機能を利用してアプリをテストする

Android プログラム上でビデオをトリミングするサンプルコード

Android
Android ビデオ録画機能を作る

Android プログラム上でビデオをトリミングするサンプルコード

Androidでビデオ(動画)を作って、トリミングするアプリを作っています。
トリミングするというのはひとつの動画があるとすると、どこからかどこかまでを切り取ってコピーし他のファイルにペーストするというアレです。

最初の録画とかプレビューについて、苦戦したのはAndroid ビデオを録画するサンプルコードに記述したとおりです。

で、トリミングについても予想通り苦戦しましたので、書いておきます。

基本的にはライブラリとしてmp4parserを使います。sanniesさん、ありがとう!!
https://github.com/sannies/mp4parser

二つの動画をあわせたりするプログラムは、下記のサイトさんが公開していて大変参考になりました。ありがとうございます!

Developers.IO
http://dev.classmethod.jp/smartphone/android/mp4parser/

しかし、秒数を指定して、何秒から何秒までをトリミングするのか、というのはどうやってやればよいのかわかりませんでした。
最初は単純にDevelopers.IOさんのサイトで書かれている

 movie.addTrack(new AppendTrack(new CroppedTrack(track, 200, 400)));

で引数にされている、200とか400はミリセカンドだろうぐらいに思っていたのですが、これはサンプリングとあわせる必要があり、適当にやっていると

 mp4parser java.lang.IndexOutOfBoundsException

の嵐になります。

あーらし、あーらし、For dream。

さて、サンプリングとか、どうでもいいんだよ!というせっかちな貴殿のために、もうサンプルコードに突入します。

これは、下記で公開されているトリミングのコードを元に、mp4parserの1.0.5.4にあわせました。

TrimVideoUtils.java
https://android.googlesource.com/platform/packages/apps/Gallery2/+/android-4.2.2_r1/src/com/android/gallery3d/app/TrimVideoUtils.java

合わせる方法は、上記のTrimVideoUtils.javaの作者、Eduardさんが、下記にかいてくれていますが、なんか抜粋とかでわかりにくかったので、まとめた次第です。

https://groups.google.com/forum/#!topic/mp4parser-discussion/d6qklvfdDZg

 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.channels.WritableByteChannel;
 import java.util.LinkedList;
 import java.util.List;
 
 import com.coremedia.iso.boxes.Container;
 import com.coremedia.iso.boxes.MovieHeaderBox;
 import com.googlecode.mp4parser.FileDataSourceImpl;
 import com.googlecode.mp4parser.authoring.Movie;
 import com.googlecode.mp4parser.authoring.Track;
 import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
 import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
 import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
 import com.googlecode.mp4parser.util.Matrix;
 import com.googlecode.mp4parser.util.Path;
 
 public class TrimVideoUtils {
 
    /**
     * ビデオをトリミングする
     * @param src トリミングするもともとのファイル
     * @param dst トリミングしたファイルの書きだし場所
     * @param startMs スタートのミリセカンド
     * @param endMs 終了のミリセカンド
     * @throws IOException
     */
 
    public static void startTrim(File src, File dst, int startMs, int endMs) throws IOException {
 
        FileDataSourceImpl file = new FileDataSourceImpl(src);
        Movie movie = MovieCreator.build(file);
 
        // remove all tracks we will create new tracks from the old
        List<Track> tracks = movie.getTracks();
        movie.setTracks(new LinkedList<Track>());
        double startTime = startMs / 1000;
        double endTime = endMs / 1000;
        boolean timeCorrected = false;
 
        // Here we try to find a track that has sync samples. Since we can only
        // start decoding
        // at such a sample we SHOULD make sure that the start of the new
        // fragment is exactly
        // such a frame
        for (Track track : tracks) {
            if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
                if (timeCorrected) {
                    // This exception here could be a false positive in case we
                    // have multiple tracks
                    // with sync samples at exactly the same positions. E.g. a
                    // single movie containing
                    // multiple qualities of the same video (Microsoft Smooth
                    // Streaming file)
                    throw new RuntimeException(
                            "The startTime has already been corrected by another track with SyncSample. Not Supported.");
                }
                startTime = correctTimeToSyncSample(track, startTime, false);
                endTime = correctTimeToSyncSample(track, endTime, true);
                timeCorrected = true;
            }
        }
        for (Track track : tracks) {
            long currentSample = 0;
            double currentTime = 0;
            long startSample = -1;
            long endSample = -1;
            for (int i = 0; i < track.getSampleDurations().length; i++) {
                if (currentTime <= startTime) {
 
                    // current sample is still before the new starttime
                    startSample = currentSample;
                }
                if (currentTime <= endTime) {
                    // current sample is after the new start time and still
                    // before the new endtime
                    endSample = currentSample;
                } else {
                    // current sample is after the end of the cropped video
                    break;
                }
                currentTime += (double) track.getSampleDurations()[i]
                        / (double) track.getTrackMetaData().getTimescale();
                currentSample++;
            }
            movie.addTrack(new CroppedTrack(track, startSample, endSample));
        }
 
        Container out = new DefaultMp4Builder().build(movie);
        MovieHeaderBox mvhd = Path.getPath(out, "moov/mvhd");
        mvhd.setMatrix(Matrix.ROTATE_180);
        if (!dst.exists()) {
            dst.createNewFile();
        }
        FileOutputStream fos = new FileOutputStream(dst);
        WritableByteChannel fc = fos.getChannel();
        try {
            out.writeContainer(fc);
        } finally {
            fc.close();
            fos.close();
            file.close();
        }
    }
 
    private static double correctTimeToSyncSample(Track track, double cutHere, boolean next) {
 
        long currentSample = 0;
        double currentTime = 0;
        long startSample = -1;
        long endSample = -1;
        
        for (int i = 0; i < track.getSampleDurations().length; i++) {
            if (currentTime <= cutHere) {
 
                // current sample is still before the new starttime
                startSample = currentSample;
            }
            if (currentTime <= cutHere) {
                // current sample is after the new start time and still before the new endtime
                endSample = currentSample;
            } else {
                // current sample is after the end of the cropped video
                break;
            }
            currentTime += (double) track.getSampleDurations()[i] / (double) track.getTrackMetaData().getTimescale();
            currentSample++;
        } 
        
        return currentTime;
    }
 }