「アイマス専用音楽プレイヤーソフト」のAndroid版を作ってみたが完成しなかった

ニコニコ動画で気になる動画がありました。

 

アプリの内容は動画を見てもらいたいのですが、これのAndroid版を作れないのかな、と思ってやってみました。

 

アプリとしては全く使い物にならないので、配布してみます。
問題があればすぐ削除します。
『アイマス専用音楽プレイヤーソフトforAndroid』ダウンロードページ

 

結論から言うと完成しませんでした。
開発中にいくつかの問題点が発生し、その要因がプログラムではなくAndroid端末仕様によるものであるため、解決が不可能(あるいは解決できてもかなりの工数がかかる)と判断したため、開発を断念します。
悔しい。

 

問題点1.メモリが圧倒的に足りない

最近のAndroid端末はメモリも増えてきたのでマシではありますが、ほとんどの端末では「オケ+12人分」の音声ファイルを同期させて処理するのはメモリ不足で難しいです。再生できても途中でフリーズしたり、変に遅延したりします。プログラム側で処理を軽くする対策を講じましたが、付け焼刃にしかなりませんでした。

 

問題点2.50MB制限の壁

Androidアプリは、「1アプリにつき容量が最大50MBまで」という制限があります。プログラム本体の容量は非常に小さいのですが、問題は音声ファイルです。
1曲実装するためには、「オケ+12人分」の音声ファイルで、だいたい25MBの音声ファイルが必要です。つまり、2曲もアプリに実装すると、それだけで50MBを超えてしまい、インストールできなくなります。
それを解決する方法として、「初回起動時に、アプリ内に必要なファイルをウェブからダウンロードして保存する」という方法があります(本格的なゲームアプリなどでよく見られる手法です)。しかし、その機能を実装しようとすると、アプリのソースの修正だけでなく、ウェブの構築なども必要になり、一気に工数と難易度が跳ね上がります。

 

問題点3.保存先の確保

では、頑張って問題点2を解決したとします。すると今度は、「ウェブから取得したデータをどこに保存するか?」という問題が発生します。
10曲分の音声ファイルを保存するとして、約250MBもの大容量のデータを、Android端末に保存するのは、果たして現実的でしょうか? 本体、またはSDカードに空き容量がない場合はどうするのでしょうか?
これを解決するには、アプリ側に、ファイル管理機能を付けなくてはいけません。選択的にどの曲をダウンロードしてくるかを決定し、すでにダウンロード済みの曲を任意に削除できるようにしなくてはなりません。
そこでまた工数と難易度があがります。

 

Android仕様要因ではない問題点.素材のありかが解らない

動画内でふんだんに使われている素材、あれ何なんですかね。どこでゲットしてきてるんですか? 軽く調べてみたのですが、見つかりません。

 

以上の要因から、開発は断念させていただきます。

 

悔しい、悔しいよー。

さっそうと完成させて、ニコニコにドヤ顔で動画あげたかったよー。

全ソース一式、Githubで公開しています。

作業時間:20時間くらい

https://github.com/gucci1208/imas_player.git

ギャラリーから画像を選択するときに一部端末でnullになる

ギャラリーから画像を選択して、アプリ内に持ってくるときに、URIをFileに変換していると思います。

こんな感じでIntentして

private Uri mImageUri; //画像のURI
private String image; //画像のパス
final private int IMAGE_SELECT = 10002;

//画像を選ぶイベント
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(intent, IMAGE_SELECT);

こんな感じでonActivityResultして

@Override
protected void onActivityResult(int requestCode, int resultCode,Intent intent) {
	// キャンセルボタンで戻ってきたときはRESULT_CANCELEDが入る
	if (resultCode == RESULT_OK) {
		switch (requestCode) {
		case intent_code1:
			break;
		case intent_code2:
			break;
		case IMAGE_SELECT:
			if (intent != null) {
				mImageUri = intent.getData();
				image = getPath(mImageUri);
			}
			break;
		default:
			break;
		}
	}
}

こんな感じで変換します

// uriからpathを取得するメソッド
public String getPath(Uri uri) {
	String[] projection = { MediaStore.Images.Media.DATA };
	Cursor cursor = managedQuery(uri, projection, null, null, null);
	int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
	cursor.moveToFirst();

	//無事取得できました
	String result = cursor.getString(column_index);
	return result;
}

しかし、一部端末ではこの方法で上手くいかなくなりました。
uriからpathに変換する過程で、nullになってしまうのです。

最初、原因は変換メソッドであるgetPath()にミスがあるのではないかと思いました。
が、しかしどうやら違うようで、実は、ギャラリーを立ち上げるintent部分に問題があったのです。
ただしいintentはこちら。

Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(Intent.createChooser(intent, "Select picture"), IMAGE_SELECT);

この”Select picture”の部分は、任意のメッセージを入れてください。
setActionの値が、Intent.ACTION_GET_CONTENTからIntent.ACTION_PICKになりました。
これで全ての端末で問題なくギャラリーの画像を取得できるようになりました。

何が問題なのかは解っていませんが、Intent.ACTION_GET_CONTENTでギャラリーを立ち上げたときは、削除したはずの古い画像も一覧に表示されていました。もちろんその古い画像は選択しませんでしたが、存在するはずの画像でもnullになったので、もしかしたら、ギャラリーのサムネイルを表示するためのキャッシュにアクセスしていたのかもしれません。

javaにおけるboolean変数の反転の簡単なやりかた

boolean変数を、「trueならfalseに」「falseならtrueに」と現在の値から反転させる方法についてです。

これまで私は

if (piyo) {
	piyo = false;
}
else {
	piyo = true;
}

という書き方をしていました。

しかし、それよりももっと簡単な方法があったのです。

piyo = !piyo;

これだけです。1行ですみます。

ViewPagerでwrap_contentを有効にする簡単な方法

ViewPagerは全画面で設置することが想定されているのか、wrap_contentをサポートしていません。
前回の投稿で、main.xmlのViewPagerビューをandroid:layout_height=”wrap_content”とすると、高さが0になってしまいます。

これを解決しようとすると、下記のページが参考になるでしょう。
Android: I am unable to have ViewPager WRAP_CONTENT

なかなか難しそうです。
もっと簡単にwrap_contentを有効にする方法はないかと試行錯誤しました。
そして、ちょっと卑怯っぽいですが、とても簡単に有効にする方法を見つけました。

まずはレイアウトxml部分。

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />

前回の投稿から、android:layout_height=”fill_parent”からwrap_contentに変更します。
そして、下記のような追記をします。

<ImageView
    android:id="@+id/pager_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:adjustViewBounds="true"
    android:scaleType="fitXY"
    android:src="@drawable/image01" />

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />

このImageViewはどこから来たかというと、ViewPagerの1ページごとに貼りつける画像です。

いったんこのままで実行してみてください。
当たり前ですが、image01が貼りつけられた、動かないImageViewが載っているだけかと思います。

重要なのはここからです。
実際にViewPagerを動かすとき、どれくらいの高さにしたいのでしょうか。
そうです、今回追記したImageViewのそのままの高さにしたいのです。
では、ImageViewの高さを取得しましょう。

参考:
TextViewやImageViewなど、設置されているViewのサイズを取得したいとき
[MainActivity.java]

@Override
public void onWindowFocusChanged(boolean hasFocus) {
	super.onWindowFocusChanged(hasFocus);
	//いったん仮でImageViewに元画像をセットして、ImageViewの高さだけ取得してView.GONE
	ImageView pager_image = (ImageView)findViewById(R.id.pager_image);
	int height = pager_image.getMeasuredHeight();
	pager_image.setVisibility(View.GONE);

	//viewPagerに高さをセット
	@SuppressWarnings("deprecation")
	LayoutParams lp = new LayoutParams(LayoutParams.FILL_PARENT, height);
	viewPager.setLayoutParams(lp);
}

高さをheightに取得したあとは、ImageViewは用済みなのでView.GONEです。
あとは、ViewPagerにLayoutParamsでheightを当てはめたらwrap_contentが有効(にしたのと同じ効果)になります!

けっこう、「邪道」というか「裏ワザ」というか、コードを駆使して問題を解決したというよりも、ズルがしこく切り抜けたという感じの方法ですね。(自分でもそう思います。汗)
あと、ライフサイクル的にこの方法が問題ないのか、というのもちょっと気になります。色んな端末でチェックしたところ、今の所は問題なさそうですが……。

もし何か危険なことがあれば、どなたかコメント欄で教えてくださいm(__)m

ViewPagerの実装

今回はViewPagerの実装の仕方についてです。

ViewPagerは動かすまでにたくさん記述しなくてはいけないので、なかなか時間がかかりますが、これがあるだけでなんとなくアプリがリッチに見えるのでオススメです!

今回は、画像とページ名をセットで表示するようなViewPagerを作っていきます。
まずはページ名を決めていきましょう。この配列の要素数によって挙動が変わってきます。
[strings.xml]

<string-array name="data_names">
  <item>テキスト1</item>
  <item>テキスト2</item>
  <item>テキスト3</item>
  <item>テキスト4</item>
  <item>テキスト5</item>
  <item>テキスト6</item>
</string-array>

これでテキストの準備は完了です。

それでは、レイアウトxmlにViewを追加します。
[main.xml]

<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal" >

    <Button
        android:id="@+id/bt_l"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/button_back"
        android:onClick="LEFT"
        android:text="" />

    <TextView
        android:id="@+id/data_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:text="" />

    <Button
        android:id="@+id/bt_r"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/button_next"
        android:onClick="RIGHT"
        android:text="" />
</LinearLayout>

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_weight="1"
    android:layout_width="fill_parent"
    android:layout_height="0dp" />

このとき、ViewPagerのwidthやheightをwrap_contentにしてはいけません。
Viewの高さが反映されないようで、wrap_contentだと無効化されてしまいます。

次に、アクティビティ側の宣言。
[MainActivity.java]

public class MainActivity extends Activity {
	private String[] data_strs;
	private int data_num;
	private int now_page;
	private ViewPager viewPager;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		
		//ページビューのセット
		now_page = 0;
		data_strs = getResources().getStringArray(R.array.data_names);
		data_num = data_strs.length;
		SetPageView();
	}

	private void SetPageView() {
		viewPager = (ViewPager) findViewById(R.id.viewpager);
		PagerAdapter mPagerAdapter = new AdapterPager(this);
		viewPager.setAdapter(mPagerAdapter);

		//ページが変わったことを受け取るイベント
		viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
			@Override
			public void onPageSelected(int position) {
				now_page = position;
				SetArrowImages(position);
			}
		});

		viewPager.setCurrentItem(now_page);
		SetArrowImages(now_page);
	}

	//矢印の見え方を定義
	private void SetArrowImages(int position) {
		Button arrow_l	= (Button)findViewById(R.id.bt_l);
		Button arrow_r	= (Button)findViewById(R.id.bt_r);
		TextView data_name	= (TextView)findViewById(R.id.data_name);

		if (position == 0) {arrow_l.setVisibility(View.INVISIBLE);}
		else {arrow_l.setVisibility(View.VISIBLE);}

		if (position == (data_num-1)) {arrow_r.setVisibility(View.INVISIBLE);}
		else {arrow_r.setVisibility(View.VISIBLE);}

		data_name.setText(data_strs[position]);

		if (position > 0) {
			arrow_l.setText(data_strs[position - 1]);
		}
		if (position < (sound_num-1)) {
			arrow_r.setText(data_strs[position + 1]);
		}
	}

	public void LEFT(View v) {
		now_page = ((now_page + data_num - 1) % data_num);
		viewPager.setCurrentItem(now_page, true);	//2番目の引数はアニメーションの有無
	}
	public void RIGHT(View v) {
		now_page = ((now_page + 1) % data_num);
		viewPager.setCurrentItem(now_page, true);
	}
}

main.xmlに、ページをめくる左右の矢印ボタンがあるという想定です。
SetArrowImages()メソッドは、それを設定するためです。
矢印ボタンは、ページ位置に応じて消えたり現れたりしますし、ボタンの文字領域に、前ページと次ページのページ名が表示されます。

次はアダプターです。
[AdapterPager.java]

public class AdapterPager extends PagerAdapter {
	private int N;
	private LayoutInflater _inflater = null;

	String[] names;

	public AdapterPager(Context c) {
		super();
		_inflater = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		names = c.getResources().getStringArray(R.array.data_names);
		N = names.length;
	}

	@Override
	public Object instantiateItem(ViewGroup container, int position) {
		LinearLayout layout = (LinearLayout) _inflater.inflate(R.layout.page_data, null);

		//ページごとに表示する画像
		ImageView img = (ImageView) layout.findViewById(R.id.frame_image);
		int rsrc[] = {
				R.drawable.image01,
				R.drawable.image02,
				R.drawable.image03,
				R.drawable.image04,
				R.drawable.image05,
				R.drawable.image06,
		};
		img.setImageResource(rsrc[position]);

		container.addView(layout);
		return layout;
	}

	@Override
	public void destroyItem(ViewGroup container, int position, Object object) {
		((ViewPager) container).removeView((View) object);
	}

	@Override
	public int getCount() {
		return N;
	}

	@Override
	public boolean isViewFromObject(View view, Object object) {
		return view.equals(object);
	}
}

アダプターはこんな感じですが、ページごとのレイアウトを定義するpage_dataがまだありませんね。

page_dataを作っていきましょう。
[page_data.xml]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/frame_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:scaleType="fitXY"
        android:src="@drawable/image01" />

</LinearLayout>

以上です。

これらを実装して動作させてみましょう!
今回はコードが多くて記事を書くのに疲れました。。。

Top