DropboxAPIを使ってファイル一覧を表示

DropboxAPIの使い方を知るためにファイル一覧を表示できるものを作ってみた

ダウンロード

以下のURLからSDKはダウンロードできる。

API keyの発行

以下のURLにアクセスしてAPI keyをとる

Android Manifestを修正

InternetにアクセスするPermissionが必要だから追加

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

ついで、認証用のdropboxのActivityを設定。db-hogehogeのhogehoge部分をAPI Keyに変更する。db-は必要な文字列みたいだから消しちゃダメ。

    <activity
      android:name="com.dropbox.client2.android.AuthActivity"
      android:launchMode="singleTask"
      android:configChanges="orientation|keyboard">
      <intent-filter>
        <!-- Change this to be db- followed by your app key -->
        <data android:scheme="db-ここをAPIキーに変更" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE"/>
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
    </activity>

認証

以下のコードで認証。KEYとAccessTypeはApplicationを登録した際の設定に変更する。ActivityNameって書いてる部分は省略してthisのみとかで書くとエラーに成ったんで注意が必要。

	final static private String APP_KEY = "api-key";
	final static private String APP_SECRET = "api-secret";
	final static private AccessType ACCESS_TYPE = AccessType.DROPBOX;

	AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET);
	AndroidAuthSession session = new AndroidAuthSession(appKeys, ACCESS_TYPE);
	DropboxAPI<AndroidAuthSession> mDBApi = new DropboxAPI<AndroidAuthSession>(session);
	mDBApi.getSession().startAuthentication(ActivityName.this);

ファイル一覧を取得

DropboxAPIのmetadataでフォルダ情報を取ってきて、contents内にそのフォルダ配下のファイル情報があるからそれを取ってきてるだけ。

	List<Entry> entryList = new ArrayList<Entry>();
	try {
		Entry rootEntry = mDBApi.metadata(path, 10, null, true, null);
		List<Entry> entries = rootEntry.contents;
		for (final Entry entry : entries) {
			entryList.add(entry);
		}
	} catch (DropboxException e) {
		e.printStackTrace();
	}

Entryを表示

こんぐらいあれば表示できるようになるはず。entry.pathをmDBApi.metadata()に食わせてやればフォルダを掘っていくことができるようになる。

	for (Entry entry : entryList) {
		Log.d(TAG, entry.path); // ファイルパス
		Log.d(TAG, entry.fileName()); // ファイル名
		Log.d(TAG, String.valueOf(entry.isDir)); // エントリーがフォルダか?
	}

毎回認証が走るのがだるい

これは非常にだるいからTokenを保存して使う方法もある。

	// こんな感じで保存して
	SharedPreferences sp = getSharedPreferences('dropbox', MODE_PRIVATE);
	Editor edit = sp.edit();
	edit.putString('token', tokens.key);
	edit.putString('tokenSecret', tokens.secret);
	edit.commit();

	// こんな感じで使う
	SharedPreferences sp = getSharedPreferences('dropbox', MODE_PRIVATE);
	String accessToken = sp.getString('token', "");
	String accessTokenSecret = sp.getString('tokenSecret', "");
	AccessTokenPair tokens = new AccessTokenPair(accessToken, accessTokenSecret);
	mDBApi.getSession().setAccessTokenPair(tokens);

	// んで、認証判定して未認証なら認証開始みたいなね
	if (!mDBApi.getSession().authenticationSuccessful()) {
		mDBApi.getSession().startAuthentication(MainActivity.this);
	}

まとめ

携帯端末だと容量が少ないとかいうけど、SD使ったりすると別段そこまで気にならない。僕がDropboxを使うのは一つを修正すれば全てが変わるってところがいいんだと思う。パソコンでいじくっているものを携帯端末でちょっとだけ見れるとかそういう部分を作る際に気軽に使えるDropboxはいいよね。大体わかったからなんか作ろうかと思ってたらSyncAPIってのが出たっていうね。こっちのが便利そうだからこっちも調べてブログに書いてみようかと思う。

Support LibraryでLoaderを使ってみた

先日Androidの開発環境を新しくしたら初期作成できるアプリの雛形が増えてた。

  • Tab
  • Dropdown
  • Tab and Swipe
  • Swipe and Title

これは便利と早速試してみようとしたんだけど、これってminSdkVersionがAndroid3系以上じゃないとそもそも選択できないみたい。まだまだ2系って主流だからそれを切り捨てて使うことはできないんで、SupportV4使ってどうにかできないものかと調べてみた。supportV4のrefarenceは以下のURLに乗っている。

ActionBarってSupportV4で対応したかなぁと思ってたんだけど、まだ対応してないみたいね。。。

The ActionBar is not supported by the library. However, when creating your Options Menu, you can declare which items should be added to the Action Bar when it’s available (on Android 3.0 or later). You can do so with the MenuCompat.setShowAsAction() method, for example:

ActionBarはまだ対応してないけど、MenuCompat.setShowAsAction()を使ってOptionMenuで入れてる奴は2系だとOptionボタンで、Android4系だとActionBarに表示されますよって意味かな。ActionBarはAndroid2系では完全に諦めるとして、他にSupportV4でどういうことができるようになるのか?って言うことなんだけど、以下のClassに対応したって書いてある。

  • Fragment
  • FragmentManager
  • FragmentTransaction
  • ListFragment
  • DialogFragment
  • LoaderManager
  • Loader
  • AsyncTaskLoader
  • CursorLoader

FragmentとLoaderに対応したのかー。FragmentはSwipeで使ったことあるけど、Loaderってつかったことないからちょっと調べてみた。Loaderがどんなものかってのは以下にのってる。

ざっくり読んだところ、バックグラウンドでデータを取得することができますよって感じっぽい。まぁ、名前からしてそうですね。AsyncTaskのデータ取得に特化したものって考えればいいのかな。ContentProviderからデータを取得したい場合はCursorLoaderつかって、それ以外はAsyncTaskLoaderを使うってのが基本らしい。

んじゃ、早速コードを組んでみるかと以下を参考にLoaderを試してみた。

結果、エラー。。。コンパイルすら通らないレベル。エラー箇所は以下。

		getLoaderManager().initLoader(0, null, loaderSample);

んで、エラー文言がこんな感じ。

The method initLoader(int, Bundle, LoaderManager.LoaderCallbacks) in the type LoaderManager is not applicable for the arguments (int, null, MainActivity.LoaderSample)

エラー内容的にgetLoaderManager()で取得できているのがsupport.v4のじゃなくて、API LEVEL11以上のやつを持ってきてるっぽい。import文を書き換えたりとか色々してもだめで、頭を悩ましていたらActivityじゃ使えないらしい・・・。

これにそって継承をActivityからFragmentActivityに変更して、getLoaderManager()ではなくてgetSupportLoaderManager()を使うことでsupport.v4の方のLoaderCallbacksを引数に取るようになって使うことができましたとさ。って、FragmentActivityを絶対使わないとダメだってどういう仕様なんだよっていう。

FragmentAcitivityとかPagerつかってFragmentバリバリ使ってるようなアプリだったらLoader使うことはできそうだけど、Android2系を切り捨てることができない現状としてLoaderを使うのはまだまだ先っぽい。そもそもだけど、Loader使ったらちょっと書きやすくなるっていう程度でAsyncTaskとかで代用できそうだし別に使う必要もないよね。

と、ここまで書いてリファレンスを再度読んだらFragmentActivityのを使いなさいって書いてありますね・・・。

To manage your fragments and loaders, you must use the methods FragmentActivity.getSupportFragmentManager() and FragmentActivity.getSupportLoaderManager() (instead of the getFragmentManager() and getLoaderManager() methods).

リファレンスはちゃんと読まないとダメですね。。。

参照先へのツッコミ

これに以下の記述があるけど、そんなことないよね。

Loaderを呼び出すActivity/Fragmentには、LoaderCallbacksインターフェースをimplementsする必要があります。

これは、initLoader()の第三引数にthisを渡してるからimplementsされてるだけで、別クラスを定義して第三引数に設定してあげれば全然問題なく動く。僕はActivityに複数implementsして引数にthisを渡しまくるのはあまり好きじゃないから使うなら分割して使いそう。

Androidの開発環境を更新した(ADT Bundle)

僕の環境にAndroidの開発環境を入れたのが今から1年ほど前なので時間があったのでアップデートしてみた。

開発環境の構築がスゲー楽。前はEclipseダウンロードしてきて、AndroidSDK入れて、パス通してとかやらないといけなかったのに、今回は上記リンクからダウンロードしてきて解凍するだけで基本的に終了、なんと素晴らしいことでしょう。やっぱり開発初心者の方が始めようと思って開発環境の構築でつまずいてしまってヤル気を失うのはもったいないので、プラットフォームを売るっていう意味で裾の尾を広げるには開発環境を楽に作れる状況を作るってのは大事なんだろうね。と、思ったけど何点かつまずいたので備忘録。

アプリケーション作成ってやっても画面がでない

なんか初期画面でこういうことでいますよって画面が出てるんだけど、これが新規アプリケーションを作成しても自動に閉じない。アプリケーションのコードを表示している画面がその裏にあるせいで、新規作成されたアプリはどうやって開けばいいってちょっと悩んだんだけど、普通に表示されている画面の右上にいる×ボタンで閉じればみなれた画面が出てくる。

Layoutファイル画面がエラー

アプリを開くと自動的にLayoutファイルが開かれるみたいなんだけど、いきなりエラー画面がバーンって出てくる。慌てず騒がず[windows]→[Android SDK Manager]を使って必要なモジュールをダウンロードしましょう。いきなりエラー画面が出るのは親切なのかなんなのかよくわからないけどちょっとビビった。

フォルダを移動すると動かない

解凍すると「sdk」「eclipse」って2つのフォルダができて、どうも起動時にAndroidSDKのパスに自動的に「/folderName/sdk」が設定されるみたい。だからフォルダを移動するとSDKが見つからんって怒られる。[Prefarence]→[Android]で[SDK Location]を変更したパスに変えてあげれば大丈夫。

まとめ

環境構築が楽なのは嬉しいですね。起動時に出てくる画面が違うってのもちょっといいかも。でもそのせいでEclipse使ってるっていうことを知らずにEclipseの便利なことが検索されなさそうな気がしなくもない。

ImageDownloaderを作った

カッとなって作った。

どんなソフトかっていうとブラウザでネットを見ている最中になんかいい画像があったら保存したくなるじゃないですか。それを簡単にダウンロードできたら需要があるかなぁと思って簡単にダウンロードできるやつを作ってみた感じ。もともと会社でピンタレスト的なことをやりたくて作ったんだけどおじゃんになったからせっかくコード考えたしって思って作った奴。ちゃんと会社のコードとは別に1から作り直してますよ。パクったとか言わないでくださいね。

ちなみに著作権で保護された画像をダウンロードすることは違法です。そんなことには使わないでくださいね。Yukiちゃんが可愛いからってYukiちゃんの画像まとめとかひらいてダウンロードしちゃったりしたらダメですよ。まぁそんなこといってもばれなければ うわなにをするやめrくぁwせdrftgyふじこlp;@:

AndroidのServiceについて調べてみた

Serviceについて調べてみたのでまとめ。

Serviceを起動前にBindしても起動できる

bindService()でServiceにBindすると一緒にServiceがCreateされる。ただし、bindService()で起動されたServiceはunbindService()された時に殺される。unBindした後も起動しておいて欲しい場合はstartService()後にbindService()してあげる事でunbindService()したとしても殺されない。bindしたActivityがunbindせずに死ぬと例外吐くからちゃんとonDestoryとかでunbindしてあげましょう。

Serviceはメモリがなくなったら殺される

Backgroundで起動して動いている間は死なないものだと思っていたけど、Serviceはメモリがなくなったら死ぬ。メモリがなくなった場合はonDestroyも通らずに強制的にKillされるらしい。なので、常駐させる場合はstartForeground()で今起動中だということを教えてあげると表示中のActivityと同じぐらい優先度が上がってそうそう殺されなくなる。ただメモリがどうしてもなくなったときは殺されるらしいが、それは気にしなくていい程度の頻度だそうで。startForeground()で起動中にした場合は、stopForeground()を呼んであげる事で死んでも大丈夫だよと教えることができる。

startForeground()でNotificationを出したくない場合

ユーザに起動中というのを教えるためにService起動中はNotificationを出したほうがいいとは思うけど、どうしても出したくない場合は引数に渡すnotificationのiconを0で渡せばNotificationになにも表示されなくなる。最も手軽な方法は以下だと思う。

startForeground(NOTIFICATION_ID, new Notification()) 

aidlでActivity→Serviceの連携

普通にaidlファイルを作ってonBind()の戻り値として返してあげるとbindService()の引数で渡すServiceConnection#onServiceConnectedで取得することができる。取得した後はaidlで定義されたInterfaceを使って自由に色々やりましょうって感じ。後、AndroidManifest.xmlにactionとしてaidlを設定してあげないと通信できないらしい。

Binderを継承してActivity→Serviceの連携

aidlを使用した連携とほぼ同じ。ただし、ActivityとServiceが同一プロセスで動いている場合じゃないとだめ。こっちのほうがお手軽にできるからActivityとServiceが同一プロセス上で動く保証があるならこっち使ったほうがいいんじゃないかと。ってか、プロセスが違う場合ってServiceが起動している状態でActivityが再起動した時とかかだと思うからそういうことが起こりそうならaidl使って連携したほうがいいはず。

BroadcastReceiverでService→Activityの連携

Service側からBroadCastでメッセージ投げてActivityで受け取るだけ。bindしてんだから関数で呼び出せないものかと思ったけど、Messageで通信するのがいいっぽい。receiverはandroid:exported=”false”を設定してあげて同一アプリ無いじゃないと受け取らないようにしておいたほうがセキュリティ上いいと思う。

サンプル

ServiceSample.java

package com.choilabo.activityservicebindsample;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Toast;

public class ServiceSample extends Service {

	private IServiceSample.Stub serviceSampleInf = new IServiceSample.Stub() {
		@Override
		public void makeToast(String value) throws RemoteException {
			Intent intent = new Intent("param");
			sendBroadcast(intent);
		}
	};
	
	@Override
	public void onRebind(Intent intent) {
		super.onRebind(intent);
		Toast.makeText(this, "onRebind ServiceSample", Toast.LENGTH_SHORT).show();
	}

	@Override
	public boolean onUnbind(Intent intent) {
		super.onUnbind(intent);
		Toast.makeText(this, "onUnbind ServiceSample", Toast.LENGTH_SHORT).show();
		return true;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		Toast.makeText(this, "onCreate ServiceSample", Toast.LENGTH_SHORT).show();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		Toast.makeText(this, "onDestroy ServiceSample", Toast.LENGTH_SHORT).show();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Toast.makeText(this, "onStartCommand ServiceSample", Toast.LENGTH_SHORT).show();
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onStart(Intent intent, int startId) {
		super.onStart(intent, startId);
		Toast.makeText(this, "onStart ServiceSample", Toast.LENGTH_SHORT).show();
	}

	@Override
	public IBinder onBind(Intent intent) {
		Toast.makeText(this, "onBind ServiceSample", Toast.LENGTH_SHORT).show();
		return serviceSampleInf;
	}
	
	public void makeToast() {
		Toast.makeText(this, "This is service message", Toast.LENGTH_SHORT).show();
	}
}

MainActivity.java

package com.choilabo.activityservicebindsample;

import com.choilabo.activityservicebindsample.R;

import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {

	private Button makeToast;
	private Button btnBind;
	private Button btnUnBind;
	private Button btnStart;
	private Button btnStop;
	
	private ServiceConnection mConnection;
	private IServiceSample connectionService;
	private BroadcastReceiverSample receiver = new BroadcastReceiverSample();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        makeToast = (Button)findViewById(R.id.btnMakeToast);
        makeToast.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				if(connectionService != null) {
					try {
						connectionService.makeToast(null);
					} catch (RemoteException e) {
						e.printStackTrace();
					}
				}
			}
		});
        
        btnBind = (Button)findViewById(R.id.btnBind);
        btnBind.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
		        mConnection = new ServiceConnection() {
					@Override
					public void onServiceConnected(ComponentName name, IBinder service) {
						Toast.makeText(MainActivity.this, "onServiceConnected MainActivity", Toast.LENGTH_SHORT).show();
						connectionService = IServiceSample.Stub.asInterface(service);
					}

					@Override
					public void onServiceDisconnected(ComponentName name) {
						Toast.makeText(MainActivity.this, "onServiceDisconnected MainActivity", Toast.LENGTH_SHORT).show();
					}
		        };

				Intent intent = new Intent(MainActivity.this, ServiceSample.class);
				bindService(intent, mConnection, Service.BIND_AUTO_CREATE);
			}
		});
        
        btnUnBind = (Button)findViewById(R.id.btnUnBind);
        btnUnBind.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				if(mConnection != null) {
					unbindService(mConnection);
					mConnection = null;
				}
			}
		});
        
        btnStart = (Button)findViewById(R.id.btnStart);
        btnStart.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				Intent intent = new Intent(MainActivity.this, ServiceSample.class);
				startService(intent);
			}
		});
        
        btnStop = (Button)findViewById(R.id.btnStop);
		btnStop.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				Intent intent = new Intent(MainActivity.this, ServiceSample.class);
				stopService(intent);
			}
		});
    }

	@Override
	protected void onStart() {
		super.onStart();
		IntentFilter filter = new IntentFilter("param");
		registerReceiver(receiver, filter);
	}

	@Override
	protected void onStop() {
		super.onStop();
		unregisterReceiver(receiver);
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
		if(mConnection != null) {
			unbindService(mConnection);
			mConnection = null;
		}
	}
}

IServiceSample.aidl

package com.choilabo.activityservicebindsample;

interface IServiceSample {
	void makeToast(String value);
}

BroadcastReceiverSample.java

package com.choilabo.activityservicebindsample;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class BroadcastReceiverSample extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		Toast.makeText(context, "BroadcastReceiverSample get broadcast", Toast.LENGTH_SHORT).show();
	}
}

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.choilabo.activityservicebindsample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="5"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <receiver
			android:name=".BroadcastReceiver"
            android:exported="false"
            />
        
        <service android:name=".ServiceSample">
            <intent-filter>
                <action android:name="IServiceSample"></action>
            </intent-filter>
        </service>
        
    </application>

</manifest>

まとめ

Servieなのに殺されるってどういうことだっていう。スマホはメモリが少ないからしょうがないっちゃしょうがないのかなぁ。startForeground()はNotificationを出すからそうそう殺されないっていう理屈のはずなのにicon消せるってのもどうかと思う。まぁ、やっと少しService周りを理解できた気がする。

DialogをカスタマイズするならDialogを継承した独自Viewを作成したほうが捗る

AlertDialogをカスタムする方法は巷に溢れてるけど、カスタマイズするのが非常にめんどくさかったのでDialogを継承して独自Viewとして作成したら捗ったのでメモ

サンプル

ソース

public class WarningDialog extends Dialog {

	private TextView txtWarning;
	private RelativeLayout rltWarning;
	private View.OnClickListener listener;
	private String warningText;
	
	public WarningDialog(Context context) {
		super(context, android.R.style.Theme_Translucent_NoTitleBar);
	}

	@Override
	protected void onCreate(Bundle bundle) {
		super.onCreate(bundle);
		setContentView(R.layout.warning);
		
		txtWarning = (TextView)findViewById(R.id.txtWarning);
		txtWarning.setText(this.warningText);
		
		rltWarning = (RelativeLayout)findViewById(R.id.rltWarning);
		rltWarning.setOnClickListener(this.listener);
	}
	
	public void setWarningText(String warningText) {
		this.warningText = warningText;
	}
	
	public void setCallbackListener(View.OnClickListener listener) {
		this.listener = listener;
	}
}

xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rltWarning"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center|center"
    android:layout_margin="20dp"
    android:clickable="true"
    android:background="@drawable/warning_bg" >

    <ImageView
        android:id="@+id/imgWarning"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:scaleType="centerCrop"
        android:src="@drawable/warning_icon" />

    <TextView
        android:id="@+id/txtWarning"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/imgWarning"
        android:layout_margin="30dp"
        android:text="Large Text"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>

使い方

	final WarningDialog dialog = new WarningDialog(getApplicationContext());
	dialog.setWarningText(getString(R.string.waring));
	dialog.setCallbackListener(new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			dialog.dismiss();
		}
	});
	dialog.show();

解説

このサンプルは独自デザインのWarningを出しているサンプル。表示されているDialogをタップすることでDialogが消える。

onCreateのsetContentViewで独自デザインのxmlを渡すことでDialogを作成する。後は入れたい値とかちょっとしか変更しないものとかをsetter作って渡してあげて、onCreate時に設定された値を表示して上がれば問題なく動く。.show()のタイミングでonCreateが走るようなので、setterでそのままViewに値を設定してあげようとするとぬるぽで落ちるからちょっと注意が必要。後、android:layout_gravity=”center|center”を設定しないと中央に来ないのでちゃんと設定してあげましょう。

感想

これならAlertDialogのBackgroundの黒が気に食わないとか、Titleの場所が気に食わないとかボタンをもっと増やしたいとか色々細かいところまで触りたおせる。標準部品で作るならAlertDialogとかAndroidが標準で提供しているものを使ったほうがいいけど、凝ったデザインとか作る場合はこっちのほうが捗るぞ。なにかこれじゃだめだろとかツッコミがあればよろしくどうぞ。

無限Swipeを実装してみる(ViewPager編)

無限にSwipeできる画面ってどうやって実装しているのかなぁと考えたのが事の発端。無限だったり循環だったりどっちでもいいんだけど、左はじまでいったら右はじに戻る奴だったり何個表示するかわからないものをSwipeで実装したりする場合を考えてみる。ぱっと思いつくのは以下の2つ。

  1. FrameLayoutで次のやつをずらしたりして頑張る
  2. ViewPagerでページ数をほぼ無限にする

ほんとうの意味で無限といえるのはFrameLayoutでtouchイベント拾ってScrollイベントをOverrideするやつなんだろうけど、数が増えるとLayoutの管理を自分で実装しないとメモリをばかみたいに食うことになるから手間がかかる。ということで、今回はViewPagerを使ってさっくりと組んでみた。

MainActivity.java

public class Main extends FragmentActivity {

    private InfinateSwipeViewPagerAdapter mPagerAdapter;
    private ViewPager mViewPager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mPagerAdapter = new InfinateSwipeViewPagerAdapter(getSupportFragmentManager());

        mViewPager = (ViewPager)findViewById(R.id.viewpager);
        mViewPager.setAdapter(mPagerAdapter);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mViewPager.setCurrentItem(InfinateSwipeViewPagerAdapter.MAX_PAGE_NUM/2);
    }
}

InfinateSwipeViewPagerAdapter.java

public class InfinateSwipeViewPagerAdapter extends FragmentPagerAdapter {

    public static final int MAX_PAGE_NUM = 1000;
    private static final int OBJECT_NUM = 3;

    public InfinateSwipeViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
    	Fragment fragment;
    	int diff = (position - (MAX_PAGE_NUM / 2)) % OBJECT_NUM;
    	int index = (0 > diff) ? (OBJECT_NUM + diff) : diff;
    	switch(index) {
    	case 0:
    		fragment = new FirstFragment();
    		break;
    	case 1:
    		fragment = new SecondFragment();
    		break;
    	case 2:
    		fragment = new ThirdFragment();
    		break;
    	}
        return  fragment;
    }

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

解説

今回の例は3つのFargmentが循環する例。ポイントは以下の3点。

  1. PagerAdapterのgetCountでそんなにSwipeできないだろーって数を与える(*1)。
  2. MainActivityのonResumeでPositionを真ん中に持ってくることで最初から左にもSwipeできるようにする。
  3. Positionは初期値をonResume()で変えてるからgetItem()で渡されるpositionを元に初期値からどれだけ移動したかを計算してInstanceを返答する。

注意点

実装する上で気をつけなければならないのは以下の3点。

  1. getItem()で返答するInstanceはPositionごとに別のものを返さなければならない。そうしないとAndroidが怒って落ちちゃう。開放をAndroidがやってくれてるからだめなんだと推測される。
  2. 毎回NewしてるからNewするコストがかかる。
  3. 循環しているとはいえ実際は別のInstanceが返答されてるから前に変更した値が循環して表示した時には初期値に戻る。

まとめ

ViewPagerでやるとAndroidが勝手に表示しているViewの左右1つを作ってくれてそれ以外は開放してくれるからメモリ管理がひたすら楽。循環じゃない場合はindexをFragmentのコンストラクタに渡してごちゃごちゃやってやればうまくいくんじゃないかと。気が向いたらFrameLayoutで実装してみるかも。まぁ、使えるところはCalenderとかそのへんだろうから、わざわざFrameLayoutとかで実装しないでViewPagerでやるほうが無難だと思う。

*1: Integer.MAX_VALUEとか本当はやりたいんだけど、それやるとViewPagerが許容してくれなくてバグるみたいだから適当な値を入れる。

LinearLayoutをタップ時にBackgroundの色を変えたい(詳細)

せっかくコメントを頂いたので詳細版を。

作るもの(左:押す前、右:押した後)

/res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/lnrButton"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="left|center"
    android:clickable="true"
    android:background="@xml/linear" >

    <ImageView
        android:id="@+id/imgBefore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:duplicateParentState="true"
        android:src="@xml/img" />

    <TextView
        android:id="@+id/txtTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:duplicateParentState="true"
        android:textColor="@xml/text"
        android:text="Sample Title" />

    <ImageView
        android:id="@+id/imgAfter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:duplicateParentState="true"
        android:src="@xml/img" />

</LinearLayout>

/res/xml/linear.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:state_pressed="true" >
        <color android:color="#555" />
    </item>
    <item android:state_pressed="false">
        <color android:color="#000" />
    </item>

</selector>

/res/xml/text.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:state_pressed="true"
        android:color="#FF0" />

    <item android:state_pressed="false"
        android:color="#FFF" />

</selector>

/res/xml/img.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:state_pressed="true"
        android:drawable="@android:drawable/btn_star_big_on" />

    <item
        android:state_pressed="false"
        android:drawable="@android:drawable/btn_star_big_off" />

</selector>

ざっくり解説

各要素でタップ時に変更したい要素についてselector要素を指定している。selector要素のstate_pressedが押されているかどうか。trueなら押されていて、falseなら押されていない。それぞれで設定したい値をitemに書き込む。後は前回書いたようにclickできるようにしてあげればきちんと動く。本当はSelectorのxmlファイルはdrawableフォルダに入れてあげたほうがいいのかもしれないけど、ごちゃごちゃしてわかりにくくなるから僕はxmlフォルダに入れる派(*1)。itemの各状態についてはここらへんが参考になるかも。

一応動作確認したので動くはずです。もしもっといいやり方があれば教えてください。

*1: 別にこだわりがあるわけじゃないからすぐに辞めるかも

LinearLayoutをタップ時にBackgroundの色を変えたい

LinearLayoutを使用してちょっと凝ったボタンみたいなのを作った時に有効。クリックした時に色を変えてちゃんとこいつがクリックされてますよーってのがわかるようにする。

selectorを作る必要があるけど、そこの説明は省略。こことか参考になる

<?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="wrap_content"
    android:clickable="true"
    android:background="@drawable/button_sample_background" >

    <ImageView
        android:id="@+id/imgIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:duplicateParentState="true"
        android:src="@drawable/button_sample_icon" />

    <TextView
        android:id="@+id/txtTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:duplicateParentState="true"
        android:textColor="@drawable/button_sample_title" />

</LinearLayout>

重要なのはLinearLayoutのclickableと子Viewに設定しいるduplicateParentState。

clickable
trueを設定するとClickを察知するようになる。
duplicateParentState
trueを設定すると親ViewのClickを優先的に動くようになる。要は親ViewをClickすると子Viewにもそれが伝わる。

この2つを設定することで、LinearLayoutのClickイベントを子Viewに伝えて文字の色を変えたり、画像を差し替えたりしてちょっと凝ったボタンを作ることができる。duplicateParentStateがわからなかったなー。

ギャラリーの共有からアプリを呼び出す方法

Androidのgalleryで画像を選択して共有を押した時にアプリを呼び出したい場合はIntent-filterをActivityに設定してあげればいい。

<intent-filter>
	<action android:name="android.intent.action.SEND" />
	<category android:name="android.intent.category.DEFAULT" />
	<data android:mimeType="image/*" />
</intent-filter>

複数選択したものを取得したい場合はactionを変更する。

<intent-filter>
	<action android:name="android.intent.action.SEND_MULTIPLE" />
	<category android:name="android.intent.category.DEFAULT" />
	<data android:mimeType="image/*" />
</intent-filter>

mimeType=image/*だと動画まで取得してしまう。写真だけ撮りたい場合はmimeType=pngとか色々設定してあげるといい。

<intent-filter>
	<action android:name="android.intent.action.SEND" />
	<category android:name="android.intent.category.DEFAULT" />
	<data android:mimeType="image/jpeg" />
	<data android:mimeType="image/jpg" />
	<data android:mimeType="image/png" />
	<data android:mimeType="image/bmp" />
	<data android:mimeType="image/bitmap" />
</intent-filter>

こんな感じで大丈夫。以下、参考になったURL。