2012年7月アーカイブ

customLogo.png 

2012年7月8日、東京・京都・名古屋・中国・四国・九州・信州・米国 で同時開催された

DevFestX Japan 2012 Summer に参加してきました。

 

公式サイト

 

講演スライド(直リンク)

 

各会場からアップされた画像

最近 Google+ に追加された「イベント」機能のパーティーモードを使った写真

 

Togetter

@roishi2j2 さんによるまとめ

@tamacjp さんによるまとめ

 

お土産

「android ロボットのUSBメモリ」 や 「Google のビーチサンダル」 を頂きました。

usb.jpg

 

信州会場

Google Maps API Expert の 石丸(@kehi)さん、古籏(@openspc)さんの働きかけにより、

信州会場(パブリックビューイング)として開催していただけました。

「GTUG がない → DevFestX 行けない → しょぼーん(´・ω・`) 」 ではなく、

「ないなら創る → DevFestX やる → イヤッホゥ(゜∀゜)!!」 という流れです。

 

パブリックビューイングということでしたが、実際には他の会場と同様に Google+ Hangout で接続され、

地方にいながら、皆さんと同じ体験をすることができました。これも、

当日ぎりぎりまで準備してくださった Google Expert の方々、素晴らしい会場を提供してくださった S.I.P の方々、

裏方で運営をサポートしてくださった方々、満席になるほど参加して盛り上げてくださった皆様のおかげですね。

大変有難いことですm_ _m

 

@lychee さんのツイートにもありましたが、まさにそのとおりだと感じました。

221873380897460224.png

 

まとめると・・・

 

221834100988846080.png

 

九州会場凄い!ハングアウト凄い!

Android 4.1 (API Level 16)の ViewPropertyAnimator の変更点を調べました。

 

変更点

以下の3メソッドが追加されています。

  • withLayer()
  • withStartAction(Runnable)
  • withEndAction(Runnable)

 

解説

  • withLayer()

ViewPropertyAnimator を使用してアニメーションを実行する時、

アニメーション対象の View をハードウェアレイヤを使用してアニメーションさせます。

アニメーション終了後は、この設定はクリアされます。

ただし、ハードウェアアクセラレータが使用できない環境では、ソフトウェアレイヤが使用されます。

レイヤーの合成処理をハードウェアに任せることで高速化が期待できます。

 

  • withStartAction(Runnable) / withEndAction(Runnable)

ViewPropertyAnimator を使用したアニメーションの開始/終了時に実行したい処理を設定できます。

 

追加されたメソッドについて、実際に呼び出してみました。

以下のコードを実行すると、アニメーション開始時(3秒後)に、withStartAction() で指定した処理、

アニメーション終了時に、withEndAction() で指定した処理が呼び出されます。

また、withLayer() を呼び出しているので、ビュー(ここではボタン)のアニメーションは、ハードウェアレイヤ上で処理されます。

 

サンプルソースコード

final Button button = (Button) findViewById(R.id.button);

// 指定した View から ViewPropertyAnimator インスタンスを取得する
ViewPropertyAnimator vpa = button.animate();

// ハードウェアアクセラレータが使用できる環境では、ハードウェアレイヤを使用するように設定
vpa.withLayer();

// アニメーション継続期間を設定する(ミリ秒)
vpa.setDuration(3000);

// アニメーション開始時に実行する処理を設定する
vpa.withStartAction(new Runnable() {
    @Override
    public void run() {
        // アニメーション開始時に呼び出されます。
        // アニメーション開始遅延時間が設定されている場合は、
        // アニメーション開始遅延時間後に呼び出されます。
        String layerTypeString = "";
        int layerType = button.getLayerType();
        switch (layerType) {
            case View.LAYER_TYPE_NONE:
                // withLayer() を呼び出さなかった時
                layerTypeString = "LAYER_TYPE_NONE";
                break;
            case View.LAYER_TYPE_SOFTWARE:
                // withLayer() を呼出した時、
                // ハードウェアアクセラレータが使用できない環境では、ソフトウェアレイヤを使用される
                layerTypeString = "LAYER_TYPE_SOFTWARE";
                break;
            case View.LAYER_TYPE_HARDWARE:
                // withLayer() を呼出した時、
                // ハードウェアアクセラレータが使用できる環境では、ハードウェアレイヤを使用される
                layerTypeString = "LAYER_TYPE_HARDWARE";
                break;
                
        }
        Log.v("ViewPropertyAnimatorSample",
                "withStartAction() using " + layerTypeString);
    }
});

// アニメーション終了時に実行する処理を設定する
vpa.withEndAction(new Runnable() {
    @Override
    public void run() {
        // アニメーション修了時に呼び出されます。
        Log.v("ViewPropertyAnimatorSample", "withEndAction()");
        button.setVisibility(View.GONE);
    }
});

// ビューに対してアニメーションを設定する(-> アニメーションが開始されます)
vpa.rotation(90);

 

[Android] TimeAnimator 使い方

| トラックバック(0) |

Nyandroid.png

Android 4.1(API Level 16)から android.animation パッケージに TimeAnimator クラスが追加されました。

このクラスを使用すると、アニメーションフレームの更新通知を受け取って処理をすることができます。

 

アニメーションフレームの更新通知を受け取って処理することは、従来の ValueAnimator などのクラスでも実現できましたが、

TimeAnimator クラスでは、アニメーションの継続期間や補完関数を使用しておらず、

単純に、アニメーション開始後の すべてのアニメーションフレームの更新時(※)に呼び出されます。

※最短では VSYNC に合わせて、16 ミリ秒間隔。

 

使い方

  1. TimeAnimator クラスをインスタンス化する
  2. setTimeListener() を呼び出して、TimeAnimator.TimeListener を設定する。
  3. start() を呼び出して、アニメーションを開始する。

 

サンプルソースコード

TimeAnimator timeAnimator = new TimeAnimator();
timeAnimator.setTimeListener(new TimeAnimator.TimeListener() {
    @Override
    public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
        // totalTime : アニメーション開始後の経過時間(ミリ秒)
        // deltaTime : 前回の onTimeUpdate 呼出し時からの差分(ミリ秒) ex.16ミリ秒

        // ここで必要な処理をする
    }
});
timeAnimator.start();

 

おまけ

TimeAnimator クラスは、正確に言えば、以前のバージョンでも hide としては存在していました。

Android 4.0 以降では、TimeAnimator クラスは、以下の記事で紹介した機能で使用されています。

 

参考(Nyandroid ソースコード)

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui;

import android.animation.AnimatorSet;
import android.animation.PropertyValuesHolder;
import android.animation.ObjectAnimator;
import android.animation.TimeAnimator;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.util.HashMap;
import java.util.Random;

public class Nyandroid extends Activity {
    final static boolean DEBUG = false;

    public static class Board extends FrameLayout
    {
        public static final boolean FIXED_STARS = true;
        public static final int NUM_CATS = 20;

        static Random sRNG = new Random();

        static float lerp(float a, float b, float f) {
            return (b-a)*f + a;
        }

        static float randfrange(float a, float b) {
            return lerp(a, b, sRNG.nextFloat());
        }

        static int randsign() {
            return sRNG.nextBoolean() ? 1 : -1;
        }

        static <E> E pick(E[] array) {
            if (array.length == 0) return null;
            return array[sRNG.nextInt(array.length)];
        }

        public class FlyingCat extends ImageView {
            public static final float VMAX = 1000.0f;
            public static final float VMIN = 100.0f;

            public float v, vr;

            public float dist;
            public float z;

            public ComponentName component;

            public FlyingCat(Context context, AttributeSet as) {
                super(context, as);
                setImageResource(R.drawable.nyandroid_anim); // @@@

                if (DEBUG) setBackgroundColor(0x80FF0000);
            }

            public String toString() {
                return String.format("<cat (%.1f, %.1f) (%d x %d)>",
                    getX(), getY(), getWidth(), getHeight());
            }

            public void reset() {
                final float scale = lerp(0.1f,2f,z);
                setScaleX(scale); setScaleY(scale);

                setX(-scale*getWidth()+1);
                setY(randfrange(0, Board.this.getHeight()-scale*getHeight()));
                v = lerp(VMIN, VMAX, z);

                dist = 0;

//                android.util.Log.d("Nyandroid", "reset cat: " + this);
            }

            public void update(float dt) {
                dist += v * dt;
                setX(getX() + v * dt);
            }
        }

        TimeAnimator mAnim;

        public Board(Context context, AttributeSet as) {
            super(context, as);

            setLayerType(View.LAYER_TYPE_HARDWARE, null);
            setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
            setBackgroundColor(0xFF003366);
        }

        private void reset() {
//            android.util.Log.d("Nyandroid", "board reset");
            removeAllViews();

            final ViewGroup.LayoutParams wrap = new ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);

            if (FIXED_STARS) {
                for(int i=0; i<20; i++) {
                    ImageView fixedStar = new ImageView(getContext(), null);
                    if (DEBUG) fixedStar.setBackgroundColor(0x8000FF80);
                    fixedStar.setImageResource(R.drawable.star_anim); // @@@
                    addView(fixedStar, wrap);
                    final float scale = randfrange(0.1f, 1f);
                    fixedStar.setScaleX(scale); fixedStar.setScaleY(scale);
                    fixedStar.setX(randfrange(0, getWidth()));
                    fixedStar.setY(randfrange(0, getHeight()));
                    final AnimationDrawable anim = (AnimationDrawable) fixedStar.getDrawable();
                    postDelayed(new Runnable() { 
                        public void run() {
                            anim.start();
                        }}, (int) randfrange(0, 1000));
                }
            }

            for(int i=0; i<NUM_CATS; i++) {
                FlyingCat nv = new FlyingCat(getContext(), null);
                addView(nv, wrap);
                nv.z = ((float)i/NUM_CATS);
                nv.z *= nv.z;
                nv.reset();
                nv.setX(randfrange(0,Board.this.getWidth()));
                final AnimationDrawable anim = (AnimationDrawable) nv.getDrawable();
                postDelayed(new Runnable() { 
                    public void run() {
                        anim.start();
                    }}, (int) randfrange(0, 1000));
            }

            if (mAnim != null) {
                mAnim.cancel();
            }
            mAnim = new TimeAnimator();
            mAnim.setTimeListener(new TimeAnimator.TimeListener() {
                public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
                    // setRotation(totalTime * 0.01f); // not as cool as you would think
//                    android.util.Log.d("Nyandroid", "t=" + totalTime);

                    for (int i=0; i<getChildCount(); i++) {
                        View v = getChildAt(i);
                        if (!(v instanceof FlyingCat)) continue;
                        FlyingCat nv = (FlyingCat) v;
                        nv.update(deltaTime / 1000f);
                        final float catWidth = nv.getWidth() * nv.getScaleX();
                        final float catHeight = nv.getHeight() * nv.getScaleY();
                        if (   nv.getX() + catWidth < -2
                            || nv.getX() > getWidth() + 2
                            || nv.getY() + catHeight < -2
                            || nv.getY() > getHeight() + 2)
                        {
                            nv.reset();
                        }
                    }
                }
            });
        }

        @Override
        protected void onSizeChanged (int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w,h,oldw,oldh);
//            android.util.Log.d("Nyandroid", "resized: " + w + "x" + h);
            post(new Runnable() { public void run() { 
                reset();
                mAnim.start(); 
            } });
        }


        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            mAnim.cancel();
        }

        @Override
        public boolean isOpaque() {
            return true;
        }
    }

    private Board mBoard;

    @Override
    public void onStart() {
        super.onStart();

        getWindow().addFlags(
                  WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                );
    }

    @Override
    public void onResume() {
        super.onResume();
        mBoard = new Board(this, null);
        setContentView(mBoard);

        mBoard.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
            @Override
            public void onSystemUiVisibilityChange(int vis) {
                if (0 == (vis & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)) {
                    Nyandroid.this.finish();
                }
            }
        });
    }

    @Override
    public void onUserInteraction() {
//        android.util.Log.d("Nyandroid", "finishing on user interaction");
        finish();
    }
}

Android SDK Tools, Revision 20 から、システムワイドなトレースツールである systrace が追加されました。

このツールを使うと、ユーザランドとカーネルの詳細なトレース結果を得ることができるので、パフォーマンス解析に役立ちます。

 

準備

  • PC に「Android SDK Tools, Revision 20」をインストールする。
  • PC に Python (実行環境)をインストールする。
  • Android 4.1 端末を PC と接続する。
  • 端末上の設定アプリで「開発者オプション」 > 「トレースを有効にする」 で任意の有効なトレースを選択する。

 

systrace の使い方

android-sdk/tools/systrace ディレクトリに移動し、systrace.py を実行する(※)。

※実行するには「python systrace.py」、または、実行権(x)を付加して「./systrace.py」

デフォルトでは 5 秒間トレースされますが、-t でトレース時間(単位は秒)を指定できます。

systrace1.png

 

実行結果

実行すると、trace.html ファイルが生成されるので、Chrome ブラウザ(※)で開きます。

※Firefox では正常に動作しませんでした

 

下図は、「API Demos」 > 「Graphics」 > 「OpenGL ES」 > 「Sprite Text」 を 1 秒間トレースした結果です。

trace.html ファイルは、HTML + Javascript で操作できるようになっていて、

色のついているエリアをダブルクリックすると、拡大できたり、範囲選択すると、下側に詳細な説明が表示されます。

VSYNC 美しいです!

VSYNC 美しいです!

trace.png

 

おまけ

systrace の出力結果には、trace-viewer という Chrome の Javascript ベースなトレースツールを使っているようです。

trace-viewer is the javascript frontend for Chrome's about:tracing UI, and can be used to visualize and analyze traces from various tracing libraries, including the linux kernel and Chrome's base/trace_event system.

また、systrace ツール自体は、python スクリプトで記述されていて、内部的には、adb shell から  atrace を実行しているようです。

 

あわせて読みたい

Android 4.1(Jelly Bean) から Activiy 起動アニメーションが簡単に指定できるようになりました。

 

Activity 起動アニメーション種類

  • スケールアップ(ScaleUpAnimation)
  • サムネイルスケールアップ(ThumbnailScaleUpAnimation)
  • カスタムアニメーション(CustomAnimation)

 

使い方

  1. ActivityOptions クラスの静的メソッド(makeXXX)を呼び出して同クラスをインスタンス化する。
  2. インスタンス化した ActivityOptions を、startActivity の引数に渡す。

 

以下、それぞれのアニメーションの解説です。

 

スケールアップ(ScaleUpAnimation)

画面上の指定された開始位置から、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// アニメーションの基点となるビュー
Button view = (Button) findViewById(R.id.button);

// スケールアップ用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(
    view,
    0, 0, view.getWidth(), view.getHeight());

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

 

サムネイルスケールアップ(ThumbnailScaleUpAnimation)

指定したサムネイル(Bitmap)を指定した位置から、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// アニメーションの基点となるビュー
Button view = (Button) findViewById(R.id.button);

// ここではサンプルとしてスケールアップするアニメーションの画像に
// ボタンの画像(Bitmap)を使用していますが、任意の画像を使用することができます。
view.setDrawingCacheEnabled(true); // 後で元に戻すこと
view.setPressed(false);
view.refreshDrawableState();
Bitmap bitmap = view.getDrawingCache();

// サムネイルスケールアップ用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeThumbnailScaleUpAnimation(
    view,
    bitmap, 0, 0);

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

view.setDrawingCacheEnabled(false);

 

カスタムアニメーション(CustomAnimation)

カスタムアニメーション(リソース)を指定して、新しいアクティビティを拡大するアニメーション。

 

  • ソースコード
// カスタムアニメーション用 ActivityOptions をインスタンス化
ActivityOptions opts = ActivityOptions.makeCustomAnimation(
    MainActivity.this,
    R.anim.zoom_enter, R.anim.zoom_exit);

// アニメーションを指定してアクティビティを起動
startActivity(
    new Intent(this, SecondActivity.class),
    opts.toBundle());

 

  • カスタムアニメーションリソース(サンプル)・・・R.anim.zoom_enter
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/decelerate_interpolator" >
    <scale
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXScale="2.0"
        android:fromYScale="2.0"
        android:pivotX="50%p"
        android:pivotY="50%p"
        android:toXScale="1.0"
        android:toYScale="1.0" />
</set>

 

  • カスタムアニメーションリソース(サンプル)・・・R.anim.zoom_exit
    • <set xmlns:android="http://schemas.android.com/apk/res/android"
          android:interpolator="@android:anim/decelerate_interpolator"
          android:zAdjustment="top" >
          <scale
              android:duration="@android:integer/config_mediumAnimTime"
              android:fromXScale="1.0"
              android:fromYScale="1.0"
              android:pivotX="50%p"
              android:pivotY="50%p"
              android:toXScale=".5"
              android:toYScale=".5" />
          <alpha
              android:duration="@android:integer/config_mediumAnimTime"
              android:fromAlpha="1.0"
              android:toAlpha="0" />
      </set>
      

       

      あわせて読みたい

      Android 4.1(Jelly Bean) から新しく 3 つのスタイルが使用できるようになりました。

      • BigTextStyle
      • BigPictureStyle
      • InboxStyle

      また、通知領域に簡単にボタンを追加できるようにもなっています。

       

      以下、それぞれの画面キャプチャとソースコードです。

      画面キャプチャと、ソースコード中の文字列を比較しながら見ると、わかりやすいと思います^^

       

      Notification.BigTextStyle

      BigTextStyle は「テキスト(Summary Text)」と「小さいアイコン」を表示できます。

       

      • 画面キャプチャ

      Notification.BigTextStyle.png

      •  ソースコード
      // NotificationManager
      NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      
      // Small and large icon
      int smallIconId = R.drawable.ic_action_search;
      Bitmap largeIconBitmap = BitmapFactory.decodeResource(
          getResources(), R.drawable.ic_launcher);
      
      //-----------------------------------------------------------------------
      // BigTextStyle
      //-----------------------------------------------------------------------
      Notification notificationBigTextStyle = new Notification.BigTextStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
          .bigText("BigText")
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(0, notificationBigTextStyle);
      

       

      Notification.BigPictureStyle

      BigPictureStyle は「大きな画像(Big Picture)」を表示できます。

       

      • 画面キャプチャ

      Notification.BigPictureStyle.png

      •  ソースコード
      //-----------------------------------------------------------------------
      // BigPictureStyle
      //-----------------------------------------------------------------------
      Notification notificationBigPictureStyle = new Notification.BigPictureStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
      //  .bigLargeIcon(largeIconBitmap) // Notification.Builder#setLargeIcon() を上書き
          .bigPicture(largeIconBitmap)
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(1, notificationBigPictureStyle);

       

      Notification.InboxStyle

      InboxStyle は、複数行の「テキスト(Line)」、「テキスト(Summary Text)」と「小さいアイコン」を表示できます。

      ※8 行目以降は、"..."表示されます

       

      • 画面キャプチャ

      Notification.InboxStyle.png

      •  ソースコード
      //-----------------------------------------------------------------------
      // InboxStyle
      //-----------------------------------------------------------------------
      Notification notificationInboxStyle = new Notification.InboxStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap))
          .addLine("addLine(1)")
          .addLine("addLine(2)")
          .addLine("addLine(3)")
          .addLine("addLine(4)")
          .addLine("addLine(5)")
          .addLine("addLine(6)")
          .addLine("addLine(7)")
          .addLine("addLine(8)") // 8行目以降は"..."で表示される
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle()  を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(2, notificationInboxStyle);

       

      ボタンを追加する

      Notification 作成時、Notification.Builder#addAction() を呼び出すと、ボタンを追加することができます。

      ※最大3つまで追加できます

      ※ボタン押下時のアクションは、PendingIntent で指定します

       

      • 画面キャプチャ

      NotificationWithButtons.png

       

      • ソースコード
      // PendingIntent
      PendingIntent pendingIntent = PendingIntent.getActivity(this,
          0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
      
      //-----------------------------------------------------------------------
      // BigTextStyle + Button
      //-----------------------------------------------------------------------
      Notification notificationBigTextStyle = new Notification.BigTextStyle(
          new Notification.Builder(this)
              .setContentTitle("ContentTitle")
              .setSmallIcon(smallIconId)
              .setLargeIcon(largeIconBitmap)
              .addAction(android.R.drawable.ic_input_add, "Add", pendingIntent)
              .addAction(android.R.drawable.ic_input_delete, "Delete", pendingIntent))
          .bigText("BigText")
      //  .setBigContentTitle("BigContentTitle") // Notification.Builder#setContentTitle() を上書き
          .setSummaryText("SummaryText")
          .build();
      nm.notify(4, notificationBigTextStyle);
      
      1

      2016年8月

        1 2 3 4 5 6
      7 8 9 10 11 12 13
      14 15 16 17 18 19 20
      21 22 23 24 25 26 27
      28 29 30 31