Hello World!編→http://qiita.com/YSRKEN/items/945d134c540367ba8734
スクショソフト編→ここ
概要
前回ではとりあえずHello World!させてみましたが、当然このままでは終わりません。ボタンを設置して、本格的なアプリケーションを作成してみます。今回作成したいアプリの仕様は次の通り。
- 「スタート!」ボタンを押すと、タイマーが起動する
- アプリが起動している間、1秒間隔で画面のスクショが保存される
あの日押したボタンの結果を僕達はまだ知らない。
以前Swingを利用してJavaアプリを作成した記憶から、今回も同じような手順なんだろうなと考えていました。ただ、「どうせRADだし簡単だろwww」と、ボタンをウィンドウに配置してとりあえずダブルクリックしても、Visual Studioみたいに自動でClickイベントが生成されてそこまで表示が遷移しないんですね……。
じゃあどうやってイベントを記述するかですが、手順としては次のようになります。
(参考:2.1 ボタンのタップイベントを取得する)
- ボタンのプロパティから、idを適当な名前(StartButtonなど)、textを適当な名前("スタート!"など)に変更する。前者はオブジェクトのID、後者はオブジェクトのテキスト(ボタンで言えば表示される名前)となる
- 上記の変更点は、デフォルトではactivity_main.xmlに記録されている。気になる人はプロジェクト全体から検索を掛けてみよう
- OnClickListenerをimplementsすることでonClickメソッドを引っ張ってくる。OnClickListenerを利用するには他にも多種多様な手法があるが、一番手っ取り早い方法はこれなはず。以下にサンプルを示すが、//addが追記場所であることに注意したい(宣言を端折ってたりした場合は形名などが赤字で表示されるので、Alt+Enterで自動補完することが出来る。なかなかに優秀ですな)
package com.example.(略).capture;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button; //add
import android.view.View; //add
import android.widget.Toast; //add
import android.view.View.OnClickListener; //add
//add(implements OnClickListener)
public class MainActivity extends AppCompatActivity implements OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.StartButton); //add
button.setOnClickListener(this); //add
}
public void onClick(View v){ //add(メソッド全体)
Toast.makeText(this, "ボタンが押されました", Toast.LENGTH_LONG).show();
}
}
- ここでまず注目したいのがonClickメソッド。OnClickListenerを利用する際は必ず実装しなければならない。setOnClickListenerで登録して~という部分は、普通のJavaプログラミングでもおなじみなので迷うことはないはず。
- また、Toastは下記画像のようにポップアップメッセージを出すための機能。Toast.LENGTH_SHORTで短時間、Toast.LENGTH_LONGで長時間表示します。スマホ触ってたら時々見ますよね?
そのタイマーイベントが、僕達の永遠になる
Javaにおけるタイマーイベントは幾つか種類がありますが、今回はJavaで艦これ用高性能スクショソフトを作成した時とは違い、java.util.Timerを利用することにします。だって今回は別にSwing使ってないしね。
こちらも使用方法はとても簡単で、java.util.TimerTaskを継承したクラスのrunメソッドで定期実行したいタスクを記述した後、Timerのインスタンスを生成してTimer#scheduleでスケジューリングすれば実行できます。やったぜ。
……ただ、UIスレッドじゃないとGUI部品にアタッチできないので、次のようなコードだとボタンを押した際に「java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()」が出て停止してしまいます。
import java.util.Timer;
import java.util.TimerTask;
class showMessageTask extends TimerTask {
public void run() {
Toast.makeText(MainActivity.this, "メッセージ表示", Toast.LENGTH_SHORT).show();
}
}
public void onClick(View v) {
Timer timer = new Timer();
timer.schedule(new showMessageTask(), 100, 5000);
}
そこで、Handlerで更新処理をUIスレッドにpostすることで、上記エラーを防止しています。
import android.os.Handler;
import java.util.Timer;
import java.util.TimerTask;
class showMessageTask extends TimerTask {
private final Handler handler = new Handler();
public void run() {
handler.post(new Runnable() {
public void run() {
Toast.makeText(MainActivity.this, "メッセージ表示", Toast.LENGTH_SHORT).show();
}
});
}
}
public void onClick(View v) {
Timer timer = new Timer();
timer.schedule(new showMessageTask(), 100, 5000);
}
心がスクショしたがっているんだ。
後はTimer内のrun()にスクショ機能を組み込めばいいだけです。具体的には、Viewオブジェクトに対してView#getDrawingCacheでBitmap型のキャッシュを取得し、Bitmap#createBitmapで複製すればいいでしょう。ただ、Androidは画像保存にjavax.imageio.ImageIOが使えませんので、FileOutputStreamとBitmap#compressメソッドで保存することになります。また、「<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>」という宣言をAndroidManifest.xmlに入れておかないと権限不足で画像を保存できませんのでご注意下さい。以上の手順の詳細については次の記事をご覧ください。
AndroidでViewのキャプチャを撮る ‹ ワンダープラネット株式会社(Wonderplanet Inc.)
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.os.Environment;
import android.view.View;
import java.io.File;
import java.io.FileOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public void run() {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
final Date date = new Date(System.currentTimeMillis());
final File file = new File(Environment.getExternalStorageDirectory() + "/hoge/" + df.format(date) + ".png");
file.getParentFile().mkdir();
saveCapture(findViewById(android.R.id.content),file);
Toast.makeText(MainActivity.this, df.format(date), Toast.LENGTH_SHORT).show();
}
// スクリーンショットを取得して保存する
public void saveCapture(View view, File file) {
Bitmap capture = getViewCapture(view);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file, false);
// 画像のフォーマットと画質と出力先を指定して保存
capture.compress(CompressFormat.PNG, 100, fos);
fos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos == null) return;
try {
fos.close();
} catch (Exception ie) {
fos = null;
}
}
}
// スクリーンショットを取得する
public Bitmap getViewCapture(View view) {
view.setDrawingCacheEnabled(true);
Bitmap cache = view.getDrawingCache();
if(cache == null) return null;
Bitmap screen_shot = Bitmap.createBitmap(cache);
view.setDrawingCacheEnabled(false);
return screen_shot;
}
……んー? これだと、自分のアプリでのスクショは撮れるけど、それ以外は撮れないぞ?
というのも、Androidの場合、セキュリティの都合上、Android 5.0以前では他アプリのスクショも撮れるAPIを提供していませんでした。逆に言えば5.0以降なら権限で許可すれば使えるのですが、手持ちのスマホが5.0以前なのでやる気が出ませんでした……。
以下おまけ
とあるスマホの遠隔動作確認《無線LANデバッグ》
※《》記号は青空文庫形式のルビ
スマホの実機で動作確認する際は、普通PCにスマホを有線で接続します。だけど面倒臭いなーと思っていたところ、なんと無線LAN接続でデバッグが利用できると判明。具体的には、スマホのIPアドレスをメモった後、そこに(任意の)指定したポートでアクセスするようにadbを設定するだけ!
ただ、PCを再起動とかするとリセットされるのでその辺はご注意下さい。
# PCとスマホを接続した後に下のコマンド(ポート番号は任意)を入れる
> adb tcpip 5555
# 192.168.0.17 はメモったIPアドレス。番号は先ほどと同じ
> adb connect 192.168.0.17:5555
# 接続解除するためのコマンド(PCを再起動すると自動で切られるので注意)
> adb disconnect 192.168.0.17
上記で使用したスクショアプリの全ソース
package com.example.(略).capture;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.os.Environment;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class MainActivity extends AppCompatActivity implements OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.StartButton);
button.setOnClickListener(this);
}
class showMessageTask extends TimerTask {
private final Handler handler = new Handler();
public void run() {
handler.post(new Runnable() {
public void run() {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
final Date date = new Date(System.currentTimeMillis());
final File file = new File(Environment.getExternalStorageDirectory() + "/hoge/" + df.format(date) + ".png");
file.getParentFile().mkdir();
saveCapture(findViewById(android.R.id.content),file);
Toast.makeText(MainActivity.this, df.format(date), Toast.LENGTH_SHORT).show();
}
});
}
}
public void onClick(View v) {
Timer timer = new Timer();
timer.schedule(new showMessageTask(), 100, 5000);
}
// スクリーンショットを取得して保存する
public void saveCapture(View view, File file) {
Bitmap capture = getViewCapture(view);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file, false);
// 画像のフォーマットと画質と出力先を指定して保存
capture.compress(CompressFormat.PNG, 100, fos);
fos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos == null) return;
try {
fos.close();
} catch (Exception ie) {
fos = null;
}
}
}
// スクリーンショットを取得する
public Bitmap getViewCapture(View view) {
view.setDrawingCacheEnabled(true);
Bitmap cache = view.getDrawingCache();
if(cache == null) return null;
Bitmap screen_shot = Bitmap.createBitmap(cache);
view.setDrawingCacheEnabled(false);
return screen_shot;
}
}