Android ビューの基本的な色のハーモナイゼーション

1. 始める前に

この Codelab では、カスタムカラーとダイナミック テーマによって生成されたカラーを調和させる方法を学びます。

前提条件

開発者は

  • Android における基本的なテーマ設定のコンセプトに精通している
  • Android ウィジェット ビューとそのプロパティの操作に慣れている

学習内容

  • アプリケーションで複数の方法を使用して色のハーモナイゼーションを使用する方法
  • ハーモナイゼーションの仕組みと色の変化

必要なもの

  • Android がインストールされたパソコン(必要に応じて)。

2. アプリの概要

VoyaÃi は、すでに動的なテーマを使用している交通機関アプリです。多くの交通機関では、列車、バス、路面電車の重要なインジケーターとして色が使用されます。これらの色を、使用可能な動的なプライマリ カラー、セカンダリ カラー、ターシャリ カラーに置き換えることはできません。色付きの交通機関カードの RecyclerView に重点的に取り組みます。

62ff4b2fb6c9e14a.png

3. テーマの生成

Material3 テーマを作成するには、最初に Google のツール Material Theme Builder を使用することをおすすめします。カスタムタブで、テーマに色を追加できるようになりました。右側には、これらの色のカラーロールと色調パレットが表示されます。

拡張カラーのセクションでは、色を削除したり、名前を変更したりできます。

20cc2cf72efef213.png

エクスポート メニューには、利用可能なエクスポート オプションが表示されます。現時点では、マテリアル テーマビルダーによるハーモナイゼーション設定の特別な処理は、Android ビューでのみ使用できます。

6c962ad528c09b4.png

新しいエクスポート値について

ハーモナイズするかどうかにかかわらず、これらの色とそれに関連するカラーロールをテーマで使用できるように、各カスタムカラーのカラーロール名を含む attrs.xml ファイルがエクスポートされたダウンロードに含まれるようになりました。

<resources>
   <attr name="colorCustom1" format="color" />
   <attr name="colorOnCustom1" format="color" />
   <attr name="colorCustom1Container" format="color" />
   <attr name="colorOnCustom1Container" format="color" />
   <attr name="harmonizeCustom1" format="boolean" />

   <attr name="colorCustom2" format="color" />
   <attr name="colorOnCustom2" format="color" />
   <attr name="colorCustom2Container" format="color" />
   <attr name="colorOnCustom2Container" format="color" />
   <attr name="harmonizeCustom2" format="boolean" />
</resources>

customers.xml では、カスタムカラー(color<name>, colorOn<name>, color<name>Container, and colorOn<nameContainer>)ごとに 4 つのカラーロールを生成しました。harmonize<name> プロパティは、デベロッパーがマテリアル テーマビルダーでオプションを選択しているかどうかを反映します。コアテーマの色は変化しません。

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

colors.xml ファイルでは、上記のカラーロールを生成するために使用されるシードカラーが、カラーパレットがシフトされるかどうかのブール値とともに指定されます。

<resources>
   <!-- other colors used in theme -->

   <color name="custom1">#1AC9E0</color>
   <color name="custom2">#32D312</color>
</resources>

4. カスタムカラーを調べる

マテリアル テーマビルダーのサイドパネルにズームインすると、カスタムカラーを追加すると、4 つの主要なカラーロールを持つパネルがライトとダークのパレットで表示されます。

c6ee942b2b93cd92.png

Android ビューでは、これらの色をエクスポートしますが、舞台裏では ColorRoles オブジェクトのインスタンスで表現できます。

ColorRoles クラスには、accentonAccentaccentContaineronAccentContainer の 4 つのプロパティがあります。これらのプロパティは、4 つの 16 進数色の整数表現です。

public final class ColorRoles {

  private final int accent;
  private final int onAccent;
  private final int accentContainer;
  private final int onAccentContainer;

  // truncated code

}

getColorRoles という MaterialColors クラスの getColorRoles を使用して、実行時に任意の色から 4 つの主要なカラーロールを取得できます。これにより、実行時に特定のシードカラーを指定して 4 つのカラーロールのセットを作成できます。

public static ColorRoles getColorRoles(
    @NonNull Context context,
    @ColorInt int color
) { /* implementation */ }

同様に、出力値は実際の色の値であり、それらへのポインタではありません。**

5. 色のハーモナイゼーションとは

マテリアルの新しいカラーシステムはアルゴリズムに基づいて設計されており、特定のシードカラーからプライマリ カラー、セカンダリ カラー、ターシャリ カラー、ニュートラル カラーを生成します。社内外のパートナーと話し合う際、多くの懸念が寄せられたのが、一部の色を制御しながらダイナミック カラーを採用するにはどうすればよいかというものでした。

これらの色は多くの場合、ランダムな色で置き換えられると失われる、アプリケーションで特定の意味やコンテキストを持ちます。また、そのままにしておくと、これらの色が見えにくくなったり、違和感もあったりする可能性があります。

Material You の色は、色相、彩度、トーンで表現されます。色の色相は、ある色範囲に属するものとして、または別の色範囲に含まれていると認識するかどうかに関係します。トーンは明るさや暗さを表します。彩度は色の強さを表します。色相の認識は、文化的および言語的要因の影響を受けることがあります。たとえば、古代の文化では青という言葉が無いと言われ、緑と同じ家系として見なされがちです。

57c46d9974c52e4a.png特定の色相は、色相スペクトルにおける位置に応じて、暖色または寒色として判断されます。一般的に、赤、オレンジ、黄色にシフトすると暖色に、青、緑、紫にシフトすると寒くなると言われています。暖色または寒色の中でも、暖色系と寒色系の色調になります。その下の「ウォーマー」黄色はオレンジ色がかってますが黄色は緑色の影響を受けがちです。597c6428ff6b9669.png

カラー ハーモナイゼーション アルゴリズムは、シフトしていない色の色相と、ハーモナイズすべき色を調べて、調和したが基本的な色品質を変えない色相を見つけます。最初の図では、調和のとれた緑色、黄色、オレンジ色の色調がスペクトル上にプロットされています。次の図では、緑とオレンジが黄色の色調と調和しています。新しい緑はより暖かく、新しいオレンジはよりクールです。

オレンジ色と緑色で色相が変化していますが、それでもオレンジ色と緑色として認識できます。

766516c321348a7c.png

設計上の決定事項、検討事項、考慮事項について詳しく知りたい場合は、私の同僚である Ayan Daniels と Andrew Lu が、このセクションよりももう少し深く掘り下げたブログ投稿を書いています。

6. 色の調整を手動で行う

単一のトーンにハーモナイズするために、MaterialColorsharmonizeharmonizeWithPrimary に 2 つの関数があります。

harmonizeWithPrimary は、現在のテーマとそのプライマリ カラーにアクセスする手段として Context を使用します。

@ColorInt
public static int harmonizeWithPrimary(@NonNull Context context, @ColorInt int colorToHarmonize) {
    return harmonize(
        colorToHarmonize,
        getColor(context, R.attr.colorPrimary, MaterialColors.class.getCanonicalName()));
  }


@ColorInt
  public static int harmonize(@ColorInt int colorToHarmonize, @ColorInt int colorToHarmonizeWith) {
    return Blend.harmonize(colorToHarmonize, colorToHarmonizeWith);
  }

4 つのトーンのセットを取得するには、もう少し処理を行う必要があります。

ソースカラーがすでに決まっている場合、次のようにする必要があります。

  1. ハーモナイズすべきかどうかを判断し
  2. ダークモードかを判断し
  3. ハーモナイズされたオブジェクトまたはハーモナイズされていない ColorRoles オブジェクトを返します。

ハーモナイズするかどうかの決定

マテリアル テーマビルダーからエクスポートされたテーマには、harmonize<Color> という命名法を使用したブール値属性を含めました。以下は、その値にアクセスするための便利な関数です。

見つかった場合は、その値を返します。それ以外の色を調和させるべきではないと判断します。

// Looks for associated harmonization attribute based on the color id
// custom1 ===> harmonizeCustom1
fun shouldHarmonize(context: Context, colorId: Int): Boolean {
   val root = context.resources.getResourceEntryName(colorId)
   val harmonizedId = "harmonize" + root.replaceFirstChar { it.uppercaseChar() }
   
   val identifier = context.resources.getIdentifier(
           harmonizedId, "bool", context.packageName)
   
   return if (identifier != 0) context.resources.getBoolean(identifier) else false
}

ハーモナイズされた ColorRoles オブジェクトの作成

retrieveHarmonizedColorRoles も、前述のすべてのステップを結合した便利な関数です。名前付きリソースの色値を取得し、ブール値属性を解決してハーモナイゼーションを判断し、(特定のライトまたはダークのスキームに基づいて)元の色またはブレンド色から派生した ColorRoles オブジェクトを返します。

fun retrieveHarmonizedColorRoles(
   view: View,
   customId: Int,
   isLight: Boolean
): ColorRoles {
   val context = view.context
   val custom = context.getColor(customId);
  
   val shouldHarmonize = shouldHarmonize(context, customId)
   if (shouldHarmonize) {
       val blended = MaterialColors.harmonizeWithPrimary(context, custom)
       return MaterialColors.getColorRoles(blended, isLight)
   } else return MaterialColors.getColorRoles(custom, isLight)
}

7. 交通機関のカードの入力

前述のように、RecyclerView とアダプターを使用して、交通機関のカードのコレクションを入力し、色を付けます。

e4555089b065b5a7.png

乗換案内データの保存

交通機関のカードのテキストデータと色情報を保存するために、名前、目的地、カラーリソース ID を格納するデータクラスを使用しています。

data class TransitInfo(val name: String, val destination: String, val colorId: Int)

/*  truncated code */

val transitItems = listOf(
   TransitInfo("53", "Irvine", R.color.custom1),
   TransitInfo("153", "Brea", R.color.custom1),
   TransitInfo("Orange County Line", "Oceanside", R.color.custom2),
   TransitInfo("Pacific Surfliner", "San Diego", R.color.custom2)
)

この色を使用して、リアルタイムで必要なトーンを生成します。

実行時に次の onBindViewHolder 関数とハーモナイズできます。

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   val transitInfo = list.get(position)
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = retrieveHarmonizedColorRoles(
           holder.itemView, color,
           !isNightMode(holder.itemView.context)
       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

8. 色を自動的に調整する

ハーモナイゼーションを手動で処理する代わりに、自分で処理することもできます。HarmonizedColorOptions は、これまでの処理の多くを手作業で指定できるビルダークラスです。

現在のコンテキストを取得して現在のダイナミック スキームにアクセスできるようにしたら、調和させるベースカラーを指定し、その HarmonizedColorOptions オブジェクトと DynamicColors が有効になっているコンテキストに基づいて新しいコンテキストを作成する必要があります。

色を調整したくない場合は、単に harmonizedOptions に含めないでください。

val newContext = DynamicColors.wrapContextIfAvailable(requireContext())


val harmonizedOptions = HarmonizedColorsOptions.Builder()
 .setColorResourceIds(intArrayOf(R.color.custom1, R.color.custom2))
 .build();

harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

処理済みのハーモナイズされたベースカラーを使用して、onBindViewHolder を更新して単純に MaterialColors.getColorRoles を呼び出し、返されるロールをライトとダークのどちらにするかを指定できます。

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   /*...*/
   val color = transitInfo.colorId
   if (!colorRolesMap.containsKey(color)) {

       val roles = MaterialColors.getColorRoles(context.getColor(color), !isNightMode(context))

       )
       colorRolesMap.put(color, roles)
   }

   val card = holder.card
   holder.transitName.text = transitInfo.name
   holder.transitDestination.text = transitInfo.destination

   val colorRoles = colorRolesMap.get(color)
   if (colorRoles != null) {
       holder.card.setCardBackgroundColor(colorRoles.accentContainer)
       holder.transitName.setTextColor(colorRoles.onAccentContainer)
       holder.transitDestination.setTextColor(colorRoles.onAccentContainer)
   }
}

9. テーマ属性を自動的に調整する

これまでに紹介したメソッドは、個々の色からカラーロールを取得する方法に依存しています。これは、本物のトーンが生成されているが、ほとんどの既存のアプリケーションでは非現実的であることを示すのに適しています。多くの場合、色を直接導き出すのではなく、既存のテーマ属性を使用します。

この Codelab の前半で、テーマ属性のエクスポートについて説明しました。

<resources>
   <style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
       <!--- Normal theme attributes ... -->

       <item name="colorCustom1">#006876</item>
       <item name="colorOnCustom1">#ffffff</item>
       <item name="colorCustom1Container">#97f0ff</item>
       <item name="colorOnCustom1Container">#001f24</item>
       <item name="harmonizeCustom1">false</item>

       <item name="colorCustom2">#016e00</item>
       <item name="colorOnCustom2">#ffffff</item>
       <item name="colorCustom2Container">#78ff57</item>
       <item name="colorOnCustom2Container">#002200</item>
       <item name="harmonizeCustom2">false</item>
   </style>
</resources>

最初の自動メソッドと同様に、HarmonyizedColorOptions に値を提供し、HarmonyizedColors を使用してハーモナイズされた色でコンテキストを取得できます。この 2 つの方法には重要な違いが 1 つあります。さらに、ハーモナイズするフィールドを含むテーマ オーバーレイを提供する必要があります。

val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

// Harmonizing individual attributes
val harmonizedColorAttributes = HarmonizedColorAttributes.create(
 intArrayOf(
   R.attr.colorCustom1,
   R.attr.colorOnCustom1,
   R.attr.colorCustom1Container,
   R.attr.colorOnCustom1Container,
   R.attr.colorCustom2,
   R.attr.colorOnCustom2,
   R.attr.colorCustom2Container,
   R.attr.colorOnCustom2Container
 ), R.style.AppTheme_Overlay
)
val harmonizedOptions =
 HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

val harmonizedContext =
 HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)

アダプターはハーモナイズされたコンテキストを使用します。テーマ オーバーレイの値は、非調和のライトまたはダーク バリアントを参照する必要があります。

<style name="AppTheme.Overlay" parent="AppTheme">
   <item name="colorCustom1">@color/harmonized_colorCustom1</item>
   <item name="colorOnCustom1">@color/harmonized_colorOnCustom1</item>
   <item name="colorCustom1Container">@color/harmonized_colorCustom1Container</item>
   <item name="colorOnCustom1Container">@color/harmonized_colorOnCustom1Container</item>

   <item name="colorCustom2">@color/harmonized_colorCustom2</item>
   <item name="colorOnCustom2">@color/harmonized_colorOnCustom2</item>
   <item name="colorCustom2Container">@color/harmonized_colorCustom2Container</item>
   <item name="colorOnCustom2Container">@color/harmonized_colorOnCustom2Container</item>
</style>

XML レイアウト ファイル内では、これらの調整された属性を通常どおり使用できます。

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   style="?attr/materialCardViewFilledStyle"
   android:id="@+id/card"
   android:layout_width="80dp"
   android:layout_height="100dp"
   android:layout_marginStart="8dp"
   app:cardBackgroundColor="?attr/colorCustom1Container"
   >

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_margin="8dp">

       <TextView
           android:id="@+id/transitName"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:textSize="28sp"
           android:textStyle="bold"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintTop_toTopOf="parent" />

       <TextView
           android:id="@+id/transitDestination"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginBottom="4dp"
           android:textColor="?attr/colorOnCustom1Container"
           app:layout_constraintBottom_toBottomOf="parent" />
   </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

10. ソースコード

package com.example.voyagi.harmonization.ui.dashboard

import android.content.Context
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.voyagi.harmonization.R
import com.example.voyagi.harmonization.databinding.FragmentDashboardBinding
import com.example.voyagi.harmonization.ui.home.TransitCardAdapter
import com.example.voyagi.harmonization.ui.home.TransitInfo
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.ColorRoles
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.HarmonizedColorAttributes
import com.google.android.material.color.HarmonizedColors
import com.google.android.material.color.HarmonizedColorsOptions
import com.google.android.material.color.MaterialColors


class DashboardFragment : Fragment() {

 enum class TransitMode { BUS, TRAIN }
 data class TransitInfo2(val name: String, val destination: String, val mode: TransitMode)

 private lateinit var dashboardViewModel: DashboardViewModel
 private var _binding: FragmentDashboardBinding? = null

 // This property is only valid between onCreateView and
 // onDestroyView.
 private val binding get() = _binding!!

 override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
 ): View? {
   dashboardViewModel =
     ViewModelProvider(this).get(DashboardViewModel::class.java)

   _binding = FragmentDashboardBinding.inflate(inflater, container, false)
   val root: View = binding.root


   val recyclerView = binding.recyclerView

   val transitItems = listOf(
     TransitInfo2("53", "Irvine", TransitMode.BUS),
     TransitInfo2("153", "Brea", TransitMode.BUS),
     TransitInfo2("Orange County Line", "Oceanside", TransitMode.TRAIN),
     TransitInfo2("Pacific Surfliner", "San Diego", TransitMode.TRAIN)
   )
  
   val dynamicColorsContext = DynamicColors.wrapContextIfAvailable(requireContext())

   // Harmonizing individual attributes
   val harmonizedColorAttributes = HarmonizedColorAttributes.create(
     intArrayOf(
       R.attr.colorCustom1,
       R.attr.colorOnCustom1,
       R.attr.colorCustom1Container,
       R.attr.colorOnCustom1Container,
       R.attr.colorCustom2,
       R.attr.colorOnCustom2,
       R.attr.colorCustom2Container,
       R.attr.colorOnCustom2Container
     ), R.style.AppTheme_Overlay
   )
   val harmonizedOptions =
     HarmonizedColorsOptions.Builder().setColorAttributes(harmonizedColorAttributes).build()

   val harmonizedContext =
     HarmonizedColors.wrapContextIfAvailable(dynamicColorsContext, harmonizedOptions)


   val adapter = TransitCardAdapterAttr(transitItems, harmonizedContext)
   recyclerView.adapter = adapter
   recyclerView.layoutManager =
     LinearLayoutManager(harmonizedContext, RecyclerView.HORIZONTAL, false)

   return root
 }

 override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
 }
}

class TransitCardAdapterAttr(val list: List<DashboardFragment.TransitInfo2>, context: Context) :
 RecyclerView.Adapter<RecyclerView.ViewHolder>() {
 val colorRolesMap = mutableMapOf<Int, ColorRoles>()
 private var harmonizedContext: Context? = context

 override fun onCreateViewHolder(
   parent: ViewGroup,
   viewType: Int
 ): RecyclerView.ViewHolder {
   return if (viewType == DashboardFragment.TransitMode.BUS.ordinal) {
     BusViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_bus, parent, false))
   } else TrainViewHolder(LayoutInflater.from(harmonizedContext).inflate(R.layout.transit_item_train, parent, false))
 }

 override fun getItemCount(): Int {
   return list.size
 }

 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   val item = list[position]
   if (item.mode.ordinal == DashboardFragment.TransitMode.BUS.ordinal) {
     (holder as BusViewHolder).bind(item)
     (holder as TransitBindable).adjustNameLength()
   } else {
       (holder as TrainViewHolder).bind(item)
       (holder as TransitBindable).adjustNameLength()
   }
 }

 override fun getItemViewType(position: Int): Int {
   return list[position].mode.ordinal
 }

 interface TransitBindable {
   val card: MaterialCardView
   var transitName: TextView
   var transitDestination: TextView

   fun bind(item: DashboardFragment.TransitInfo2) {
     transitName.text = item.name
     transitDestination.text = item.destination
   }
   fun Float.toDp(context: Context) =
     TypedValue.applyDimension(
       TypedValue.COMPLEX_UNIT_DIP,
       this,
       context.resources.displayMetrics
     )
   fun adjustNameLength(){
     if (transitName.length() > 4) {
       val layoutParams = card.layoutParams
       layoutParams.width = 100f.toDp(card.context).toInt()
       card.layoutParams = layoutParams
       transitName.setTypeface(Typeface.DEFAULT_BOLD);

       transitName.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f)
     }
   }
 }

 inner class BusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
 inner class TrainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), TransitBindable {
   override val card: MaterialCardView = itemView.findViewById(R.id.card)
   override var transitName: TextView = itemView.findViewById(R.id.transitName)
   override var transitDestination: TextView = itemView.findViewById(R.id.transitDestination)
 }
}

11. UI の例

デフォルトのテーマ設定とカスタムカラー(ハーモナイゼーションなし)

a5a02a72aef30529.png

ハーモナイズされたカスタムカラー

4ac88011173d6753.png d5084780d2c6b886.png

dd0c8b90eccd8bef.png c51f8a677b22cd54.png

12. まとめ

この Codelab では、以下のことを学びました。

  • カラー ハーモナイゼーション アルゴリズムの基本
  • 特定の表示色からカラーロールを生成する方法。
  • ユーザー インターフェースで色を選択的に調和する方法。
  • テーマ内の一連の属性を調整する方法。

不明な点がある場合は、Twitter の @MaterialDesign までいつでもお気軽にお問い合わせください。

その他のデザインに関するコンテンツやチュートリアルについては、youtube.com/MaterialDesign をご覧ください。