Add mobile camera capture, polaroid preview, gallery save & native share

- カメラ撮影 → プレビュー(ポラロイド風白縁)→ 保存 / ネイティブ共有
- iOS / Android のネイティブ実装(共有・ギャラリー保存・FileProvider)
- iOS は共有シートのプリウォームで初回表示を軽減
- iOS Info.plist 用途説明をビルド後処理で自動付与
- Android パッケージ名ガードを有効化(ビルド前チェック)
- README を整備、.gitignore に slnx / .vscode / .utmp を追加

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 12:56:15 +09:00
parent 7d4324481a
commit 1911637020
115 changed files with 25660 additions and 1 deletions

View File

@@ -0,0 +1,387 @@
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 撮影画像のプレビュー用ダイアログ。
/// 「保存」か「シェア」を選んで実行する。
/// Resources/Canvas_preview.prefab のルートに付与して使う。
/// </summary>
public class CanvasPreview : MonoBehaviour
{
[Header("プレビュー画像 (未設定なら子の RawImage を自動取得)")]
[SerializeField] private RawImage previewImage;
[Header("保存ボタン (未設定なら btn_save を自動取得)")]
[SerializeField] private Button saveButton;
[Header("シェアボタン (未設定なら btn_share を自動取得)")]
[SerializeField] private Button shareButton;
[Header("ポラロイド枠 (Frame.png を割り当て。未設定なら枠なし全画面表示)")]
[SerializeField] private Sprite frameSprite;
[Header("写真を枠内いっぱいに切り抜く (OFF=写真全体を枠内に収める)")]
[SerializeField] private bool cropPhotoToFrame = true;
[Header("カード(枠)の表示領域。画面に対する余白の割合")]
[SerializeField] private float cardMarginX = 0.04f;
[SerializeField] private float cardTopAnchor = 0.97f;
[SerializeField] private float cardBottomAnchor = 0.20f;
private string filePath;
private Texture2D previewTexture;
private bool ownsTexture;
private string shareText;
private string shareSubject;
private bool actionRunning;
private Action onClosed;
/// <summary>
/// プレハブを生成してプレビューを表示する。
/// </summary>
/// <param name="filePath">保存・共有する PNG ファイルのパス</param>
/// <param name="texture">プレビュー表示用テクスチャ</param>
/// <param name="ownsTexture">true ならダイアログを閉じる時に texture を破棄する</param>
/// <param name="onClosed">ダイアログが閉じた時に呼ばれるコールバック</param>
/// <param name="prefab">使用するプレハブ。null なら Resources/Canvas_preview を読み込む</param>
public static CanvasPreview Show(
string filePath,
Texture2D texture,
string shareText,
string shareSubject,
bool ownsTexture = true,
Action onClosed = null,
GameObject prefab = null)
{
GameObject source = prefab;
if (source == null)
{
source = Resources.Load<GameObject>("Canvas_preview");
}
if (source == null)
{
Debug.LogError("CanvasPreview: Canvas_preview プレハブが見つかりません。");
return null;
}
GameObject instance = Instantiate(source);
CanvasPreview preview = instance.GetComponent<CanvasPreview>();
if (preview == null)
{
Debug.LogError("CanvasPreview: プレハブに CanvasPreview コンポーネントがありません。");
Destroy(instance);
return null;
}
preview.Setup(filePath, texture, shareText, shareSubject, ownsTexture, onClosed);
return preview;
}
private void Awake()
{
ResolveReferences();
}
private void ResolveReferences()
{
if (previewImage == null)
{
previewImage = GetComponentInChildren<RawImage>(true);
}
if (saveButton == null)
{
saveButton = FindOrAddButton("btn_save");
}
if (shareButton == null)
{
shareButton = FindOrAddButton("btn_share");
}
}
private Button FindOrAddButton(string targetName)
{
Transform target = FindDeep(transform, targetName);
if (target == null)
{
Debug.LogWarning("CanvasPreview: " + targetName + " が見つかりません。");
return null;
}
// プレハブの btn_save / btn_share は Image だけで Button が無いので補う
Button button = target.GetComponent<Button>();
if (button == null)
{
button = target.gameObject.AddComponent<Button>();
}
return button;
}
private static Transform FindDeep(Transform parent, string targetName)
{
if (parent.name == targetName)
{
return parent;
}
for (int i = 0; i < parent.childCount; i++)
{
Transform found = FindDeep(parent.GetChild(i), targetName);
if (found != null)
{
return found;
}
}
return null;
}
private void Setup(
string filePath,
Texture2D texture,
string shareText,
string shareSubject,
bool ownsTexture,
Action onClosed)
{
this.filePath = filePath;
this.previewTexture = texture;
this.shareText = shareText;
this.shareSubject = shareSubject;
this.ownsTexture = ownsTexture;
this.onClosed = onClosed;
// シェアを押す前に共有シートを先読みしておき、初回表示の遅延を減らす
NativeShareBridge.PrewarmShare(filePath, shareText, shareSubject);
if (previewImage != null && texture != null)
{
previewImage.texture = texture;
// プレハブの初期色を白に戻して素の画像を表示する
previewImage.color = Color.white;
BuildPolaroidLayout(texture);
}
if (saveButton != null)
{
saveButton.onClick.RemoveListener(OnSaveClicked);
saveButton.onClick.AddListener(OnSaveClicked);
}
if (shareButton != null)
{
shareButton.onClick.RemoveListener(OnShareClicked);
shareButton.onClick.AddListener(OnShareClicked);
}
}
/// <summary>
/// ポラロイド風の枠(Frame.png)を組み、その窓の中に撮影画像を配置する。
/// プレハブの RectTransform 値には依存せず、すべてコード側で構築する。
/// </summary>
private void BuildPolaroidLayout(Texture2D texture)
{
RectTransform root = (RectTransform)transform;
// 枠が無ければ従来どおり全画面に収める(フォールバック)
if (frameSprite == null)
{
FillParentWithPhoto((RectTransform)previewImage.transform.parent, texture);
return;
}
// ボタンの上に、左右に余白をとった表示領域を作る
RectTransform cardArea = CreateChild("CardArea", root);
cardArea.anchorMin = new Vector2(cardMarginX, cardBottomAnchor);
cardArea.anchorMax = new Vector2(1f - cardMarginX, cardTopAnchor);
cardArea.offsetMin = Vector2.zero;
cardArea.offsetMax = Vector2.zero;
// カード(枠)= 撮影画像の縦横比に合わせる。
// フレームは 9 スライスなので、白縁の太さは一定のまま縦長/横長に伸びる。
RectTransform card = CreateChild("Card", cardArea);
SetCenterAnchors(card);
AspectRatioFitter cardFitter = card.gameObject.AddComponent<AspectRatioFitter>();
cardFitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
cardFitter.aspectRatio =
texture.height > 0 ? (float)texture.width / texture.height : 1f;
// 9 スライス境界(ピクセル)を UI 上の余白サイズに換算する。
// border = (x:左, y:下, z:右, w:上)
Vector4 border = frameSprite.border;
Canvas rootCanvas = GetComponentInParent<Canvas>();
float refPpu = rootCanvas != null ? rootCanvas.referencePixelsPerUnit : 100f;
float spritePpu = frameSprite.pixelsPerUnit > 0f ? frameSprite.pixelsPerUnit : 100f;
float k = refPpu / spritePpu; // pixelsPerUnitMultiplier は 1 とする
float left = border.x * k;
float bottom = border.y * k;
float right = border.z * k;
float top = border.w * k;
// 窓(マスク)。カード全体から白縁ぶんだけ内側に寄せる
RectTransform window = CreateChild("Window", card);
window.anchorMin = Vector2.zero;
window.anchorMax = Vector2.one;
window.offsetMin = new Vector2(left, bottom);
window.offsetMax = new Vector2(-right, -top);
window.gameObject.AddComponent<RectMask2D>();
// 写真を窓の子に移し、縦横比フィット
RectTransform photo = previewImage.rectTransform;
photo.SetParent(window, false);
SetCenterAnchors(photo);
previewImage.raycastTarget = false;
AspectRatioFitter photoFitter = previewImage.GetComponent<AspectRatioFitter>();
if (photoFitter == null)
{
photoFitter = previewImage.gameObject.AddComponent<AspectRatioFitter>();
}
photoFitter.aspectMode = cropPhotoToFrame
? AspectRatioFitter.AspectMode.EnvelopeParent // 窓いっぱいに切り抜く
: AspectRatioFitter.AspectMode.FitInParent; // 写真全体を収める
if (texture.height > 0)
{
photoFitter.aspectRatio = (float)texture.width / texture.height;
}
// 枠(Frame)を写真の背面に敷く。窓のグレーは写真が覆い、白縁とロゴが残る
RectTransform frame = CreateChild("Frame", card);
frame.SetSiblingIndex(0); // Window より後ろ(先頭)へ
StretchFull(frame);
Image frameImage = frame.gameObject.AddComponent<Image>();
frameImage.sprite = frameSprite;
frameImage.type = Image.Type.Sliced; // 9 スライスで白縁を一定幅に保つ
frameImage.pixelsPerUnitMultiplier = 1f;
frameImage.preserveAspect = false;
frameImage.raycastTarget = false;
}
private void FillParentWithPhoto(RectTransform parent, Texture2D texture)
{
RectTransform rt = previewImage.rectTransform;
rt.localScale = Vector3.one;
StretchFull(rt);
AspectRatioFitter fitter = previewImage.GetComponent<AspectRatioFitter>();
if (fitter == null)
{
fitter = previewImage.gameObject.AddComponent<AspectRatioFitter>();
}
fitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
if (texture.height > 0)
{
fitter.aspectRatio = (float)texture.width / texture.height;
}
}
private static RectTransform CreateChild(string childName, Transform parent)
{
GameObject go = new GameObject(childName, typeof(RectTransform));
go.layer = parent.gameObject.layer;
RectTransform rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
rt.localScale = Vector3.one;
return rt;
}
private static void StretchFull(RectTransform rt)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
private static void SetCenterAnchors(RectTransform rt)
{
rt.localScale = Vector3.one;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
}
private void OnSaveClicked()
{
if (actionRunning)
{
return;
}
actionRunning = true;
SetButtonsInteractable(false);
NativeShareBridge.SaveImageToGallery(filePath);
Close();
}
private void OnShareClicked()
{
if (actionRunning)
{
return;
}
actionRunning = true;
SetButtonsInteractable(false);
NativeShareBridge.ShareImage(filePath, shareText, shareSubject);
Close();
}
private void SetButtonsInteractable(bool value)
{
if (saveButton != null)
{
saveButton.interactable = value;
}
if (shareButton != null)
{
shareButton.interactable = value;
}
}
/// <summary>
/// ダイアログを閉じる。所有していれば texture も破棄する。
/// </summary>
public void Close()
{
if (onClosed != null)
{
Action callback = onClosed;
onClosed = null;
callback.Invoke();
}
if (ownsTexture && previewTexture != null)
{
if (previewImage != null && previewImage.texture == previewTexture)
{
previewImage.texture = null;
}
Destroy(previewTexture);
previewTexture = null;
}
Destroy(gameObject);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 558efcbf63fcc46c6ac404d57c8ead9b

View File

@@ -0,0 +1,517 @@
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_ANDROID
using UnityEngine.Android;
#endif
public class MobileCameraStarter : MonoBehaviour
{
[Header("カメラ映像を表示する RawImage")]
public RawImage cameraView;
[Header("シェアボタンのGameObject")]
public GameObject shareButtonObject;
[Header("シェアボタンのOnClickを自動登録する")]
public bool autoAddShareButtonOnClick = true;
[Header("キャプチャ時にシェアボタンを隠す")]
public bool hideShareButtonWhileCapturing = true;
[Header("前面カメラを使う")]
public bool useFrontCamera = false;
[Header("希望する解像度とFPSを使う")]
public bool useRequestedResolution = true;
[Header("希望する解像度とFPS")]
public int requestedWidth = 1280;
public int requestedHeight = 720;
public int requestedFPS = 30;
[Header("シェア設定")]
public string shareText = "カメラ画像をシェアします";
public string shareSubject = "Camera Screenshot";
public string shareFilePrefix = "camera_share";
[Header("プレビュー用 Canvas_preview プレハブ (未設定なら Resources から読み込み)")]
public GameObject canvasPreviewPrefab;
[Header("キャプチャ時に一時的に隠すUI")]
public GameObject[] hideWhileCapturing;
private WebCamTexture webCamTexture;
private AspectRatioFitter aspectRatioFitter;
private Button shareButton;
private bool isStartingCamera = false;
private bool isCapturing = false;
private bool isCaptureReady = false;
private int lastWidth = -1;
private int lastHeight = -1;
private int lastRotation = -1;
private bool lastMirrored = false;
#if UNITY_ANDROID
private PermissionCallbacks permissionCallbacks;
#endif
private IEnumerator Start()
{
SetupCameraView();
SetupShareButton();
SetShareButtonInteractable(false);
yield return RequestCameraPermission();
}
private void SetupCameraView()
{
if (cameraView == null)
{
Debug.LogWarning("Camera View に RawImage が設定されていません。");
return;
}
aspectRatioFitter = cameraView.GetComponent<AspectRatioFitter>();
if (aspectRatioFitter == null)
{
aspectRatioFitter = cameraView.gameObject.AddComponent<AspectRatioFitter>();
}
aspectRatioFitter.aspectMode = AspectRatioFitter.AspectMode.EnvelopeParent;
// カメラ背景として使うだけならタッチ判定は不要
cameraView.raycastTarget = false;
}
private void SetupShareButton()
{
if (shareButtonObject == null)
{
Debug.LogWarning("Share Button Object が設定されていません。");
return;
}
shareButton = shareButtonObject.GetComponent<Button>();
if (shareButton == null)
{
Debug.LogWarning("Share Button Object に Button コンポーネントがありません。");
return;
}
if (autoAddShareButtonOnClick)
{
shareButton.onClick.RemoveListener(CapturePreview);
shareButton.onClick.AddListener(CapturePreview);
}
}
private void SetShareButtonInteractable(bool value)
{
if (shareButton != null)
{
shareButton.interactable = value;
}
}
private IEnumerator RequestCameraPermission()
{
#if UNITY_IOS
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
if (Application.HasUserAuthorization(UserAuthorization.WebCam))
{
StartCoroutine(StartCamera());
}
else
{
Debug.LogWarning("iOS: カメラの使用が許可されませんでした。");
SetShareButtonInteractable(false);
}
#elif UNITY_ANDROID
if (Permission.HasUserAuthorizedPermission(Permission.Camera))
{
StartCoroutine(StartCamera());
}
else
{
permissionCallbacks = new PermissionCallbacks();
permissionCallbacks.PermissionGranted += OnAndroidCameraPermissionGranted;
permissionCallbacks.PermissionDenied += OnAndroidCameraPermissionDenied;
permissionCallbacks.PermissionDeniedAndDontAskAgain += OnAndroidCameraPermissionDeniedAndDontAskAgain;
Permission.RequestUserPermission(Permission.Camera, permissionCallbacks);
}
yield return null;
#else
StartCoroutine(StartCamera());
yield return null;
#endif
}
#if UNITY_ANDROID
private void OnAndroidCameraPermissionGranted(string permissionName)
{
if (permissionName == Permission.Camera)
{
Debug.Log("Android: カメラ権限が許可されました。");
StartCoroutine(StartCamera());
}
}
private void OnAndroidCameraPermissionDenied(string permissionName)
{
Debug.LogWarning("Android: カメラ権限が拒否されました。");
SetShareButtonInteractable(false);
}
private void OnAndroidCameraPermissionDeniedAndDontAskAgain(string permissionName)
{
Debug.LogWarning("Android: カメラ権限が拒否され、今後表示しない設定になりました。");
SetShareButtonInteractable(false);
}
#endif
private IEnumerator StartCamera()
{
if (isStartingCamera)
{
yield break;
}
isStartingCamera = true;
isCaptureReady = false;
SetShareButtonInteractable(false);
if (cameraView == null)
{
Debug.LogWarning("Camera View に RawImage が設定されていません。");
isStartingCamera = false;
yield break;
}
StopCamera();
yield return null;
WebCamDevice[] devices = WebCamTexture.devices;
Debug.Log("使用可能なカメラ数: " + devices.Length);
for (int i = 0; i < devices.Length; i++)
{
Debug.Log(
"Camera[" + i + "] " +
devices[i].name +
" / Front: " + devices[i].isFrontFacing
);
}
if (devices.Length == 0)
{
Debug.LogWarning("使用できるカメラが見つかりません。");
isStartingCamera = false;
SetShareButtonInteractable(false);
yield break;
}
string selectedCameraName = devices[0].name;
for (int i = 0; i < devices.Length; i++)
{
if (devices[i].isFrontFacing == useFrontCamera)
{
selectedCameraName = devices[i].name;
break;
}
}
Debug.Log("使用するカメラ: " + selectedCameraName);
if (useRequestedResolution)
{
webCamTexture = new WebCamTexture(
selectedCameraName,
requestedWidth,
requestedHeight,
requestedFPS
);
}
else
{
// Androidで黒画面になる場合は、まずこちらを試す
webCamTexture = new WebCamTexture(selectedCameraName);
}
cameraView.texture = webCamTexture;
webCamTexture.Play();
float timeout = 5f;
while ((webCamTexture.width <= 16 || webCamTexture.height <= 16) && timeout > 0f)
{
timeout -= Time.deltaTime;
yield return null;
}
Debug.Log(
"WebCamTexture width: " + webCamTexture.width +
" / height: " + webCamTexture.height +
" / rotation: " + webCamTexture.videoRotationAngle +
" / mirrored: " + webCamTexture.videoVerticallyMirrored
);
if (webCamTexture.width <= 16 || webCamTexture.height <= 16)
{
Debug.LogWarning("カメラ映像のフレームが取得できていません。");
isStartingCamera = false;
isCaptureReady = false;
SetShareButtonInteractable(false);
yield break;
}
ApplyCameraTextureSettings();
isStartingCamera = false;
isCaptureReady = true;
SetShareButtonInteractable(true);
Debug.Log("カメラを開始しました。キャプチャ準備完了。");
}
private void Update()
{
if (webCamTexture == null || !webCamTexture.isPlaying)
{
return;
}
if (
webCamTexture.width != lastWidth ||
webCamTexture.height != lastHeight ||
webCamTexture.videoRotationAngle != lastRotation ||
webCamTexture.videoVerticallyMirrored != lastMirrored
)
{
ApplyCameraTextureSettings();
}
}
private void ApplyCameraTextureSettings()
{
if (cameraView == null || webCamTexture == null)
{
return;
}
int rotationAngle = webCamTexture.videoRotationAngle;
cameraView.rectTransform.localEulerAngles =
new Vector3(0f, 0f, -rotationAngle);
if (webCamTexture.videoVerticallyMirrored)
{
cameraView.uvRect = new Rect(0f, 1f, 1f, -1f);
}
else
{
cameraView.uvRect = new Rect(0f, 0f, 1f, 1f);
}
float aspectRatio = (float)webCamTexture.width / webCamTexture.height;
if (aspectRatioFitter != null)
{
aspectRatioFitter.aspectRatio = aspectRatio;
aspectRatioFitter.aspectMode = AspectRatioFitter.AspectMode.EnvelopeParent;
}
lastWidth = webCamTexture.width;
lastHeight = webCamTexture.height;
lastRotation = webCamTexture.videoRotationAngle;
lastMirrored = webCamTexture.videoVerticallyMirrored;
}
// 旧名互換: ボタンの onClick が CaptureAndShare を参照していても動くようにする
public void CaptureAndShare()
{
CapturePreview();
}
public void CapturePreview()
{
if (!isCaptureReady)
{
Debug.LogWarning("まだキャプチャの準備ができていません。");
return;
}
if (isCapturing)
{
return;
}
StartCoroutine(CaptureAndPreviewCoroutine());
}
private IEnumerator CaptureAndPreviewCoroutine()
{
isCapturing = true;
SetShareButtonInteractable(false);
bool shareButtonPreviousActive = true;
if (shareButtonObject != null && hideShareButtonWhileCapturing)
{
shareButtonPreviousActive = shareButtonObject.activeSelf;
shareButtonObject.SetActive(false);
}
bool[] previousActiveStates = null;
if (hideWhileCapturing != null && hideWhileCapturing.Length > 0)
{
previousActiveStates = new bool[hideWhileCapturing.Length];
for (int i = 0; i < hideWhileCapturing.Length; i++)
{
if (hideWhileCapturing[i] != null)
{
previousActiveStates[i] = hideWhileCapturing[i].activeSelf;
hideWhileCapturing[i].SetActive(false);
}
}
}
yield return new WaitForEndOfFrame();
Texture2D screenshot = new Texture2D(
Screen.width,
Screen.height,
TextureFormat.RGB24,
false
);
screenshot.ReadPixels(
new Rect(0f, 0f, Screen.width, Screen.height),
0,
0
);
screenshot.Apply();
byte[] pngData = screenshot.EncodeToPNG();
string fileName =
shareFilePrefix + "_" +
System.DateTime.Now.ToString("yyyyMMdd_HHmmss") +
".png";
string filePath = Path.Combine(Application.temporaryCachePath, fileName);
File.WriteAllBytes(filePath, pngData);
// screenshot はここでは破棄しない。プレビュー表示に使い、
// ダイアログを閉じる時に CanvasPreview 側で破棄する。
if (hideWhileCapturing != null && previousActiveStates != null)
{
for (int i = 0; i < hideWhileCapturing.Length; i++)
{
if (hideWhileCapturing[i] != null)
{
hideWhileCapturing[i].SetActive(previousActiveStates[i]);
}
}
}
if (shareButtonObject != null && hideShareButtonWhileCapturing)
{
shareButtonObject.SetActive(shareButtonPreviousActive);
}
Debug.Log("プレビュー用画像を保存しました: " + filePath);
// 撮影が終わったらプレビューダイアログを開く。
// 保存 / シェアの実行はダイアログ側のボタンから行う。
CanvasPreview preview = CanvasPreview.Show(
filePath,
screenshot,
shareText,
shareSubject,
true, // テクスチャの所有権をプレビューに渡す
OnPreviewClosed, // 閉じたら撮影ボタンを戻す
canvasPreviewPrefab
);
if (preview == null)
{
// プレビューを開けなかった場合のフォールバック
Destroy(screenshot);
isCapturing = false;
if (isCaptureReady)
{
SetShareButtonInteractable(true);
}
yield break;
}
// プレビュー表示中はボタンを無効のままにし、
// 閉じられた時に OnPreviewClosed で復帰させる。
isCapturing = false;
}
private void OnPreviewClosed()
{
if (isCaptureReady)
{
SetShareButtonInteractable(true);
}
}
public void StopCamera()
{
isCaptureReady = false;
SetShareButtonInteractable(false);
if (webCamTexture != null)
{
if (webCamTexture.isPlaying)
{
webCamTexture.Stop();
}
webCamTexture = null;
}
if (cameraView != null)
{
cameraView.texture = null;
}
}
private void OnDestroy()
{
StopCamera();
if (shareButton != null && autoAddShareButtonOnClick)
{
shareButton.onClick.RemoveListener(CapturePreview);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 735216b038fac472297bbfd536adaf32

View File

@@ -0,0 +1,137 @@
using UnityEngine;
#if UNITY_IOS && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
public static class NativeShareBridge
{
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void _NativeShareBridge_ShareImage(
string filePath,
string text,
string subject
);
[DllImport("__Internal")]
private static extern void _NativeShareBridge_SaveImageToGallery(
string filePath
);
[DllImport("__Internal")]
private static extern void _NativeShareBridge_PrewarmShare(
string filePath,
string text,
string subject
);
#endif
/// <summary>
/// 共有シートを事前に生成しておく(初回表示の遅延対策)。
/// プレビューを開いた時など、シェアを押す前に呼ぶ。
/// </summary>
public static void PrewarmShare(string filePath, string text, string subject)
{
if (string.IsNullOrEmpty(filePath))
{
return;
}
#if UNITY_IOS && !UNITY_EDITOR
_NativeShareBridge_PrewarmShare(filePath, text ?? "", subject ?? "");
#endif
}
public static void ShareImage(string filePath, string text, string subject)
{
if (string.IsNullOrEmpty(filePath))
{
Debug.LogWarning("共有するファイルパスが空です。");
return;
}
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (AndroidJavaClass unityPlayer =
new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
AndroidJavaObject activity =
unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
using (AndroidJavaClass bridge =
new AndroidJavaClass("jp.yourname.unity.nativeshare.NativeShareBridge"))
{
bridge.CallStatic(
"shareImage",
activity,
filePath,
text ?? "",
subject ?? ""
);
}
}
}
catch (System.Exception e)
{
Debug.LogError("Android共有処理でエラー: " + e);
}
#elif UNITY_IOS && !UNITY_EDITOR
_NativeShareBridge_ShareImage(
filePath,
text ?? "",
subject ?? ""
);
#else
Debug.Log("Editorではネイティブ共有は実行されません。保存先: " + filePath);
#endif
}
/// <summary>
/// 画像を端末の写真フォルダ(カメラロール / ギャラリー)に保存する。
/// </summary>
public static void SaveImageToGallery(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
Debug.LogWarning("保存するファイルパスが空です。");
return;
}
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (AndroidJavaClass unityPlayer =
new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
AndroidJavaObject activity =
unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
using (AndroidJavaClass bridge =
new AndroidJavaClass("jp.yourname.unity.nativeshare.NativeShareBridge"))
{
bridge.CallStatic(
"saveImageToGallery",
activity,
filePath,
System.IO.Path.GetFileName(filePath)
);
}
}
}
catch (System.Exception e)
{
Debug.LogError("Androidギャラリー保存処理でエラー: " + e);
}
#elif UNITY_IOS && !UNITY_EDITOR
_NativeShareBridge_SaveImageToGallery(filePath);
#else
Debug.Log("Editorではギャラリー保存は実行されません。保存元: " + filePath);
#endif
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86408d12d32064228a5fe0a12c19f008

View File

@@ -0,0 +1,32 @@
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(AudioSource))]
public class ShatterSe : MonoBehaviour
{
private AudioSource audioSource;
void Awake()
{
// 自分にくっついている AudioSource を取得。なければ追加する
audioSource = GetComponent<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
}
// 自分にくっついている Button の OnClick に自動登録する
Button button = GetComponent<Button>();
if (button == null)
{
Debug.LogError("ShatterSe: Button コンポーネントが見つかりません。", this);
return;
}
button.onClick.AddListener(Play);
}
public void Play()
{
audioSource.Play();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ec9fcf5795b1419ead64d2185ac1127