Inkscapeのグラデーション

グラデーションの編集方法が直感的にわかりづらかったので、メモしておきます。

グラデーションの種類

  • Linear gradient(線状グラデーション)
  • Radical gradient(放射状グラデーション)

色フェーズ

英語版では「stop」として扱われている概念。日本語版では「色フェーズ」と訳されているもよう。
Gradient editorのドロップダウンで選択できる。初期状態では2つ。「stop3757」とかなっている。

Inkscapeでは、グラデーションの起点となる色を「色フェーズ」と呼びます。 グラデーションを適用すると、 初期状態では単一色のフィルの色と、 その色の完全透明色の2つが色フェーズとなります。 つまり、フィルの色から無色に遷移するグラデーションとなります。
Inkscape@JP - リファレンス (オブジェクトの編集 >> グラデーション)

オフセット

オフセット0.00が起点、オフセット1.00が終点のような感じ。

初期状態ではオフセット0に不透明度255、 オフセット1に不透明度0で同じ色が設定されます。 線形グラデーションでは左端がオフセット0、 右端がオフセット1となっています。 円形グラデーションでは中心がオフセット0、円周がオフセット1です。
Inkscape@JP - リファレンス (オブジェクトの編集 >> グラデーション)

iPhoneのロック解除画面のようなボタンをAndroidで作成する

iPhoneのロック解除画面のようなボタンの作成を、Androidで試みました。

画像を用意する

Androidのデフォルトテーマのボタンは上下左右に隙間があるので、隙間が無いボタンの画像を用意しました。なお、これらの画像はpixelmatorの体験版で自作しました。

通常時

汎用性を持たせるため、全体を透過にしました。

押された時

こちらは透過にはなっていません。デフォルトにあわせてオレンジにしました。

StateListで状態を定義

作成した画像を、下記のようにボタンの状態に割り当てました。

res/drawable/btn_custom.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="@drawable/btn_custom_normal_pressed" />
	<item android:state_enabled="true" android:state_window_focused="true"
		android:drawable="@drawable/btn_custom_normal" />
</selector>

ボタンのスタイル定義

属性をまとめて指定できるよう、スタイルを"btn_custom"として定義しました。フォントが立体的に見えるように、shadow関連をちょっといじりました。

res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<style name="btn_custom">
		<item name="android:background">@drawable/btn_custom</item>
		<item name="android:textColor">#FFF</item>
		<item name="android:textSize">20dp</item>
		<item name="android:shadowColor">#000</item>	
		<item name="android:shadowDx">-1.0</item>	
		<item name="android:shadowDy">-1.0</item>	
		<item name="android:shadowRadius">1.0</item>	
	</style>
</resources>

レイアウトの定義

LinearLayoutで9x9のボタンを並べ、それぞれにスタイルを適用しました。

res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent" android:background="#555"
	android:layout_height="fill_parent">
	<TextView android:layout_width="fill_parent"
		android:layout_height="wrap_content" android:text="@string/hello" />
	<LinearLayout android:layout_height="wrap_content"
		android:id="@+id/linearLayout1" android:layout_width="match_parent">
		<Button android:text="1" android:id="@+id/button1" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="2" android:id="@+id/button2" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="3" android:id="@+id/button3" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
	<LinearLayout android:layout_height="wrap_content"
		android:id="@+id/linearLayout2" android:layout_width="match_parent">
		<Button android:text="4" android:id="@+id/button4" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="5" android:id="@+id/button5" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="6" android:id="@+id/button6" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
	<LinearLayout android:layout_height="wrap_content"
		android:id="@+id/linearLayout3" android:layout_width="match_parent">
		<Button android:text="7" android:id="@+id/button7" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="8" android:id="@+id/button8" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
		<Button android:text="9" android:id="@+id/button9" style="@style/btn_custom"
			android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
	</LinearLayout>
</LinearLayout>

備考

  • ボタン9個にstyleを指定するのは面倒なので、実際にはthemeを使うべきだと思います。今回は、themeで指定しようとしたらなぜかpressedの画像がうまく適用されなかったので、styleでの指定にしました。もしかしたら、selectorの記述にどこかまずいところがあるのかもしれません。
  • 画像のグラデーションが、伸縮で汚く見えてしまっています。9patchでの指定の仕方によっては改善できるのかも。あるいはgradientを使う?

あわせて読みたい

Android Layout Cookbook 第2章カスタマイズ 1 独自の画像でボタンを作る(p.054)

btn_default.xmlの中身を調べる

ボタンのデザインをする場合のために、デフォルトのボタンがどのように実装されているか調べます。
デフォルトのボタンは、Widget.Buttonというstyleが適用されている。(たぶん)

Widget.Buttonスタイルの定義

android:backgroundにて、drawableのbtn_defaultが指定されている。
android.git.kernel.org Git - platform/frameworks/base.git/blob - core/res/res/values/styles.xml

 173     <style name="Widget.Button">
 174         <item name="android:background">@android:drawable/btn_default</item>
 175         <item name="android:focusable">true</item>
 176         <item name="android:clickable">true</item>
 177         <item name="android:textAppearance">?android:attr/textAppearanceSmallInverse</item>
 178         <item name="android:textColor">@android:color/primary_text_light</item>
 179         <item name="android:gravity">center_vertical|center_horizontal</item>
 180     </style>

btn_default.xmlの中身

selectorで、Buttonの状態に応じた画像を指定している。
android.git.kernel.org Git - platform/frameworks/base.git/blob - core/res/res/drawable/btn_default.xml

  17 <selector xmlns:android="http://schemas.android.com/apk/res/android">
  18     <item android:state_window_focused="false" android:state_enabled="true"
  19         android:drawable="@drawable/btn_default_normal" />
  20     <item android:state_window_focused="false" android:state_enabled="false"
  21         android:drawable="@drawable/btn_default_normal_disable" />
  22     <item android:state_pressed="true" 
  23         android:drawable="@drawable/btn_default_pressed" />
  24     <item android:state_focused="true" android:state_enabled="true"
  25         android:drawable="@drawable/btn_default_selected" />
  26     <item android:state_enabled="true"
  27         android:drawable="@drawable/btn_default_normal" />
  28     <item android:state_focused="true"
  29         android:drawable="@drawable/btn_default_normal_disable_focused" />
  30     <item
  31          android:drawable="@drawable/btn_default_normal_disable" />
  32 </selector>

色々組み合わせがあってややこしいので、表にしてみます。画像はAndroidのソースからの引用です。

line state_
window_
focused
state_
pressed
state_
enabled
state_
focused
drawable 9.png 備考
18-19 false なし true なし btn_default_normal 通常時
20-21 false なし false なし btn_default_normal_disable disable時
22-23 なし true なし なし btn_default_pressed 押された時
24-25 なし なし true true btn_default_selected フォーカスがある時
26-27 なし なし true なし btn_default_normal 通常時(18-19との違い不明)
28-29 true なし なし なし btn_default_normal_disable_focused disableでフォーカスあり
30-31 なし なし なし なし btn_default_normal_disable disable時(20-21との違い不明)

ボタンの画像をちゃんと用意するなら、下記の5パターンの画像が必要ということみたいです。
・通常時
・disable時
・押されたとき
・フォーカスがあるとき
・disableでフォーカスがあるとき

LayoutPanelについて

Developer's Guide - Layout Using Panels - Google Web Toolkit - Google Code

GWT2.0からLayoutPanelというものが導入されて、そっちを使うといいみたいです。

LayoutPanelのサンプル

LayoutPanelにsetWidgetのバリエーションでWidgetを追加するサンプルです。ドキュメントのサンプルコードは端折られていて、そのままではコンパイルが通らないので、通る形にしています。

public class MainEntryPoint implements EntryPoint {

	@Override
	public void onModuleLoad() {

		// Widget sampleWidget = new SampleWidget("hogege");
		Widget child0 = new Button("child0");
		Widget child1 = new Button("child1");
		Widget child2 = new Button("child2");

		LayoutPanel p = new LayoutPanel();
		p.add(child0);
		p.add(child1);
		p.add(child2);

		// Left panel
		p.setWidgetLeftWidth(child0, 0, Unit.PCT, 50, Unit.PCT); 
		
		// Right panel
		p.setWidgetRightWidth(child1, 0, Unit.PCT, 50, Unit.PCT); 
		
		// Center panel
		p.setWidgetLeftRight(child2, 5, Unit.EM, 5, Unit.EM); 
		p.setWidgetTopBottom(child2, 5, Unit.EM, 5, Unit.EM);

		RootLayoutPanel rootLayoutPanel = RootLayoutPanel.get();
		rootLayoutPanel.add(p);
	}

}

child0を左から50%の横幅で指定、child1を右から50%の横幅で指定、
child2を上下左右で5EMの一に配置するコード。

同一プロセス内のServiceへのバインド

Serviceへのバインドには必ずAIDLが必要なのかと思ってたんですが、同一プロセス内であればAIDLは不要みたいです。勘違いしてました。
下記の記事を読んで知りました。

 なお、このケースではサービスとクライアントが同じプロセスなので、AIDLを使わなくても問題ありません。
常駐アプリが作成できるAndroidの“サービス”とは (2/3) - @IT

Bound Services | Android Developers
Android DevelopersのBound Servicesでローカルなサービス(リモートでない)について記載されています。

アクティビティから、直接サービスのメソッドを呼び出せるか試します。

バインドされるサービス

onBindで、Binderの実装クラスを返します。接続した側が、Serviceの参照を触れるよう、Binderの実装クラスにServiceの参照を返すメソッドを持たせます。

public class SampleService extends Service {
	
	public class SampleServiceBinder extends Binder {
		SampleService getService() {
			return SampleService.this;
		}
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		Log.v(TAG, "service onBind");
		return new SampleServiceBinder();
	}

	public String hoge() {
		return "hoge";
	}

サービスにバインドするアクティビティ

bindServiceによりServiceConnection#onServiceConnected()が呼ばれ、パラメータにService#onBindの戻り値が渡されるので、キャストしてgetServiceからserviceのインスタンスを取得します。

public class SampleAppActivity extends Activity {

	private static final String TAG = "hoge";
	protected SampleService mService;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		Log.v(TAG, "activity onCreate");
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Button btn4 = (Button) findViewById(R.id.button4);
		btn4.setOnClickListener(new OnClickListener() {

			public void onClick(View v) {
				Log.v(TAG, "btn4 onClick");
				Intent intent = new Intent();
				intent.setClass(getApplicationContext(), SampleService.class);
				bindService(intent, conn , Service.BIND_AUTO_CREATE);
			}
		});

		Button btn5 = (Button) findViewById(R.id.button5);
		btn5.setOnClickListener(new OnClickListener() {

			public void onClick(View v) {
				Log.v(TAG, "btn5 onClick");
				if(mService != null) {
					String hoge = mService.hoge();
					text1.setText(hoge);
				}
			}
		});
	}
	
	private ServiceConnection conn = new ServiceConnection() {
		public void onServiceConnected(ComponentName name, IBinder binder) {
			Log.v(TAG, "onServiceConnected");
			mService = ((SampleServiceBinder) binder).getService();
		}
		
		public void onServiceDisconnected(ComponentName name) {
			Log.v(TAG, "onServiceDisConnected");
			mService = null;
			
		}
		
	};

btn5をクリックするとSampleService#hoge()を呼び出せたことが確認できました。

UIBinderを使ってみる

GWTにはUIBinderというものがあり、これを使うとコンポーネントのコードとレイアウトを分離できるようです。そういうの好きなので試してみます。
Declarative Layout with UiBinder - Google Web Toolkit - Google Code

プラグインから作成可能

右クリックしてNew>UIBinderで作成できます。
GWT WidgetsとHTMLいずれかの選択ができました。

GWT Widetsの場合

com.google.gwt.user.client.uiをインポートして、g:なんたらとしていろいろタグを呼び出しています。

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
	xmlns:g="urn:import:com.google.gwt.user.client.ui">
	<ui:style>
		.important {
			font-weight: bold;
		}
	</ui:style>
	<g:HTMLPanel>
		Hello,
		<g:Button styleName="{style.important}" ui:field="button" />
	</g:HTMLPanel>
</ui:UiBinder> 
HTMLの場合

こちらは、見慣れたhtmlタグです。デザイナーと分業するようなときはこっちが良いのかも。

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder">
	<ui:style>
		.important {
			font-weight: bold;
		}
	</ui:style>
	<div>
		Hello,
		<span class="{style.important}" ui:field="nameSpan" />
	</div>
</ui:UiBinder>

いずれの場合も、ui:fieldという属性を定義すると、それをjavaファイルから@UIFieldというアノテーションで参照できるようです。

ui.xmlと.javaのひも付け

ui.xmljavaは、同名なら勝手にひもづけてくれるようです。

 また、ui.xmlJavaのファイルの紐付けは同じパッケージ内の同じファイル名であれば、必要ないようです。もしファイル名が異なる場合は、Javaのソースに定義されているinterface上にアノテーションで @UiTemplate("ファイル名.ui.xml") と定義すれば紐付けされます。
Song of Cloud: Eclipse PluginではじめてのGWT UiBinder

GWTでRemoteServiceによる非同期HTTP通信を組み立てる

generate sample codeを使わずGWTプロジェクトを組み立ててみる - 理系のためのTIPS集
前回に続いて非同期HTTP通信の実装を試します。

ボタンをクリックしたら/mymodule/hogeにリクエストを投げて、結果をalertさせてみます。

GWTにおけるサーバとの通信方法

RPC(Remote Procedure Calls)という仕組みを利用します。
Making Remote Procedure Calls - Google Web Toolkit - Google Code

参考:通常のサーブレットの場合

RPCを試す前に、通常のサーブレットの場合を確認してみます。
com.naokichick.samplegwt.server.MyServletを作成し、/mymodule/hogeマッピング

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>com.naokichick.samplegwt.server.MyServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>MyServlet</servlet-name>
    <url-pattern>/mymodule/hoge</url-pattern>
  </servlet-mapping>
	<!-- TODO: Optionally add a <welcome-file-list> tag to display a welcome file. -->
</web-app>
package com.naokichick.samplegwt.server;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
public class MyServlet extends HttpServlet {
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/plain");
		resp.getWriter().println("hoge");
		resp.flushBuffer();
	}
}

jQueryajax通信。

$.ajax({
    url: "./mymodule/hoge",
    type: "get",
    success: function(data){
        alert(data);
    },
    error: function(){
        alert("error");
    }
});

上記を踏まえ、RPCを利用したajax通信を書いてみます。

RemoteServiceServletは普通のリクエストを受け付けない

試しにRemoteServiceServletを通常のServletのようにマッピングして、リクエストを試みます。

[WARN] Exception while dispatching incoming RPC call
javax.servlet.ServletException: Content-Type was 'application/x-www-form-urlencoded'. Expected 'text/x-gwt-rpc'.

フォームからリクエストを投げてみると、上記エラーとなりました。

リクエストのURIは何によって決まるか?

リクエストのURIは、EntryPointのModuleNameとRemoteServiceの@RemoteServieRelativePathで決まるようです。

<module rename-to="mymodule">
	<inherits name="com.google.gwt.user.User" />
	<source path="client" />
	<entry-point class="com.naokichick.samplegwt.client.MyEntryPoint" />
</module>

上記の定義で、モジュールの名前をmymoduleとリネームし、MyEntryPointをエントリポイントに指定しています。リネームしているのはデフォルトが完全修飾名だからです。

エントリポイント内からMyRemoteServiceのメソッドを呼び出します。MyRemoteServiceのURIは@RemoteServiceRelativePathの定義により、hogeになります。

@RemoteServiceRelativePath("hoge")
public interface MyRemoteService extends RemoteService {
	public String getResult(String param);
}

これで、MyEntryPointからMyRemoteServiceに対するメソッド呼び出しは"mymodule/hoge"になりました。あとは、このURIマッピングを処理するRemoteServiceServletに実装します。

@SuppressWarnings("serial")
public class MyRemoteServiceImpl extends RemoteServiceServlet implements MyRemoteService {

	@Override
	public String getResult(String param) {
		return "param is " + param;
	}
}

mymoduleから/hogeに対する非同期リクエストとハンドリングの実装。パラメータは"fuga"。

			public void onClick(ClickEvent event) {
				Window.alert(GWT.getModuleName());
				MyRemoteServiceAsync service = GWT.create(MyRemoteService.class);
				service.getResult("fuga", new AsyncCallback<String>() {
					
					@Override
					public void onSuccess(String result) {
						Window.alert(result);
					}
					
					@Override
					public void onFailure(Throwable caught) {
						Window.alert("failure");
					}
				});
			}

上記を実行すると、/mymodule/hogeマッピングされたサーブレットへリクエストが行われ、その結果を受け取れることが確認できました。