Androidで画像(スクリーンショット)付きツイートを実装する(twitter4j)
objective-cでは、SLComposeViewController クラスを利用すれば簡単に画像付きツイートが利用可能です。ほんと、10行もソースを書けば実装できてしまします。
しかし、Androidではそうはいきません。認証やら投稿やらスクリーンショットの取得やら。色々なサイトをみていけば、実装はできますが一連の作業がまとまったサイトはなかったので、自分の備忘録としてここに残します。
AndroidでTwitterの投稿をするには、TwitterのAPIであるtwitter4jを利用します。
基本的には、下記のサイトを参考にさせて認証機能などはコーディングさせて頂きました。
Android再入門 – Twitterクライアントを作ってみよう
こちらの応用で、スクリーンショットを取得してiOSと似たようなインターフェースで
呟けるようにカスタムしました。
Twitter APIを使うアプリを登録、手順通り行いました。
2014年7月10日現在 若干、Twitterサイトの画面が変わっていましたが
特に問題なく実施できるレベルです。
登録の一番のポイントは、Callback URL です。
何か入力してあれば問題ありません。空白はダメです。
あと、Consumer key と Consumer secret が、API key と API secret に変わっています。
Twitter4Jのダウンロードと準備、基本手順通り行いました。
「Javadocやソースコードを添付する」は、エラーが発生して必要になったらやろうと思い飛ばしました。結局、ソースを追わないといけないようなエラーは発生しなかったため実施していません。

画像アップロードをするには、twitter4j-core の他に twitter4j-media-support を追加する必要があります。
因みに、2014年7月10日現在、twitter4J 安定版は4.0.2 でした。
・twitter4j-core-4.0.2.jar
・twitter4j-media-support-4.0.2.jar
OAuth認証、基本手順通りですが、少しカスタムしていきます。
★カスタム点★
①メイン画面、認証画面、ツイート画面は分けない(タイムライン画面はなし)
②ツイート画面も分けずポップアップをだす
*objective-c で言うところの presentViewController 風
1) 画面の編集

左の画面が、スクリーンショットの対象となる画面です。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.sctweettest.MainActivity$PlaceholderFragment" >
<FrameLayout
android:id="@+id/root"
android:background="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<EditText
android:id="@+id/editText1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:text="ちゅ~" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="tapTweetBtn"
android:text="ツイート" />
</LinearLayout>
<FrameLayout
android:id="@+id/tweetpop"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:alpha="0.7" >
</FrameLayout>
<LinearLayout
android:layout_width="300dip"
android:layout_height="300dip"
android:background="@drawable/tweet_corner"
android:layout_gravity="center"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dip"
android:textSize="30sp"
android:text="Twitter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<EditText
android:id="@+id/tweetText"
android:layout_width="200dip"
android:layout_height="180dip"
android:background="@drawable/non_style"
android:ems="10"
android:gravity="top" >
<requestFocus />
</EditText>
<ImageView
android:id="@+id/imageView1"
android:layout_width="80dip"
android:layout_height="80dip" />
</LinearLayout>
<TextView
android:id="@+id/tweetTextCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dip"
android:text="140" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_vertical" >
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/non_style"
android:onClick="tapPopBtn"
android:tag="0"
android:layout_weight="1"
android:textColor="#0099FF"
android:text="キャンセル" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/non_style"
android:onClick="tapPopBtn"
android:tag="1"
android:layout_weight="1"
android:textColor="#0099FF"
android:text="投稿" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</FrameLayout>
</RelativeLayout>
★IDが適当なのはご愛嬌!(各自の命名規則に沿って直して下さい)
UIのイメージとしては、メインとなるフレームの上にポップアップ(子画面)用のフレームを置きます。
2) 文字列の設定
res/values/string.xmlを編集します
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">scTweetTest</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string name="twitter_consumer_key">xxxxxxxxxxxxxxxxxxxx</string>
<string name="twitter_consumer_secret">yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</string>
<string name="twitter_callback_url">gabu://twitter</string>
</resources>
6-7行目 「Twitter APIを使うアプリを登録」で確認した値を入れます。
8行目は gabu://twitter となっています。gabuの部分は、マニフェストと同じでアンダースコアが入っていないなど一部の条件を満たせば変更しても問題ありません。今回は、判りすく情報を提供して下さっている、gabu様に敬意を表してそのまま利用させて頂きました。
3) アクティビティを作る
画面は作っていませんが、アクティビティは分けています。
基本的には、gabu様の手順通りにコーディングしました。
TwitterOAuthActivity.java
package com.example.sctweettest;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
public class TwitterOAuthActivity extends Activity {
private String mCallbackURL;
private Twitter mTwitter;
private RequestToken mRequestToken;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// コールバック用URLを取得
mCallbackURL = getString(R.string.twitter_callback_url);
mTwitter = TwitterUtils.getTwitterInstance(this);
// 非同期処理でOAuth認証を行う
startAuthorize();
}
/**
* OAuth認証(厳密には認可)を開始します。
*
* @param listener
*/
private void startAuthorize() {
AsyncTask<Void, Void, String> task = new AsyncTask<Void, Void, String>() {
// バックグラウンドで行う処理を記述する
@Override
protected String doInBackground(Void... params) {
try {
mRequestToken = mTwitter.getOAuthRequestToken(mCallbackURL);
return mRequestToken.getAuthorizationURL();
} catch (TwitterException e) {
// ツイッターの認証ページのURLが取得できなかったとき
//e.printStackTrace();
}
return null;
}
// バックグラウンド処理が完了し、UIスレッドに反映する処理を記述する
@Override
protected void onPostExecute(String url) {
if (url != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
} else {
// ツイッターの認証ページのURLが取得できなかったとき
// 失敗。。。
}
}
};
task.execute();
}
// 既存アクティビティの再表示のインテントを受け取ったときに実行
@Override
public void onNewIntent(Intent intent) {
if (intent == null
|| intent.getData() == null
|| !intent.getData().toString().startsWith(mCallbackURL)) {
return;
}
String verifier = intent.getData().getQueryParameter("oauth_verifier");
// 非同期処理で認証情報をアプリに反映
AsyncTask<String, Void, AccessToken> task = new AsyncTask<String, Void, AccessToken>() {
// バックグラウンドで行う処理を記述する
@Override
protected AccessToken doInBackground(String... params) {
try {
// キャンセルボタン対応
if(params[0] == null){
return null;
}
return mTwitter.getOAuthAccessToken(mRequestToken, params[0]);
} catch (TwitterException e) {
//e.printStackTrace();
}
return null;
}
// バックグラウンド処理が完了し、UIスレッドに反映する処理を記述する
@Override
protected void onPostExecute(AccessToken accessToken) {
exitOAuth(accessToken);
}
};
task.execute(verifier);
}
// 処理
private void exitOAuth(AccessToken accessToken) {
if (accessToken != null) {
// 認証成功時は認証情報を保管
TwitterUtils.storeAccessToken(this, accessToken);
}
// アプリに戻る
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}
27-28行目 もともとは、ボタンをタップしたときの処理ですが、今回は画面内ですのでonCreate時に非同期処理を実行します。
82-85行目 私の移植の仕方が、まずかったのかツイッターの連携画面で、キャンセルした場合、ページの見つからないエラーが発生してしまうため、追加しました。
96行目 こちらで認証の成功を判定していましたが、今回は同一ページへ戻したいので、認証が失敗でも成功でも同じメソッドを呼びます。成功した場合のみ、106行目の処理を実行し、認証情報を保管します。
4) マニフェストの修正
アクティビティを作ったら、合わせてマニフェストを修正します。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.sctweettest"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.sctweettest.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".TwitterOAuthActivity"
android:launchMode="singleTask" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="twitter"
android:scheme="gabu" />
</intent-filter>
</activity>
</application>
</manifest>
11-13行目 権限を取得しています。
11行目 インターネットへのアクセス権限
12行目 ネットワーク通信状態を取得
13行目 ストレージ(SDカード、本体など)へのアクセス権限
↑これが、アプリをインストールするときにでてくる、許可の部分になります。
30-42行目 追加したアクティビティ部分です。
ポイント
32行目 android:launchMode=”singleTask” これがないと、アプリに戻れないらしいです。
39-41行目 ブラウザでの連携後戻ってこられないのはこのあたりの設定にミスがあります。
41行目 上でも書いていますが、一定の条件を見たいしていれば変更しても問題ありません。
gabu様のサイトの、質問部分を見ると載っていますので、目を通しておくとよいと思います。
5) メインアクティビティの修正
MainActivity.java
package com.example.sctweettest;
import java.io.File;
import java.io.FileOutputStream;
import twitter4j.StatusUpdate;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import android.support.v7.app.ActionBarActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Bitmap.CompressFormat;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends ActionBarActivity {
FrameLayout tweetpop;
EditText tweetText;
ImageView imageView1;
TextView tweetTextCount;
Button button3;
String sUrl;
// Twitter連携用
Twitter mTwitter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tweetpop = (FrameLayout) this.findViewById(R.id.tweetpop);
tweetText = (EditText) this.findViewById(R.id.tweetText);
imageView1 = (ImageView) this.findViewById(R.id.imageView1);
tweetTextCount = (TextView) this.findViewById(R.id.tweetTextCount);
button3 = (Button) this.findViewById(R.id.button3);
tweetpop.setVisibility(View.GONE);
// コールバック用文字列取得
mTwitter = TwitterUtils.getTwitterInstance(this);
sUrl = " http://003sh.ou-net.com/";
// ツイート文字列の編集リスナー登録
tweetText.addTextChangedListener(new TextWatcher(){
// 編集前の処理
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
}
// 値が変わったときの処理
public void onTextChanged(CharSequence s, int start, int before, int count) {
final int textColor;
int length = 140 - s.length() - sUrl.length();
if(length < 0){
textColor = Color.RED;
button3.setEnabled(false);
button3.setTextColor(Color.GRAY);
}else{
textColor = Color.GRAY;
button3.setEnabled(true);
button3.setTextColor(Color.rgb(0, 153, 255));
}
tweetTextCount.setTextColor(textColor);
tweetTextCount.setText(String.valueOf(length));
}
// 編集後の処理
public void afterTextChanged(Editable s) {
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
// ツイートボタンタップ処理
public void tapTweetBtn(View view) {
// OAuth認証
if (!TwitterUtils.hasAccessToken(this)) {
// 未認証時
Intent intent = new Intent(this, TwitterOAuthActivity.class);
startActivity(intent);
finish();
}else{
// 認証済時
// クリーンショット取得
View root = (View) findViewById(R.id.root);
root.setDrawingCacheEnabled(true);
Bitmap bitmap1 = Bitmap.createBitmap(root.getDrawingCache());
// スクリーンショットの保存
try {
// 保存先を決定
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/scTweetTest/");
if (!file.exists()) {
file.mkdir();
}
// ファイル名を指定
String AttachName = file.getAbsolutePath() + "/image.jpg";
// ファイルを保存
FileOutputStream out = new FileOutputStream(AttachName);
bitmap1.compress(CompressFormat.JPEG, 100, out);
out.flush();
out.close();
} catch (Exception e) {
showToast("スクリーンショットの保存失敗");
}
// スクリーンショットのサムネイルを表示
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/scTweetTest/image.jpg");
if(file.exists()){
Bitmap myBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
imageView1.setImageBitmap(myBitmap);
}
// ツイート可能文字数を表示
int length = 140 - tweetText.length() - sUrl.length();
tweetTextCount.setTextColor(Color.GRAY);
tweetTextCount.setText(String.valueOf(length));
// ツイート画面の表示
tweetpop.setVisibility(View.VISIBLE);
// ツイート文字にフォーカスをセット
tweetText.requestFocus();
// カーソルは最後の文字に
tweetText.setSelection(tweetText.length());
InputMethodManager inputMethodManager = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(tweetText, 0);
}
tweetpop.setVisibility(View.VISIBLE);
}
// ツイート子画面内ボタンタップ処理
public void tapPopBtn(View view) {
switch(Integer.parseInt(String.valueOf(view.getTag()))){
case 0:
// 「キャンセル」ボタン
tweetpop.setVisibility(View.GONE);
break;
case 1:
// 「投稿」ボタン
tweet();
break;
default:
break;
}
}
/** 非同期でのツイート処理 **/
private void tweet() {
AsyncTask<String, Void, Boolean> task = new AsyncTask<String, Void, Boolean>() {
@Override
protected Boolean doInBackground(String... params) {
try {
// 本文セット
final StatusUpdate status = new StatusUpdate(params[0] + sUrl);
// 画像セット
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/scTweetTest/image.jpg");
if(file.exists()){
status.media(file);
}else{
status.media(null);
}
// ツイート
mTwitter.updateStatus(status);
return true;
} catch (TwitterException e) {
e.printStackTrace();
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
showToast("ツイート完了");
tweetpop.setVisibility(View.GONE);
} else {
showToast("ツイート失敗");
}
}
};
task.execute(tweetText.getText().toString());
}
// トースト表示処理
public void showToast(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
}
34-42行目 変数を宣言します。
49-53行目 レイアウトを編集できるように取得します。
55行目 ツイート用の子画面は非表示にしておきます。
58行目 コールバック用文字列取得
60行目 利用ユーザが削除できない文字列を後ろに追加しています。不要な場合は空にしておくか削除して下さい。
★しかし、せっかくツイートしてもらうなら、自分のアプリ宣伝しないとですよね!
63-87行目 ツイートする文字を入力するEditTextのフィールドが編集された時の処理を記載しています。
サンプルの場合は、あと何文字入力できるかの表示。マイナスになった場合はカウントを赤字にして投稿ボタンを非活性にしています。
105-159行目 ここが1つ目の重要ポイントです。
107行目 ここで、ツイッターがアプリに連携されているかを確認します。
未認証時は、109行目~の TwitterOAuthActivity に移します。
認証済時は、121行目~の処理をします。
114-142行目 スクリーンショットを保存し、サムネイルとして表示までさせています。
想定としては、スクリーンショットを張り付けてツイートしたくなるほど、その画面を出すのが大変なアプリなのでスクリーンショットは、万が一に備えてストレージに保存しています。
144-156行目 初期状態の文字列を計算し、ツイート画面を表示します。
このとき、見た目重視でEditTextのフレームを消しているため、入力できるエリアかどうか判りにくいため、カーソルを立ててキーボードを表示しておきます。(iPhoneがそういう動きしますので合わせています)
158行目 実際に、非同期処理を実行します。
161-175行目 ツイート子画面の「キャンセル」・「投稿」ボタンのタップ処理です。
166行目 キャンセル時は、ツイート子画面を非表示にします。
170行目 実際のツイート処理を非同期で実施します。
177-214行目 非同期でのツイート処理です。
184行目 本文をセットします。
186-192行目 画像をセットします。
params[0] には、213行目の引数 task.execute(tweetText.getText().toString()); が入っています。
195行目 実際にツイートをします。
204-210行目 ツイート処理に対する結果を受け取ります。
ツイート処理で、エクセプションを拾った場合、引数がfalseになります。
ツイート失敗した場合は、トーストを出しもとの画面に戻します。
ツイート成功した場合は、トーストを出した後、ツイート子画面を非表示にし一連の流れは終了します。
213行目 実際にツイートの非同期処理を実行しています。
216-219行目 トースト表示を、メソッド化しています。
ここまでのソースをそのままコピペすると、レイアウトでエラーが上がります。
<LinearLayout
android:layout_width="300dip"
android:layout_height="300dip"
android:background="@drawable/tweet_corner"
android:layout_gravity="center"
android:orientation="vertical" >
–snip–
<EditText
android:id="@+id/tweetText"
android:layout_width="200dip"
android:layout_height="180dip"
android:background="@drawable/non_style"
android:ems="10"
android:gravity="top" >
–snip–
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/non_style"
android:onClick="tapPopBtn"
android:tag="0"
android:layout_weight="1"
android:textColor="#0099FF"
android:text="キャンセル" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/non_style"
android:onClick="tapPopBtn"
android:tag="1"
android:layout_weight="1"
android:textColor="#0099FF"
android:text="投稿" />
58、79、109,120行目は見た目を変えています。
res配下に、drawable というディレクトリを作成します。
私の中に、58行目で使われている、tweet_corner.xml と他で使われている、non_style.xml を作成します。
res/drawable/tweet_corner.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:angle="270"
android:startColor="#ffffff"
android:endColor="#ffffff"
android:type="linear" />
<corners android:radius="10dp" />
<stroke android:width="1px" android:color="#cccccc" />
<padding android:left="10px" android:top="10px"/>
</shape>
res/drawable/non_style.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:angle="270"
android:startColor="#ffffff"
android:endColor="#ffffff"
android:type="linear" />
</selector>
余計なものも入っていますが、気にしないで下さい。
最後に、画面のフローを説明します。
【起動直後の画面(メインアクティビティ)】

【ツイッターと連携されていなかった場合】

ブラウザで認証画面が開かれます。ユーザ名・パスワードを入れて「連携アプリを認証」ボタンをタップすると最初の画面に戻ります。
再び、ツイートボタンをタップすると、下の連携されていた場合の処理に移ります。
【ツイッターと連携されていた場合】

連携されていた場合は、認証画面は表示されず、ツイート子画面が表示されます。
認証画面から戻ってきたときに、画面が初期状態に戻るため実際のアプリでは、何らかの形で状態を維持し戻してやる必要があります。
それは、また別の話なのでここでは割愛致します。
以上、頑張ってる途ちゅ~(笑
余談ですが、このサイト名の chu
エビ中から使わせてもらっているわけではありません。
当サイトの開設が、2011年1月ですのでエビ中はすでに結成していました。
しかし、2011年1月の段階では知名度も低く当然、私も知りませんでした。ので、偶然です。
エビ中ちゃんが、目につきだしたのは、2011年4月のももクロのライブに出演してからだと思います。
さらに、xxチュウが特に意識され出したのは、頑張ってる途中のリリース(2013年1月)後だと思われます。
何が言いたいかというと、chu!はエビ中ちゃんとは関係ないってことです(笑
今は意識してますが(www
