Oandaのデモ口座における改悪について

1

2021年2月はじめに突然Oandaからメールがきて多くの改悪を行うということでびっくりしました。今までも一方的な変更が突然起こり辟易して来ましたが、今回の変化は大きすぎます。

【重要】デモ口座の利用期限およびオーダーブックインジケーター、APIの利用条件変更について
お客様各位 平素は、弊社のサービスをご利用いただき誠に有難うございます。 この度、デモ口座の利用期限を設けさせていただくとともに、オーダーブックインジケーター、およびAPIのご利用条件を変更させていただくことになりましたので、ご案内いたします。 はじめに、これらの判断に至った経緯は次の通りです。 デモ口座の利用期限の導入の経緯について  これまでデモ口座は期間無制限で使用可能でしたが、過去に作成されたものの、現在は未使用で放置されているデモ口座のデータが蓄積され、膨大なデータ量がサーバへの負荷となっていること、また、商用利用をはじめとして一部で、デモ口座の本来の目的以外で使用しているケースが散見され、サーバに負担をかけているためです。 オーダーブックインジケーター、APIの利用条件の変更の経緯について インジケーター、API提供の本来の目的である弊社との取引以外の目的での使用が多数見受けられ、サーバへの負担が増加しているためです。 いずれも、サーバへの負荷を高め、システム障害発生の原因となり、実際にお取引をしていらっしゃるお客様にご迷惑をおかけする原因の一つです。昨年はこのような事例が実際に発生しておりました。 このため、これらの無制限であったサービスの一部に制限をかけることで、実際にお取引されているお客様へのサービスの品質向上を目指すことこそがお客様にとっても、弊社にとっても健全であると判断し、このような決定に至りました。 何卒、ご理解のほどよろしくお願い申し上げます。 制限対象になるお客様に弊社のサービスをより理解していただくために、移行期間である2021年2月はキャンペーンとして、本番口座を保有しているお客様全員にゴールド会員資格をプレゼントさせていただきます。この機会のゴールド会員のサービスをお試しください。
デモ口座の利用期限について
本年2月1日からは、デモ口座の利用期限を当該口座開設後30日間とさせていただきます。 ただし、以下の条件を満たすお客様は継続してデモ口座をご利用いただけます。1. 本番口座ステータスがゴールド(Gold)であること。2. 本番口座のマイページから、種類ごとに1つのデモ口座を登録すること。 利用期限を経過、またはゴールド会員を失ったことによりデモ口座の利用権を失った口座はロックされます。(ログインできません。) ロックの解除にはロック後60日以内にゴールド会員のステータスに昇格(回復)後、再び本番口座のマイページより、当該デモ口座の登録を行うことでロックを解除することができます。(過去に登録していた口座に関しても再登録が必要です。) なお、60日以内にロックの解除が行われなかったデモ口座は閉鎖され、復元することはできませんのでご注意ください。
移行期間2021年2月1日より、デモ口座の利用期限を設定させていただきます。既存のデモ口座につきましては、利用期限を2021年2月28日までとさせていただきます。 ゴールド会員の方でデモ口座を継続使用される場合は2月末までにマイページより必ずご登録願います。(ご登録は2021年2月2日以降行うことができます。)

30日と言っていますが、その後、2月28日を待たずにOandaのデモ口座ではAPIも使えなくなりました。今までも一方的な変更が突然起こり辟易して来ましたが、今回の変化は大きすぎますね。。

GitHubのREADME.mdをグラフ入りで効率的に書く方法

GitHubのREADME.mdをグラフ入りで効率的に書く方法です。

  • VSCodeでMarkdown Preview Enhancedを入れておく
  • 普通にREADME.mdを作成する
  • 文章はここに書く
  • グラフはグラフごとに.mdファイルを生成して、Markdown Preview Enhancedに入っているmermaid.jsを使ってグラフを書く
  • それをJPEGやPNGとしてエクスポートする
  • その画像をREADME.mdから読み込む

これが一番早そうです。

Ionic(Angular)でAdmobを導入する方法(iOS編)

1

Ionic(Angular)でAdmobを導入する方法です。こちらはiOSでの実装となります。Androidでの実装方法はこちら。

ポイント

  • capacitor-admobプラグインを使用します
  • 公式の方法+Plugin.swiftの編集が必要です(この記事に方法あります)

流れ

  1. npmでcapacitor-admobをインストールし、info.plistを編集します
  2. そのままだとエラーが起こるので、Plugin.swiftを修正します
  3. app.component.tsでアプリ情報を初期化してから読み込みたいモジュールで広告呼び出します

動作環境

Ionic4, 5, 6でiOS13から14.3で確認しています。

参考文献

https://github.com/rahadur/capacitor-admob

https://developers.google.com/admob/android/test-ads?hl=ja

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

作成方法はこちら。


見た目はこんな感じ。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装する

1. ライブラリをインストールする

npmでcapacitor-admobをインストールします。

 npm install --save capacitor-admob

2. iOSのプロジェクトを生成する

iOSのプロジェクトを生成するために下記コマンドを打ちます。capacitorを入れていない方はcapacitorを入れてください。

ionic build
npx cap add ios
npx cap sync
npx cap open ios

3. info.plistを編集する

info.plistとは、iOSアプリの設定情報などを記入するものです。インフォプロパティリストと読みます。下記コマンドでXcodeを開きます。

npx cap open ios

info.plistを選択します。

このファイルを編集するのですが、編集するにはコツが入ります、info.plistの上で右クリックをして、open as > source codeを選択して編集できるようにします。

そして、下記のスニペットを下の方に貼り付けます。自分のアプリIDに変更するのを忘れないでください。

    <key>GADIsAdManagerApp</key>
    <true/>
    <key>GADApplicationIdentifier</key>
    <!-- replace this value with your App ID key-->
    <string>ca-app-pub-7171076285090910~XXXXXXXX</string>

もし場所がわからない方がいたらinfo.plistを丸ごと貼り付けるので参考にしてください。下から3行目から8行目までが貼り付けたものです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
        <string>admob-master</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLName</key>
			<string>com.getcapacitor.capacitor</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>capacitor</string>
			</array>
		</dict>
	</array>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>
	<key>NSCameraUsageDescription</key>
	<string>To Take Photos and Video</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>Always allow Geolocation?</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>Allow Geolocation?</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>To Record Audio With Video</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>Store camera photos to camera</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>To Pick Photos from Library</string>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>armv7</string>
	</array>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UIViewControllerBasedStatusBarAppearance</key>
	<true/>
    
    <key>GADIsAdManagerApp</key>
    <true/>
    <key>GADApplicationIdentifier</key>
    <!-- replace this value with your App ID key-->
    <string>ca-app-pub-7171076285090910~3362455272</string>
    
</dict>
</plist>

5. 公式にはないエラー処理のコードを書く

公式ではこれで完了ということですが、これだとエラーが起きます。これでエミュレータを起動すると下記のエラーが出ます。

右上を見ると赤い背景で9というエラーが表示されています。その右下の別の赤いボタンからエラーが起きている「Plugin.swift」へ移動します。

そうするとそれぞれのエラーの詳細が確認できます。

よく見るとどれも同じメッセージのようです。こう書いてあります、

Method 'interstitialDidReceiveAd' must be declared public because it matches a requirement in public protocol 'GADInterstitialDelegate'

publicで宣言せよと書いてあるのでそうします。

修正前

    private func interstitialDidReceiveAd(_ ad: GADInterstitial) {
        print("interstitialDidReceiveAd")
        self.notifyListeners("onAdLoaded", data: ["value": true])
    }

修正後

    public func interstitialDidReceiveAd(_ ad: GADInterstitial) {
        print("interstitialDidReceiveAd")
        self.notifyListeners("onAdLoaded", data: ["value": true])
    }

合計9箇所同じように変えると、ビルドできるようになるはずです。

広告を表示する

以上で広告を表示するためのプラグインの設定が終わったので、いよいよ広告をアプリで表示していきます。

まず、app.component.tsでアプリケーションを初期化します。

import { Plugins } from "@capacitor/core";
// import { initialize } from 'capacitor-admob'; No longar required

const { AdMob } = Plugins;

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
  styleUrls: ["app.component.scss"]
})
export class AppComponent {
  constructor() {
    // Initialize AdMob for your Application
    AdMob.initialize("YOUR APPID");
  }
}

次に、読み込みたいページで任意の広告を呼び出します。ここでは、tab1でバナー広告、tab2でインタースティシャル広告を呼び出します。

テストIDを使ってください。

https://developers.google.com/admob/android/test-ads?hl=ja

tab1でバナー広告を呼び出す

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions, AdSize, AdPosition } from "capacitor-admob";

const { AdMob } = Plugins;
@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/6300978111",
    adSize: AdSize.BANNER,
    position: AdPosition.BOTTOM_CENTER
  };

  constructor() {
    // Show Banner Ad
    AdMob.showBanner(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      console.log("Banner Ad Loaded");
    });
  }

}

tab2でインタースティシャルを表示する

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions } from "capacitor-admob";
const { AdMob } = Plugins;

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/1033173712",
    hasTabBar: false, // make it true if you have TabBar Layout.
    tabBarHeight: 56 // you can assign custom margin in pixel default is 56
  };

  constructor() {
    // Prepare interstitial banner
    AdMob.prepareInterstitial(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      // You can call showInterstitial() here or anytime you want.
      console.log("Interstitial Ad Loaded");
      // Show interstitial ad when it’s ready

      AdMob.showInterstitial().then(
        value => {
          console.log(value); // true
        },
        error => {
          console.error(error); // show error
        }
      );

    });
  }
}

時間を空ける

Admobの仕様で広告が表示されるまでに時間が必要です。

設定が正しくされていても最初は

  • Ad failed to load : 2
  • Ad failed to load : 0

などが表示されて広告が表示されません。これは時間をおけば解決なので少し忍耐が必要です。

その他何かうまくいかないときは下記を実行することをお勧めします。

  • ionic build –prodでプロダクションモードでビルドする
  • npx cap sync ios
  • npx cap copy

終わりに

あー記事まとめるのめっちゃ大変だった!お疲れ様でした。

Ionic(Angular)でAdmobを導入する方法(Android編)

1

Ionic(Angular)でAdmobを導入する方法です。こちらはAndroidでの実装となります。iOS版はこちら。

ポイント

  • capacitor-admobプラグインを使用します
  • 公式の方法+依存関係の解決(この記事に方法あります)が必要です

流れ

  1. npmでcapacitor-admobをインストールします
  2. そのままだとエラーが起こるので、依存関係を解決します
  3. エミュレータで正しくアプリが立ち上がり依存関係が解決されていることを確認したのち、app.component.tsでアプリ情報を初期化してから読み込みたいモジュールで広告呼び出します

動作環境

Ionic4, 5, 6でAndroid SDKはAPIレベル29(Android X)で検証。28以下でもいけました。

参考文献

https://github.com/rahadur/capacitor-admob

https://developers.google.com/admob/android/test-ads?hl=ja

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装する

1. ライブラリをインストールする

npmでcapacitor-admobをインストールします。

 npm install --save capacitor-admob

2. Androidのプロジェクトを生成する

Androidのプロジェクトを生成するために下記コマンドを打ちます。capacitorを入れていない方はcapacitorを入れてください。

ionic build
npx cap add android
npx cap sync
npx cap open android

3. AndroidManifest.xmlを編集する

Androidのマニフェストファイルはアプリに機能を追加するときに編集するものです。Android Studioではデフォルトで作成されるものとなります。

ここからどのファイルを編集するか分かりにくいので詳細に記載します。非常にここは苦労しました。

デフォルトでは下記のようにAndroidが選択されていると思います。

ドリルダウンを選択してProjectを選択します

この状態にまずしてください。

マニフェストファイルはProjectのandroid/app/src/main/AndroidManifest.xmlにあります。

自分のAdmobのアプリIDに編集する必要がありますが、下記のように変更します。

<application>
  <!-- this line needs to be added (replace the value!) -->
         <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-7171076285090910~XXXXXXXX" />
  <activity></activity>
</application>

つまり、編集した後のファイルは下記のようになります。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.ionic.starter">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-7171076285090910~XXXXXXXXX" />

        <activity
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
            android:name="io.ionic.starter.MainActivity"
            android:label="@string/title_activity_main"
            android:theme="@style/AppTheme.NoActionBarLaunch"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <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:scheme="@string/custom_url_scheme" />
            </intent-filter>

        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"></meta-data>
        </provider>
    </application>

    <!-- Permissions -->

    <uses-permission android:name="android.permission.INTERNET" />
    <!-- Camera, Photos, input file -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- Geolocation API -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-feature android:name="android.hardware.location.gps" />
    <!-- Network API -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- Navigator.getUserMedia -->
    <!-- Video -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- Audio -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
</manifest>

4. MainActivity.javaを編集する

admobをandroidに登録します。

Projectを選択した状態で、Android>app>src>main>java><your original name>>MainActivityを選択します。

そして、このように変更します。

// Other imports...
import app.xplatform.capacitor.plugins.AdMob;

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{

      add(AdMob.class);  // Add AdMob as a Capacitor Plugin

    }});
  }
}

つまり、このようにします。

package io.ionic.starter;

import android.os.Bundle;

import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import java.util.ArrayList;

import app.xplatform.capacitor.plugins.AdMob;

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Initializes the Bridge
    this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
      // Additional plugins you've installed go here
      // Ex: add(TotallyAwesomePlugin.class);
      add(AdMob.class);  // Add AdMob as a Capacitor Plugin

    }});
  }
}

5. 公式にはないエラー処理のコードを書く

公式ではこれで完了ということですが、これだとエラーが起きます。これでエミュレータを起動すると下記のエラーが出ます。

/Users/masaya/Desktop/MyGoodness/admob-master/admob-master/node_modules/capacitor-admob/android/src/main/java/app/xplatform/capacitor/plugins/AdMob.java:4: エラー: パッケージandroid.support.design.widgetは存在しません
import android.support.design.widget.CoordinatorLayout;

そこで、焦らずに下記の対応を行います。

https://github.com/rahadur/capacitor-admob/issues/35

Admob.javaの編集

まず、エラーが出ているAdmob.javaのファイルのエラー発生している行を下記に変更します。

import androidx.coordinatorlayout.widget.CoordinatorLayout;

前のものはコメントアウトでもしておいてこのようにします。

そして、capacito-admobのbuild.gradleのdependenciesの中にimplementation ‘androidx.coordinatorlayout:coordinatorlayout:1.1.0’を追加します。

implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'

編集するファイルを間違えるとドツボにハマるので注意。

これでサイドエミュレータを立ち上げると下記のエラーが出ます。

2020-12-21 15:44:03.866 9236-9236/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: io.ionic.starter, PID: 9236
    java.lang.RuntimeException: Unable to start activity ComponentInfo{io.ionic.starter/io.ionic.starter.MainActivity}: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
     Caused by: android.view.InflateException: Binary XML file line #2 in io.ionic.starter:layout/bridge_layout_main: Error inflating class android.support.design.widget.CoordinatorLayout
     Caused by: java.lang.ClassNotFoundException: android.support.design.widget.CoordinatorLayout
        at java.lang.Class.classForName(Native Method)
        at java.lang.Class.forName(Class.java:454)
        at android.view.LayoutInflater.createView(LayoutInflater.java:815)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:659)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:555)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
        at com.getcapacitor.BridgeActivity.init(BridgeActivity.java:60)
        at com.getcapacitor.BridgeActivity.init(BridgeActivity.java:48)
        at io.ionic.starter.MainActivity.onCreate(MainActivity.java:17)
        at android.app.Activity.performCreate(Activity.java:7802)
        at android.app.Activity.performCreate(Activity.java:7791)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
2020-12-21 15:44:03.866 9236-9236/? E/AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "android.support.design.widget.CoordinatorLayout" on path: DexPathList[[zip file "/data/app/io.ionic.starter-wIOzWpenwsyjIPlSvEtd-g==/base.apk"],nativeLibraryDirectories=[/data/app/io.ionic.starter-wIOzWpenwsyjIPlSvEtd-g==/lib/x86, /system/lib, /system/product/lib]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        	... 28 more

そこで、この辺を解決してくれるように下記のライブラリを導入します。

terminalで下記を打ちます。

npm install jetifier
npx jetify
npx cap sync android

これで再度エミュレータを起動すると、ようやく無事に立ち上がるようになります。これで依存関係が無事に解決されました。

広告を表示する

以上で広告を表示するためのプラグインの設定が終わったので、いよいよ広告をアプリで表示していきます。

まず、app.component.tsでアプリケーションを初期化します。

import { Plugins } from "@capacitor/core";
// import { initialize } from 'capacitor-admob'; No longar required

const { AdMob } = Plugins;

@Component({
  selector: "app-root",
  templateUrl: "app.component.html",
  styleUrls: ["app.component.scss"]
})
export class AppComponent {
  constructor() {
    // Initialize AdMob for your Application
    AdMob.initialize("YOUR APPID");
  }
}

次に、読み込みたいページで任意の広告を呼び出します。ここでは、tab1でバナー広告、tab2でインタースティシャル広告を呼び出します。

テストIDを使ってください。

https://developers.google.com/admob/android/test-ads?hl=ja

tab1でバナー広告を呼び出す

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions, AdSize, AdPosition } from "capacitor-admob";

const { AdMob } = Plugins;
@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/6300978111",
    adSize: AdSize.BANNER,
    position: AdPosition.BOTTOM_CENTER
  };

  constructor() {
    // Show Banner Ad
    AdMob.showBanner(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      console.log("Banner Ad Loaded");
    });
  }

}

tab2でインタースティシャルを表示する

import { Component } from '@angular/core';
import { Plugins } from "@capacitor/core";
import { AdOptions } from "capacitor-admob";
const { AdMob } = Plugins;

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  options: AdOptions = {
    adId: "ca-app-pub-3940256099942544/1033173712",
    hasTabBar: false, // make it true if you have TabBar Layout.
    tabBarHeight: 56 // you can assign custom margin in pixel default is 56
  };

  constructor() {
    // Prepare interstitial banner
    AdMob.prepareInterstitial(this.options).then(
      value => {
        console.log(value); // true
      },
      error => {
        console.error(error); // show error
      }
    );

    // Subscibe Banner Event Listener
    AdMob.addListener("onAdLoaded", (info: boolean) => {
      // You can call showInterstitial() here or anytime you want.
      console.log("Interstitial Ad Loaded");
      // Show interstitial ad when it’s ready

      AdMob.showInterstitial().then(
        value => {
          console.log(value); // true
        },
        error => {
          console.error(error); // show error
        }
      );

    });
  }
}

時間を空ける

Admobの仕様で広告が表示されるまでに時間が必要です。

設定が正しくされていても最初は

  • Ad failed to load : 2
  • Ad failed to load : 0

などが表示されて広告が表示されません。これは時間をおけば解決なので少し忍耐が必要です。

その他何かうまくいかないときは下記を実行することをお勧めします。

  • ionic build –prodでプロダクションモードでビルドする
  • npx cap sync android
  • npx cap copy

終わりに

あー記事まとめるのめっちゃ大変だった。お疲れ様でした。

Angularで画像をトリミングする方法

1

Angularで読み込んだ画像をトリミングできるようにする方法です。

ポイント

  • ngx-image-cropperを使います

流れ

  1. ngx-image-cropperをnpmでinstallします
  2. 読み込みたい場所でngx-image-cropperをmoduleに登録します
  3. その場所のHTMLとTSファイルを編集します

環境

Ionic 4, 5, 6で動作検証しています。

ソースコード

GitHubに上げました。

https://github.com/NP-Systems/Ionic_tips/tree/id1137-imageCropper

参考文献

https://www.npmjs.com/package/ngx-image-cropper

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
├── tabs-routing.module.ts
├── tabs.module.ts
├── tabs.page.html
├── tabs.page.scss
├── tabs.page.spec.ts
└── tabs.page.ts

実装方法

それではTab1ページに実装していきます。

インストール

プロジェクトのフォルダに移動して下記コマンドでインストールします。

npm install ngx-image-cropper --save

モジュールへの登録

今回はtab1に実装することにするので、tab1.module.tsに下記のようにします。

import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { ImageCropperModule } from 'ngx-image-cropper';

import { Tab1PageRoutingModule } from './tab1-routing.module';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    ExploreContainerComponentModule,
    Tab1PageRoutingModule,
    ImageCropperModule
  ],
  declarations: [Tab1Page]
})
export class Tab1PageModule {}

import { ImageCropperModule } from ‘ngx-image-cropper’;を追加して、

@NgModuleのimportsにImageCropperModuleを追加します。

HTMLとTSファイルの編集

上記で準備は完了なので、HTMLとTSファイルの編集を行います。

tab1.page.htmlに下記のように追加します、

<input type="file" (change)="fileChangeEvent($event)" />

<image-cropper
    [imageChangedEvent]="imageChangedEvent"
    [maintainAspectRatio]="true"
    [aspectRatio]="4 / 3"
    format="png"
    (imageCropped)="imageCropped($event)"
    (imageLoaded)="imageLoaded()"
    (cropperReady)="cropperReady()"
    (loadImageFailed)="loadImageFailed()"
></image-cropper>

<img [src]="croppedImage" />

tab1.page.tsを下記のようにします。

import { Component } from '@angular/core';
import { ImageCroppedEvent } from 'ngx-image-cropper';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
    imageChangedEvent: any = '';
    croppedImage: any = '';

    constructor(
    ) {}

    fileChangeEvent(event: any): void {
        this.imageChangedEvent = event;
    }
    imageCropped(event: ImageCroppedEvent) {
        this.croppedImage = event.base64;
    }
    imageLoaded(image: HTMLImageElement) {
        // show cropper
    }
    cropperReady() {
        // cropper ready
    }
    loadImageFailed() {
        // show message
    }

}

応用編

以上が基礎ですが、ここからは切り取った画像を新たに別の画像をとして読み込む方法について記載します。

まず、HTMLに切り取った後に押下するためのボタンを配置します。

tab1.page.htmlの当該部分を下記のように変更します。

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Tab 1</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-item>
    <input type="file" accept='image/*'  (change)="fileChangeEvent($event)" />
  </ion-item>

  <ion-progress-bar type="indeterminate" *ngIf="show" reversed="true"></ion-progress-bar>
  <!--プログレスバーの追加です-->

  <image-cropper
  [imageChangedEvent]="imageChangedEvent"
  [maintainAspectRatio]="false"
  [aspectRatio]="5/ 3"
  format="jpeg"
  (imageCropped)="imageCropped($event)"
  (imageLoaded)="imageLoaded()"
  (cropperReady)="cropperReady()"
  (loadImageFailed)="loadImageFailed()"
  imageQuality=25
></image-cropper>

  <img [src]="croppedImage" />

  <ion-button expand='block' (click)="onButtonClicked()" shape='round'>Get cropped image</ion-button>

</ion-content>

そして、tsファイルで追加で下記2つのモジュールを読み込みます。

import { LoadingController } from '@ionic/angular';
import { Observable } from 'rxjs';

'
'
  constructor(
    public loadingController: LoadingController,
  ) {}

そして、tsファイルの変数として下記を追加します。

export class Tab1Page {
  imageChangedEvent: any = '';
  originalImgName:string;
  buffCroppedImageBase64:string;
  croppedImageBase64: any = '';
  croppedFileObject:any;
  show:any=false;
  • imageChangedEventは、画像の選択範囲が変わったら都度呼び出されるイベントに関する情報を保持するものです。
  • originalImgNameは、inputタグで読み込んだ画像から元のファイル名を取得できるのでそれを格納する変数です
  • buffCroppedImageBase64は、切り取った画像(base64です)を格納する一次ファイルです。HTML側で<img [src]=”croppedImageBase64″ />と定義していますが、ユーザーが選んでいる時に都度croppedImageBase64″に入れると切り取った画像がチラチラしてしまうので、一時的に格納するものとしてbuffCroppedImageBase64を用意しました。
  • CroppedImageBase64は最終的な結果を入れるものです。ここにBase64の画像を入れると、HTML側で表示されます
  • croppedFileObjectは、切り取った画像を別のファイルとして読み込むのに使用します。
  • showはProgressバーを制御するものです

fileChangeEventの部分を下記のように拡張します。

  fileChangeEvent(event: any): void {
    this.show = true;
    this.read(event).subscribe(()=>{
      console.log('done');
      this.show = false;
    });
  }

  read(event: any):Observable<any> {
    return new Observable(observer => {
      setTimeout(() => {
        this.imageChangedEvent = event;
        this.originalImgName = event.target.files[0].name;
        observer.next('e');
      }, 500);
    })
  }

重い画像だと読み込むのに数秒かかってしまいます。そこで、読み込み開始したらprogressバーで動作していることを知らせています、500は使った感触で決めました。

imageCroppedの関数を下記のように変えます。

  imageCropped(event: ImageCroppedEvent) {
      this.buffCroppedImageBase64 = event.base64;
  }

先ほど行ったように一度Bafferの方に切り取った画像を入れています。

最後にonButtonClickedの関数を下記のように変更します。


  onButtonClicked(){
    this.croppedImageBase64 = this.buffCroppedImageBase64;
    console.log(this.croppedImageBase64,'this.croppedImageBase64');

    var bin = atob(this.buffCroppedImageBase64.replace(/^.*,/, ''));
    // バイナリデータ化
    var buffer = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
    }
    this.croppedFileObject = new File([buffer.buffer], "heihei.jpg",{type: "image/jpeg"});
    console.log(this.croppedFileObject,'this.croppedFileObject');
  }

最終的に下記にようにします。

import { Component } from '@angular/core';
import { ImageCroppedEvent } from 'ngx-image-cropper';
import { LoadingController } from '@ionic/angular';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  imageChangedEvent: any = '';
  originalImgName:string;
  buffCroppedImageBase64:string;
  croppedImageBase64: any = '';
  show:any=false;

  constructor(
    public loadingController: LoadingController,
  ) {}

  fileChangeEvent(event: any): void {
    console.log(event,'abcde');
    this.show = true;
    this.read(event).subscribe(()=>{
      console.log('done');
      this.show = false;
    });
  }

  read(event: any):Observable<any> {
    return new Observable(observer => {
      setTimeout(() => {
        this.imageChangedEvent = event;
        this.originalImgName = event.target.files[0].name;
        observer.next('e');
      }, 500);
    })
  }


  imageCropped(event: ImageCroppedEvent) {
      this.buffCroppedImageBase64 = event.base64;
  }
  imageLoaded(image: HTMLImageElement) {
      // show cropper
  }
  cropperReady() {
      // cropper ready
  }
  loadImageFailed() {
      // show message
  }

  onButtonClicked(){
    this.croppedImageBase64 = this.buffCroppedImageBase64;
    console.log(this.croppedImageBase64,'this.croppedImageBase64');

    var bin = atob(this.buffCroppedImageBase64.replace(/^.*,/, ''));
    // バイナリデータ化
    var buffer = new Uint8Array(bin.length);
    for (var i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i);
    }
    this.croppedFileObject = new File([buffer.buffer], "heihei.jpg",{type: "image/jpeg"});
    console.log(this.croppedFileObject,'this.croppedFileObject');
  }


}

Ionic(Angular)でアコーディオンを実装する方法

1

Ionic(Angular)でアコーディオンを実装する方法です。

ポイント

  • Componentを自作して、それを読み込むようにします
  • これによりいろいろな場所で使い回しができるようになります

結果

こんな感じでクリックするとカードが広がるようになります。

環境

Ionic 4, 5, 6で動作検証しています。

流れ

  1. コンポーネントを作成し、HTMLとSCSSを記載します
  2. 読み込みたい場所で作成したコンポーネントをmoduleに登録します
  3. 使いたい場所のHTMLでapp-expandableタグで呼び出します

ソースコード

GItHubに上げました。

https://github.com/NP-Systems/Ionic_tips/tree/id1131-createAccordion/frontend

参考文献

https://www.joshmorony.com/creating-an-accordion-list-in-ionic/

初期状態

IonicのTabsのデフォルトテンプレートから始めます。

appフォルダ以下の構成は下記のようになっています。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
    ├── tabs-routing.module.ts
    ├── tabs.module.ts
    ├── tabs.page.html
    ├── tabs.page.scss
    ├── tabs.page.spec.ts
    └── tabs.page.ts

自作コンポーネントの作成

下記のコマンドで自作コンポーネントを作成します。

% ionic g component components/Expandable

app.module.tsと同じ階層にcomponentsフォルダができて新しいコンポーネントが生成されます。

.
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── components
│   └── expandable
│       ├── expandable.component.html
│       ├── expandable.component.scss
│       ├── expandable.component.spec.ts
│       └── expandable.component.ts
├── explore-container
│   ├── explore-container.component.html
│   ├── explore-container.component.scss
│   ├── explore-container.component.spec.ts
│   ├── explore-container.component.ts
│   └── explore-container.module.ts
├── tab1
│   ├── tab1-routing.module.ts
│   ├── tab1.module.ts
│   ├── tab1.page.html
│   ├── tab1.page.scss
│   ├── tab1.page.spec.ts
│   └── tab1.page.ts
├── tab2
│   ├── tab2-routing.module.ts
│   ├── tab2.module.ts
│   ├── tab2.page.html
│   ├── tab2.page.scss
│   ├── tab2.page.spec.ts
│   └── tab2.page.ts
├── tab3
│   ├── tab3-routing.module.ts
│   ├── tab3.module.ts
│   ├── tab3.page.html
│   ├── tab3.page.scss
│   ├── tab3.page.spec.ts
│   └── tab3.page.ts
└── tabs
    ├── tabs-routing.module.ts
    ├── tabs.module.ts
    ├── tabs.page.html
    ├── tabs.page.scss
    ├── tabs.page.spec.ts
    └── tabs.page.ts

tab1.module.tsでの読み込み

今回はtab1ページに埋め込むことにするので、tab1.module.tsに下記のように登録します。

import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Tab1Page } from './tab1.page';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { ExpandableComponent } from "../components/expandable/expandable.component";

import { Tab1PageRoutingModule } from './tab1-routing.module';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    ExploreContainerComponentModule,
    Tab1PageRoutingModule
  ],
  declarations: [Tab1Page,ExpandableComponent]
})
export class Tab1PageModule {}

expandable.component.htmlの変更

 src/components/expandable/expandable.component.htmlのファイルを下記のように変更します。

<p>
  expandable works!
</p>

<div #expandWrapper class='expand-wrapper' [class.collapsed]="!expanded">
    <ng-content></ng-content>
</div>

expandable.component.scssの変更

.expand-wrapper {
  transition: max-height 0.4s ease-in-out;
  overflow: hidden;
  height: auto;
}

.collapsed {
  max-height: 0 !important;
}

expandable.component.tsの変更

Before

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-expandable',
  templateUrl: './expandable.component.html',
  styleUrls: ['./expandable.component.scss'],
})
export class ExpandableComponent implements OnInit {

  constructor() { }

  ngOnInit() {}

}

After

import { Component, AfterViewInit, Input, ViewChild,  ElementRef, Renderer2 } from "@angular/core";

@Component({
  selector: "app-expandable",
  templateUrl: "./expandable.component.html",
  styleUrls: ["./expandable.component.scss"]
})
export class ExpandableComponent implements AfterViewInit {
  @ViewChild("expandWrapper", { read: ElementRef }) expandWrapper: ElementRef;
  @Input("expanded") expanded: boolean = false;
  @Input("expandHeight") expandHeight: string = "150px";

  constructor(public renderer: Renderer2) {}

  ngAfterViewInit() {
    this.renderer.setStyle(this.expandWrapper.nativeElement, "max-height", this.expandHeight);
  }
}

tab1.page.htmlの変更

Before

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Tab 1
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Tab 1</ion-title>
    </ion-toolbar>
  </ion-header>

  <app-explore-container name="Tab 1 page"></app-explore-container>
</ion-content>

After

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Tab 1
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card (click)="expandItem(item)" *ngFor="let item of items">
    <ion-card-header>
      <ion-card-title>隣のトトロ</ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <app-expandable expandHeight="100px" [expanded]="item.expanded">
        <p>
          とっとろ、と、とーろ
        </p>
        <p>
          とっとろ、と、とーろ
        </p>
      </app-expandable>
    </ion-card-content>
  </ion-card>
</ion-content>

tab1.page.tsの変更

Before

import { Component } from '@angular/core';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {

  constructor() {}

}

After

import { Component } from '@angular/core';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  public items: any = [];

  constructor() {
    this.items = [
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false },
      { expanded: false }
    ];
  }

  expandItem(item): void {
    if (item.expanded) {
      item.expanded = false;
    } else {
      this.items.map(listItem => {
        if (item == listItem) {
          listItem.expanded = !listItem.expanded;
        } else {
          listItem.expanded = false;
        }
        return listItem;
      });
    }
  }
}