もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

React(Gatsby)+ Firebaseでサーバレス入門した

個人開発でFirebase使ってなんか作ろうかなということで、素振りで作ったものを公開してみました。Reduxのチュートリアルで作るTodoアプリをStoreをFirebaseにした体で作り変えたやつになります。Firebaseは古の時代に触ったときは単なるPub/SubできるDBだったのにいろいろ出来るようになっててビビりますね。触る前に公式のドキュメントをざっと読んでcodelabを試したくらいの事前知識で作りました。

作ったもの

サイトはこちら。単に最近遊んでるという理由だけでGatsby.jsで作ってます。Netlifyでホスティングしてますが、特にFirebase hostingを使ってない理由はありません。AuthとFirestoreだけ試したかったので。

gatsby-firebase-todo.netlify.com

コードはこちら

GitHub - rei-m/gatsby-firebase: Sample of Gatsby.js with Firebase

実装

Firebaseの設定

何はともあれエントリーポイントでfirebase.initializeAppをしなければいけないのですが、Gatsbygatsby-browser.jsにonClientEntryというAPIが生えているのでそこで行いました。これだけですね。

export const onClientEntry = () => {
  const config = {
    // your config
  };
  firebase.initializeApp(config);
};

Firebase.authentication

firebase.auth().onAuthStateChangedにObserverを登録します。これはアプリケーション全体に影響するのでReact.Contextに認証状態を保持してアプリケーション全体を囲むようにしました。こんな感じのcustom hookを作って取れたuserオブジェクトをcontextで持つようにしてます。

useFirebaseAuth.ts

export const useFirebaseAuth = () => {
  const [user, setUser] = useState<User | null>();

  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      if (user) {
        console.info(`firebase: authorized (uid: ${user.uid})`);
        const userName = user.displayName ? user.displayName : '名無し';
        setUser({ uid: user.uid, name: userName });
      } else {
        console.info(`firebase: unauthorized`);
        setUser(null);
      }
    });

    return () => {
      console.info(`firebase: unsubscribe onAuthStateChanged`);
      unsubscribe();
    };
  }, []);

  return user;
};

useEffectを使ってonAuthStateChangedのsubscribeを開始、componentが破棄されるときにunsbscribeするという感じになりますね。contextはこれだけになります。Gatsbygatsby-browserとgatsby-ssrのwrapRootElementでこのProbiderで包んであげればOKです。

FirebaseAuthProvider.tsx

export const FirebaseAuthContext = React.createContext<{
  user?: User | null;
}>({});

const FirebaseAuthProvider: React.FC<{}> = ({ children }) => (
  <FirebaseAuthContext.Provider value={{ user: useFirebaseAuth() }}>
    {children}
  </FirebaseAuthContext.Provider>
);

これで認証状態が変わったタイミングでcontextのuserが更新されて再描画が走るようになりました。contextの情報はuseContextを使えば参照できます。

Firebase.firestore

今回はよくあるTodoアプリを作るのでユーザーごとのTodoリストをFirestoreに保存します。構成は素朴にUsersというCorrectionの下にUser単位でdocmentを作り、その下にSubCorrectionでtodosを保存します。 FirestoreもonSnapshotでObserverを登録する実装になる(リアルタイム同期が必要なければいらないけど)ので、同様にuseEffectを使ったcustom hookを作ります。

useFirestoreTodos.ts

export const useFirestoreTodos = (uid: string, filter: VisibilityFilter) => {
  const [todos, setTodos] = useState<Todo[]>();

  useEffect(() => {
    const collection = todosCollection(uid);

    let query: firebase.firestore.Query;
    switch (filter) {
      case SHOW_ACTIVE:
        query = collection
          .where(`completed`, `==`, false)
          .orderBy(`createdAt`, `desc`);
        break;
      case SHOW_COMPLETED:
        query = collection
          .where(`completed`, `==`, true)
          .orderBy(`createdAt`, `desc`);
        break;
      default:
        query = collection.orderBy(`createdAt`, `desc`);
        break;
    }

    const unsubscribe = query.onSnapshot(snapshot => {
      console.info(`firestore: receive todos: size=${snapshot.docs.length}`);

      const todos = snapshot.docs.map(doc => toModel(doc.id, doc.data()));
      setTodos(todos);
    });

    return () => {
      console.info(`firestore: unsubscribe onSnapshot:todos`);
      unsubscribe();
    };
  }, [filter]);

  return todos;
};

authと同様ですが、リストの検索条件は変更出来るので、useEffectの第2引数にfilterを指定してfilterが変更されたらobserverを登録し直すようにします。実際にこのhookを使ったcomponentはこんな形になります。

TodoContents.tsc

const TodoContents = ({ user }: Props) => {
  const [filter, setFilter] = useState<VisibilityFilter>(SHOW_ALL);
  const todos = useFirestoreTodos(user.uid, filter);

  const handleAddTodoSubmit = async (todoName: string) => {
    await addTodoAction(user.uid, todoName);
  };

  const handleClickTodo = async (todo: Todo) => {
    await updateTodoAction(user.uid, todo.id, !todo.completed);
  };

  const handleClickDeleteTodo = async (todo: Todo) => {
    await deleteTodoAction(user.uid, todo.id);
  };

  return (
    <section>
      {todos ? (
        <>
          <AddTodoForm onSubmit={handleAddTodoSubmit} />
          <TodoList
            todos={todos}
            onClickTodo={handleClickTodo}
            onClickDeleteTodo={handleClickDeleteTodo}
          />
          <TodoFilter currentFilter={filter} onClick={setFilter} />
        </>
      ) : (
        <div>ろーでいんぐ</div>
      )}
    </section>
  );
};

動かしてみるとこんな感じ

f:id:Rei19:20190608212724g:plain

おしまい

  • 簡単にしか触っていないけどFirestoreまわりの設計がキモになるなーという印象です。RDBでは○○できるのにとか思わずに(そもそも全然別物だけど)、Firestoreに最適化した設計を考えてかないといかんですね。
  • あとはCloudFunctionあたりを素振りすれば、だいたいサービス作るのに必要なものは事足りそう。
  • docment更新したときにonSnapshotで2回通知が流れてくるのがちょっと謎挙動でした。serverTimestampの仕様っぽいけど要調査。
  • テスト書くのはどうやるのかわかってないので後ほど

関連

というようなことを試してたらエンジニアHUBで気合の入った入門記事が流れてきたので紹介など

employment.en-japan.com

TSLintからESLintへ移行した

React/TypeScriptなアプリで TSLint + Prettier でlint/formatをかけていたところを ESLint-TypeScript + Prettierに移行したときのメモ。急いでやる必要はないのですが、仕事の方で使いたかったので個人プロジェクトで試しました。

github.com

github.com

必要なライブラリの追加

yarn add -D eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react @typescript-eslint/eslint-plugin

eslintの設定の追加

  • .eslintrc.jsonを追加 (フォーマットはいろいろ選べる)
{
  "extends": [
    // とりあえず推奨ルールに従っておく。後でメンテ。
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": [
    "@typescript-eslint", // TypeScript用
    "react"  // react用
  ],
  "env": {
    "browser": true,  // 事前に定義済みの条件を設定。例えばjestを入れないと`describe`や`it`が未定義の参照でlintにひっかかる
    "jest": true,
    "es6": true,
    "node": true
  },
  "parser": "@typescript-eslint/parser", // TypeScript用
  "parserOptions": {
    "sourceType": "module", // iTypeScriptはimportが使われるので
    "project": "./tsconfig.json", // TypeScriptのconfig path
    "ecmaFeatures": {
      "jsx": true  // react用
    }
  },
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], // TypeScript用のno-unused-varsを有効にしている。
    "no-console": ["error", { "allow": ["info", "error"] }], // この辺はなくても
    "react/prop-types": "off"  // TypeScriptではprop-types不要。というかoffらないとlintに引っかかる
  },
  "settings": {
    "react": {
      "version": "detect" // デフォルトっぽい設定だけど入れないとwarningが出た。。。どこか間違ったのだろうか。。。
    }
  }
}
  • tslint.jsonを削除

package.json の更新

  • npmコマンドの修正
 "scripts": {
  "lint": "eslint '{src,helper}/**/*.{ts,tsx}'",
}
  • tslint系の依存を削除

必要な人はVSCodeの設定の更新

  • tslint系の設定を削除して以下を追加。保存時にlintのruleに従って自動で修正されるようになります。
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        { "language": "typescript", "autoFix": true },
        { "language": "typescriptreact", "autoFix": true }
    ]

おしまい

  • 割とすっと移行できました。実際のPRはこちら

github.com

参考

teppeis.hatenablog.com

Gatsby.js + Storybook でStaticQueryが動かなかったのでどうにかした

概要

  • Gatsbyで書いたアプリにStorybook導入してSnapshotのテストはStoryshots使うようにしようと思ったのですが、普通にドキュメントのとおりに導入したらgraphqlを呼んでいるcomponentで以下のようなログが出てコケました。Gatsbyのコンテキストで起動してないとgraphqlは呼ばれたときに死ぬみたいですね。
index.js:39 Error: It appears like Gatsby is misconfigured. Gatsby related `graphql` calls are supposed to only be evaluated at compile time, and then compiled away,. Unfortunately, something went wrong and the query was left in the compiled code.

どうやった

  • ドキュメントやらissueやら掘ってみたものの見つからず。。。しょうがないのでaliasでgatsbyのモジュールをモックに差し替えるようにしました。

  • とりあえず雑にモック作ります。もともとjestを導入するときに作るモックなのですが、jestを使わない体に変え、useStaticQueryのところはQueryの名前に引っ掛けて対応するそれっぽいデータを返すようにします。

mocks/gatsby.js

const linkActionHandler = action("Link:");
const navigateActionHandler = action("NavigateTo:");

module.exports = {
  graphql: (args) => args,
  Link: ({ to, ...rest }) =>
    React.createElement("a", {
      ...rest,
      href: to,
      onClick: (e) => {
        e.preventDefault();
        linkActionHandler(to);
      }
    }),
  StaticQuery: () => {},
  useStaticQuery: (args) => {
    const query = args[0];
    if (query.indexOf("query SEOQuery") > -1) {
      return {
        site: {
          siteMetadata: {
            title: "タイトル",
            description: "説明",
            author: "rei-m",
          },
        },
        ogpImage: {
          publicURL: iconImage,
        },
      };
    }
    
    (中略)

    return {};
  },
  navigate: (to, options) => {
    navigateActionHandler(to, options);
  }
}
  • そして、storybookのwebpackのaliasでgatsbyが呼ばれたらモックが読み込まれるようにします。

.storybook/webpack.config.js

module.exports = ({ config }) => {
  // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
  config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

  (中略)

  config.resolve.alias = {
    ...config.resolve.alias,
    '@src': resolve(__dirname, '../src'),
    '@helper': resolve(__dirname, '../helper'),
    'gatsby': resolve(__dirname, '../__mocks__/gatsby'), // 追加
  }

  return config
}

これでめでたく?Storybookが動いてStoryshotsもall greenになりました

f:id:Rei19:20190426221321p:plain

おしまい

  • 最初useStaticQueryをReact.Contextで保持するようにして描画時に差し替える感じで書いてたのですが過剰な気がしたので、上記のような形に。
  • あまりStorybook詳しくないので絶対もっといい方法があると思われます。。。
  • コードはこちら

github.com

百人一首を暗記するサイトをGatsby.jsに乗せてNetlifyで公開した

ここ最近やってた作業がきりのいいところまで終わったのでログ代わりのブログです。もともとピュアなSPAとして作っていたものをGatsbyに乗せて静的化しました。コアのゲーム部分は以前と変わらずSPAとして動きますが、Googleにインデックスさせる部分は実体のHTMLがちゃんと存在する状態になっています。

作ったもの

hyakuninanki.net

前のバージョンの問題点

  • SEOで事故る
    • 最近のGoogle先生BotはJSによるレンダリング後のページを見てくれてるっぽくて、SearchConsoleでFetch as Googleしてちゃんと表示されたし、インデックスにも乗ったので放っておいたのですが、ある時点で検索結果から消滅してキャッシュを見ると真っ白なページになっていました。おそらくレンダリングがされずに空のページが登録されたのだろうというところです。レスポンス自体は常時200返してますしね。
  • アドセンスの審査に落ちる
    • 直接関係があるかは謎ですが、前の状態だとコンテンツ不足と言われ、リメイクした後だとすんなり審査がおりました。

コメント

  • だいたい調べたいことはGatsbyを検証したときに終わっていたので、ページレベルのComponentを多少作り変えるのと画像類の参照をGatsbyの仕組みに載せ替えるくらいで割とすんなり移行できました。もともとReact + TypeScript + styled-componentsで作ってたのでコアな部分は割とそのままですね。今のところ、SPA作るときはこの組み合わせで満足してます。
  • コードはいつもどおりポートフォリオ代わりに公開してます。

GitHub - rei-m/web_hyakuninisshu

参考

去年公開した時の記事

rei19.hatenablog.com

事前の素振りとして検証した記事

rei19.hatenablog.com

Androidアプリ版もあるよの記事

rei19.hatenablog.com

Gatsby.jsでTypeScriptで書かれたReact/ReduxなSPAを配信する

個人で公開しているSPAが素朴なReactで作られていてSSRも何もしておらず、もうちょっとGoogle先生Botに優しく作ってあげようということで、前段階の準備としていろいろ試してみたメモです。対象のサイトはそんなに大きくないので、SSRを頑張るのではなくて事前にレンダリング済のページを静的なHTMLに出力して配信という形式にします。

作ったもの

いきなり作り変えるには知見がないのでReduxのチュートリアルでよくやるTodoアプリケーションをGatsby.jsを使って実装して、できたものをNetlifyにデプロイしました。言語は一部のGatsbyの設定ファイル以外はTypeScriptで書いています。

で、作ってみたのがこちら gatsby-ts-todo.netlify.com

オリジナルのチュートリアルには無いページですがTodoの詳細のページを追加していて 、pathはtodos/:id/ になります。Gatsby.jsで動的に静的なページを生成するための検証として、Todoを2つ初期状態で登録済にしておいて、これらのTodoの詳細ページは静的なページとして配信して、URLを直接叩いて見えるようになっています。

これは直接見える https://gatsby-ts-todo.netlify.com/todos/1/

これは直接見えない。 https://gatsby-ts-todo.netlify.com/todos/3/

はまったところ

ページを動的に生成するためにはgatsby-node.jsに生えているAPIを実装するのですが、ここもTSで書きたいなあというとこで、ちょっと試行錯誤しました。結果としては以下のようになりました。

gatsby-node-config.tsにTSで実装を書いてます。createPagesではStoreの初期データからcreatePageしてるのがわかると思います。

gatsby-node-config.ts

import { resolve } from 'path';
import { GatsbyCreatePages, GatsbyOnCreatePage } from './types';
import { initialData } from '@src/state/todos';
import { Todo } from '@src/types';

export const createPages: GatsbyCreatePages<{ todo: Todo }> = async ({ actions }) => {
  const { createPage } = actions;
  initialData.forEach(todo => {
    createPage({
      path: `/todos/${todo.id}`,
      component: resolve(`./src/templates/todos.tsx`),
      context: {
        todo,
      },
    });
  });
};

export const onCreatePage: GatsbyOnCreatePage<{ todo: Todo }> = async ({ page, actions }) => {
  const { createPage } = actions;

  // page.matchPath is a special key that's used for matching pages
  // only on the client.
  if (page.path.match(/^\/todos/)) {
    page.matchPath = '/todos/:id';
    createPage(page);
  }
};

で、本来実装する gatsby-node.js では gatsby-node-config からrequireしてexportしてスルーパスしています。configのビルドはts-nodeを使うことでうまいこと通りました。

gatsby-node.js

'use strict'

require('source-map-support').install()

require("tsconfig-paths").register({
  baseUrl: './',
  paths: {
    '@src/*': [ 'src/*' ],
    '@test/*': [ 'test/*' ]
  }
});

require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'es2017',
    noImplicitAny: false
  }
})

const { resolve } = require('path');

const config = require('./gatsby-node-config');

exports.createPages = config.createPages;

exports.onCreatePage = config.onCreatePage;

exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    resolve: {
      alias: {
        '@src': resolve(__dirname, 'src/'),
        '@test': resolve(__dirname, 'test/')
      }
    }
  })
}

おしまい

Gatsbyチュートリアルが充実してたので、Reactを触ったことがあれば基本的な機能は割とすぐ使えるかなという印象です。GraphQLまわりのところがちょっとまだ完全には把握できてないので、そのあたりをもうちょっとドキュメントを読みつつ試してみようかなと思います。

全体のコードはこちら

GitHub - rei-m/gatsby-todo: Todo Application created by Gatsby with TypeScript.

2018年を振り返る

このエントリは、今年一年の自己の振り返り Advent Calendar 2018 - Adventar の23日目の記事で、34歳のWeb界隈で働いているおじさんが2018年を振り返ります。

仕事の開発話

  • 去年の春ぐらいに転してから変わらず10人程度のベンチャーで2BのWebサービスを作り続けてます。幸いなことにユーザーの評判もよく、ヘビーに使っていただいているのでいろいろ作ったかいはあったかなというところです。
  • 使っている技術スタックはバックエンドはRoR、フロントエンドはReact/Redux with TypeScriptでベンチャーによくある構成です。他と多少違うのはバックエンドもフロントエンドもテストを結構ガチガチに書いているところですかね。2Bでユーザーの業務に影響が出るものを作ってるので、速度 < 安定 で開発してます。

個人の開発話

  • アプリを1本クローズしてWebサイトを2本公開しました。 Webサイトは最初はAWSで公開してたのですが、Netlifyに切り替えました。今のスキルセット的にはAWSを習熟するよりはJAMStackな構成で高速にサービスを立ち上げられる方に寄せたほうがいいかなーと思ったのでNetlifyにしてます。
  • 公開してるもの1

play.google.com

2016年の12月にリリースしたやつです。順調にDL数が伸びてこの記事を書いてる時点で13.4Kでした。あんまり機能改善はできてないけど今年はAndroidX対応したり、AAC取り入れたりと、まあまあモダンな感じに手を入れました。

  • 公開してるもの2

https://hyakuninanki.net/

↑のアプリのWeb版てことで春ぐらいにリリースしました。しばらく放置していてユーザーも全然来なかったのですが、10月くらいにSEO意識してコンテンツ見直したら改善しました。今は百人一首で検索すると1ページの3〜5番目の表示で安定しています。↓は劇的に改善されたのがよくわかるグラフ。

f:id:Rei19:20181222225931p:plain

  • 公開してるもの3

https://osechi.jp/

osechi.jpというドメイン買ったのでおせちのメタサーチサイト作りました。が、改善する前にシーズンのピークが過ぎたのでまた来年。

その他四方山

  • 娘が無事に1歳になりました。同時に保育園にも入り妻も仕事し始めたので、土日はワンオペで子供の面倒を見ることも少なくありません。必然的に自分がコードに向き合える時間は深夜くらいしかないので、いろいろやりたいことに対して物理的な時間が足りないですね。まあ子供優先だし人生の中ではそんな時期もあるだろうという感じです。
  • 前ほど勉強会に顔を出さなくなりました。↑な状態なので勉強会のインプットを自分のアウトプットに変換できないためです。DroidKaigiとか大きいカンファレンスには引き続き参加します。
  • 故あって青色申告するために開業届を出しました。屋号はまだない。せっかくならなんか軽い副業くらいしてみたいすな。

MotionLayoutでアニメーション作る

この記事は Android Advent Calendar 2018 の5日目(の代打)です。

だいたいこれ読めば基本は一通りわかる

medium.com

サンプルはこのRepositoryの中のmotionlayoutディレクトリをビルドすれば試せる github.com

雑な概要

  • MotionLayoutはConstraintLayout 2.0に含まれている。ConstraintLayoutのパッケージに含まれているのはMotionLayoutがConstraintLayoutのサブクラスになっているため。
  • オブジェクトのモーション開始状態と終了状態のConstraintを定義して、モーションスタートさせると開始状態から終了状態までオブジェクトが変化する。単に開始・終了を指定しただけだとシンプルに変化するだけだが、KeyFrameSetやCustomAttributeを使うことで、多様な変化を定義できる。
  • 基本の設定
    1. MotionLayout内に動かしたいオブジェクトを配置する。動かしたいオブジェクト以外は配置しない。
    2. オブジェクトのモーションの開始状態と終了状態を定義したSceneを作成する。
    3. Transitionを設定し、開始状態と終了状態を設定する。必要ならモーション開始のハンドラを設定する(クリックやスワイプなど)。設定しなくても他のViewからMotionLayoutのprogressプロパティを操作することでモーションをコントロールできる。
    4. 作成したシーンをMotionLayoutに設定する

基本のサンプル(冒頭のandroid-ConstraintLayoutExamplesより)

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

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

</MotionScene>
  • scene_fragment.xml
<androidx.constraintlayout.motion.widget.MotionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/scene" // 作成したSceneを指定
    app:showPaths="true"
    android:background="@android:color/white"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/button"
        android:background="@color/colorAccent"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:text="Button" />

</androidx.constraintlayout.motion.widget.MotionLayout>
  • ここで注意するポイントはモーションの対象となるオブジェクト(id/button)のMotionLayout内で設定したconstraintは無視されて、Sceneの開始状態のconstraintが優先される。
  • 動かすと↓のようになる。MotionLayoutをスワイプすると対象のオブジェクトが移動しているのがわかる。

f:id:Rei19:20181219022413g:plain

KeyFrame

  • KeyFrameを使うと開始・終了の中間の状態を定義できる。上のサンプルのTransitionの中に以下のようなKeyFrameを追加してみる。
   <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right" />
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="33"
                motion:keyPositionType="pathRelative"
                motion:percentY="-0.3"
                motion:target="@id/button" />
            <KeyPosition
                motion:framePosition="66"
                motion:keyPositionType="pathRelative"
                motion:percentY="0.3"
                motion:target="@id/button" />
        </KeyFrameSet>
    </Transition>

f:id:Rei19:20181220013332g:plain

  • framePositonは開始が0で終了が100になる。KeyFrameはその中間の値の時にどのような状態になるかを指定する。上記の例はpositonが33、66の時にy座標の位置を変えるような指定になっている。
  • KeyFrameは以下の種類を指定できる

CustomAttributeを使ってオブジェクトの属性を変化する

  • KeyAttributeで指定できない属性もCustomAttributeを使えば状態を変化できる。属性名はオブジェクトに生えているgetter/setterからget/setを除いた名前を指定する。
  • CustomAttributeはConstraintとKeyAttributeの子要素に指定できる。基本のサンプルを以下のように変更してみる。
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right" />
        <KeyFrameSet>
            <KeyAttribute
                motion:framePosition="50"
                android:scaleX="2"
                android:scaleY="2"
                motion:target="@+id/button">
                <CustomAttribute
                    motion:attributeName="backgroundColor"
                    motion:customColorValue="@color/colorPrimaryDark" />
            </KeyAttribute>
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent">
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="@color/colorAccent" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent">
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="@color/colorAccent" />
        </Constraint>
    </ConstraintSet>

</MotionScene>
  • 開始・終了のConstraintに初期表示時のbackgroundColorを追加、framePosition=50の時点のKeyAttributeを追加して、初期表示と異なるbackgroundColorを追加した。これを動かすと以下のようになる。

f:id:Rei19:20181220020706g:plain

既存のComponentとMotionLayoutを連携する

  • MotionLayoutのprogressプロパティを操作すればOnClickやOnSwipeを設定しなくてもオブジェクトを変化させることができる。試しにButtonを押したら10進むようにしてみる。

f:id:Rei19:20181220022756g:plain

  • コードは以下のようになっている。MotionLayoutのprogressをいじっているだけ。
        val motionLayout = view.findViewById<MotionLayout>(R.id.motionLayout)
        val button = view.findViewById<Button>(R.id.go)
        button.setOnClickListener {
            val current = motionLayout.progress
            val next = current + 0.1f
            motionLayout.progress = if (next > 1) 0f else next
        }
  • ConstraintLayoutExamplesにはDrawerやCoordinatorLayoutと連携するサンプルが載っているので試してみてほしい。

おしまい

  • Exampleのプロジェクトがかなり充実していたのでありがたかったです 🙏