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();
}
}
月: 2022年7月
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);
// 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
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");
}
}
}
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の最小実装
// 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/
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)
}
}
}
@State var isLongPressed = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.onEnded { ges in
self.isLongPressed = true
}
}
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
}
}
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>