カテゴリー
Android Java

RelativeLayoutを使ってCircle Viewを作る

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

    int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;

    LinearLayout layout = new LinearLayout(this);
    layout.setGravity(Gravity.CENTER);
    setContentView(layout);

    RelativeLayout circleView = new RelativeLayout(this);
    GradientDrawable gd = new GradientDrawable();
    gd.setSize(screenHeight/3, screenHeight/3);
    gd.setColor(Color.WHITE);
    gd.setStroke(10, Color.GRAY);
    gd.setCornerRadius(screenHeight/3/2);
    circleView.setBackground(gd);
    layout.addView(circleView);

    TextView tvTop = new TextView(this);
    tvTop.setText("Top");
    tvTop.setTextSize(50);
    RelativeLayout.LayoutParams tvTopLP = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    tvTopLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
    tvTopLP.addRule(RelativeLayout.CENTER_HORIZONTAL);
    tvTopLP.topMargin = screenHeight / 3 / 5;
    tvTop.setLayoutParams(tvTopLP);

    circleView.addView(tvTop);

    View divider = new View(this);
    divider.setBackgroundColor(Color.GRAY);
    RelativeLayout.LayoutParams dividerLP = new RelativeLayout.LayoutParams(screenHeight / 3 / 10 * 9, 3);
    dividerLP.addRule(RelativeLayout.CENTER_IN_PARENT);
    divider.setLayoutParams(dividerLP);

    circleView.addView(divider);

    TextView tvBottom = new TextView(this);
    tvBottom.setText("Bottom");
    tvBottom.setTextSize(50);
    RelativeLayout.LayoutParams tvBottomLP = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    tvBottomLP.addRule(RelativeLayout.ALIGN_PARENT_TOP); // when set to ALIGN_PARENT_TOP, the layout will break
    tvBottomLP.addRule(RelativeLayout.CENTER_HORIZONTAL);
    tvBottomLP.topMargin = screenHeight / 3 / 5 * 3;
    tvBottom.setLayoutParams(tvBottomLP);

    circleView.addView(tvBottom);

    getSupportActionBar().hide();
  }
}
カテゴリー
Android Java

AndroidのTextViewの設定

TextView textView = new TextView(this);

String string = "Some String";

// to make it bold (or other styling)
SpannableString spanString = new SpannableString(string);
spanString.setSpan(new StyleSpan(Typeface.BOLD), 0, spanString.length(), 0);

// to allow autosize of texts
textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM);

textView.setText(spanString);
textView.setTextColor(Color.BLUE);

Typeface appFont = getResources().getFont(R.font.oswald); // oswald
textView.setTypeface(appFont);

// centering texts
textView.setGravity(Gravity.CENTER);

// padding
textView.setPadding(10, 10, 10, 10);

// minimum width and hight
textView.setMinimumWidth(20);
textView.setMinimumHeight(20);

fontをAndroid Studioに追加するには

resフォルダ内にfontフォルダを作り、その中に.ttfファイルを置く

Typeface appFont = getResources().getFont(R.font.oswald);
カテゴリー
Android Java

Android ActivityのLifeCycle

// app launch
onCreate called
onStart called
onResume called

// app screen rotated
onResume called
onStop called
onDestroy called
onCreate called
onStart called
onResume called

// app rotated again
onResume called
onStop called
onDestroy called
onCreate called
onStart called
onResume called
カテゴリー
Android Java

Androidで背景色のTransition

public class MainActivity extends AppCompatActivity {
  Button button;
  LinearLayout layout;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    layout = new LinearLayout(this);
    layout.setGravity(Gravity.CENTER);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);

    layout.setLayoutParams(params);

    button = new Button(this);
    button.setOnClickListener(new OnButtonClicked());
    layout.addView(button);
    setContentView(layout);
  }

  private class OnButtonClicked implements View.OnClickListener {
    @Override
    public void onClick(View v) {
      layout.setBackgroundColor(Color.RED);
      ColorDrawable[] colors = {new ColorDrawable(Color.RED), new ColorDrawable(Color.WHITE)};
      TransitionDrawable transition = new TransitionDrawable(colors);
      layout.setBackground(transition);
      transition.startTransition(2000);
      Log.d("MyTest", "clicked");
    }
  }
}
カテゴリー
Flutter

FlutterでCircle Buttonを作る

Material(
  shape: CircleBorder(
    side: BorderSide(color: Constants.lineColor, width: Constants.lineWidth),
  ),
  clipBehavior: Clip.hardEdge,
  color: Constants.appBackgroundColor,
  child: IconButton(
    padding: EdgeInsets.zero,
    onPressed: () {},
    iconSize: areaHeight*0.8,
    color: Constants.playButtonColor,
    icon: Icon(
      Icons.play_arrow_rounded,
    ),
  )
)
カテゴリー
Flutter

Flutterの最小実装

// This gives below error
// No Directionality widget found. RichText widgets require a Directionality widget ancestor. The specific widget that could not find a Directionality ancestor was: 
// RichText [RichText
// The ownership chain for the affected widget is: "RichText <- Text <- [root]"
// Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the top of your application widget tree. It determines the ambient reading direction and is used, for example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve EdgeInsetDirectional, AlignmentDirectional, and other *Directional objects. See also: https://flutter.dev/docs/testing/errors

void main() => runApp(Text("Hello"));

so, below can be a minimum implementation of a Flutter application.

void main() {
  runApp(
    const Center(
      child: Text(
        "Hello world",
        textDirection: TextDirection.ltr,
    ),
  );
}

In the Flutter documentation:

The runApp() function takes the given Widget and makes it the root of the widget tree. この例ではCenterがroot.

In this example, the widget tree consists of two widgets, the Center widget and its child, the Text widget.

The framework forces the root widget to cover the screen, which means the text “Hello, world” ends up centered on screen. Flutterではroot widgetがscreen全体をcoverするので、”Hello World”はscreenのcenterに来る。

The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later. root にCenter widgetを使う場合はtextDirection プロパティを指定する必要があるが、MatrialAppを使う場合は不要。

When writing an app, you’ll commonly author new widgets that are subclasses of either StatelessWidget or StatefulWidget, depending on whether your widget manages any state. 多くの場合StatelessWidgetもしくはStatefulWidget(選択はstateをどう管理するかによる)を継承してカスタムwidgetを作ることになる。

A widget’s main job is to implement a build() function, which describes the widget in terms of other, lower-level widgets. The framework builds those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject, which computes and describes the geometry of the widget.

in order to activate “hot reload”, embed a widget in the build method

void main() => runApp(MaterialApp(
  home: Home(),
));

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Image(
          image: AssetImage("assets/girl.jpg"),
        ),
      ),
    );
  }
}

in order to let Flutter know where the assets directory is, write in pubspec.yaml the assets directory location from the root.

assets:
  - assets/
カテゴリー
Laravel PHP StoreKit2

StoreKitのTransaction HistoryをPHP LaravelでDecodeする

function verifyPurchaseAppleStoreKit2(Request $request) {

  $sandbox = true;

  $transactionID = $request->query("transactionID");

  $url = "https://api.storekit.itunes.apple.com/inApps/v1/history/$transactionID";
  if ($sandbox) {
    $url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/$transactionID";
  }

  $handle = curl_init($url);
  $token = $this->generateJWTForSigningApplePurchase();
  curl_setopt($handle, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token"]);
  curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
  // response includes multiple signedTransactions
  $response = curl_exec($handle);
  $httpcode = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
  curl_close($handle);
  
  // getting a json object from the $response
  $json = json_decode($response, false);
  return $json;
}

from the above function, you below result:

{
  "revision":"1656919939000_2000000096041637",
  "bundleId":"com.yourdomain.yourappname.test",
  "environment":"Sandbox",
  "hasMore":false,
  "signedTransactions":[LIST OF SIGNED TRANSACTION]
}

each signedTransaction contains below information.

{
  "transactionId":"SOME NUMBER",
  "originalTransactionId":"SOME NUMBER",
  "bundleId":"com.yourdomain.yourappname.test",
  "productId":"com.yourdomain.yourappname.test.in_app_product_name",
  "purchaseDate":1644992734000,
  "originalPurchaseDate":1644992734000,
  "quantity":1,
  "type":"Non-Consumable",
  "inAppOwnershipType":"PURCHASED",
  "signedDate":1658031241903,
  "environment":"Sandbox"
}

so, you want to iterate over $json->signedTransactions, and find a match.

foreach($transactions as $transaction) {

}
use Carbon\Carbon;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;

public function generateJWTForSigningApplePurchase() {
  $signer = new Sha256();
  $key = env('JWT_SECRET');
  $privateKey = new Key($key);
  $time = Carbon::now()->timestamp;
  $token = (new Builder())
    ->issuedBy(env('ISSUER_ID'))
    ->issuedAt($time)
    ->expiresAt($time + (60 * 60))
    ->withClaim('bid', env('BUNDLE_ID'))
    ->withClaim('aud', "appstoreconnect-v1")
    ->withHeader('alg', 'ES256')
    ->withHeader('kid', env('KID'))
    ->withHeader('typ', 'JWT')
    ->getToken($signer, $privateKey);
  return $token->__toString();
}

Client-side code in Swift

func verify(id: String) {	
  let urlStringVerifyPurchase = "https://YOUR_DOMAIN.COM/verify/"
  Task {
    guard let verificationResult = await products.filter({ $0.id == id}).first?.currentEntitlement else { return }
    switch verificationResult {
      case .verified(let transaction):
        // your validation function
        validation(transaction: transaction)
	let queryItems = [URLQueryItem(name: "transactionID", value: "\(transaction.originalID)")]
	var urlComps = URLComponents(string: urlStringVerifyPurchase)!
	urlComps.queryItems = queryItems
	guard let url = urlComps.url else { return }
	let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
	  guard let data = data else {
	    print("no data")
	    return
	  }
	  do {
	    let decode = try JSONDecoder().decode(ServerVerifyPurchaseResponse.self, from: data)
	    if decode.code == 200 {						   
              self?.performDownload(id: id)
	    }
	  } catch {
	    print("error occurred when decoding verify purchase data: \(error)")
	  }
	}
	task.resume()
      case .unverified(let transaction, _):
        print(transaction)
    }
  }
}
カテゴリー
iOS Swift

LongPress Gesture in SwiftUI

@State var isLongPressed = false
	
var longPress: some Gesture {
  LongPressGesture(minimumDuration: 3)
    .onEnded { ges in
    self.isLongPressed = true
  }
}
カテゴリー
iOS Swift

StoreKit2での購入フロー

ProductリストをUIに表示させるバックエンドフローは以下

// array of fetched product ids
var identifiers: [String] = []
// array of Product
var products: [Products] = []

// fetch from server and store data in Keychain
func fetchProductsFromServer() async {
  let url = URL(string: "https://jamapp.me/test_getInAppProducts")!
  do {
    let (data, _) = try await URLSession.shared.data(from: url)
    KeychainWrapper.standard.set(data, forKey: dataKey)
  } catch {
    print(error)
  }
}

// decode from keychain and fill identifier array
func decodeProductsFromKeychain() async {
  guard let data = KeychainWrapper.standard.data(forKey: self.dataKey) else { return }		
  do {
    let decode = try JSONDecoder().decode(ServerResponse.self, from: data)
    decodedProducts = Set(decode.products)
    for product in decode.products {
      identifiers.insert(product.identifier)
    }
  } catch {
    print(error)
  }
}

// fetch from AppStore
func fetchProductsFromAppStore() async {
  do {
    products = try await Product.products(for: identifiers)
    for product in products {				
      if await product.currentEntitlement != nil {
	// storing currentEntitlement in Keychain for off-line use...				    
        KeychainWrapper.standard.set(true, forKey: product.id)
	// storing in isPurchasedDict to update UI
        await MainActor.run {						   
          self.isPurchasedDict[product.id] =   KeychainWrapper.standard.bool(forKey: product.id)
        }
      } else {
        KeychainWrapper.standard.set(false, forKey: product.id)
        await MainActor.run {
          self.isPurchasedDict[product.id] = KeychainWrapper.standard.bool(forKey: product.id)
        }
      }
    }
  } catch {
    print(error)
  }
}

in order to handle any unhandled transactions, listen for Transaction.updates in appDelegate

extension AppDelegate: UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    // listening for transaction updates
    Task {
      await listenForTransactions()
    }
    return true
    }	

  func listenForTransactions() async {
    for await verificationResult in Transaction.updates {
      switch verificationResult {
        case .verified(let transaction):
          KeychainWrapper.standard.set(true, forKey: transaction.productID)
          await transaction.finish()
        case .unverified(let transaction, _):
          await transaction.finish()
      }
    }
  }
}

in order to buy a product, do below.

func buyProduct(product: Product) {
  Task {
    do {
      let result = try await product.purchase()
      switch result {
        case .success(let verification):
          switch verification {
            case .verified(let transaction):
	      await updatePurchase(transaction: transaction)
	      await transaction.finish()
	    case .unverified(let transaction, _):
	      await transaction.finish()
          }
	case .userCancelled:
	  ()
	case .pending:
	  ()
	@unknown default:
	  ()
      }
    } catch {
      print(error)
    }
  }
}
	
func updatePurchase(transaction: Transaction) async {
  KeychainWrapper.standard.set(true, forKey: transaction.productID)
  await MainActor.run {
    isPurchasedDict[transaction.productID] = true
  }
}
カテゴリー
Android Java

TitleBarとStatusBar

Title Barを消すにはres -> values -> themes -> themes.xml 内の下記を変更する

<style name="Theme.MidiPractice" parent="Theme.MaterialComponents.DayNight.NoActionBar">

もしくは、ActivityのonCreate()で以下を実行する

getSupportActionBar().hide();

StatusBarの色を変更するには res -> values -> themes -> themes.xml 内の下記を変更する

<item name="android:statusBarColor" tools:targetApi="l">@color/purple_200</item>

StatusBarのBackgroundColorを白にする場合は以下のように変更

<item name="android:statusBarColor" tools:targetApi="l">@color/white</item>
<item name="android:windowLightStatusBar">true</item>