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:
387
Assets/Scripts/CanvasPreview.cs
Normal file
387
Assets/Scripts/CanvasPreview.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CanvasPreview.cs.meta
Normal file
2
Assets/Scripts/CanvasPreview.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 558efcbf63fcc46c6ac404d57c8ead9b
|
||||
517
Assets/Scripts/MobileCameraStarter.cs
Normal file
517
Assets/Scripts/MobileCameraStarter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MobileCameraStarter.cs.meta
Normal file
2
Assets/Scripts/MobileCameraStarter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 735216b038fac472297bbfd536adaf32
|
||||
137
Assets/Scripts/NativeShareBridge.cs
Normal file
137
Assets/Scripts/NativeShareBridge.cs
Normal 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
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NativeShareBridge.cs.meta
Normal file
2
Assets/Scripts/NativeShareBridge.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86408d12d32064228a5fe0a12c19f008
|
||||
32
Assets/Scripts/ShatterSe.cs
Normal file
32
Assets/Scripts/ShatterSe.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ShatterSe.cs.meta
Normal file
2
Assets/Scripts/ShatterSe.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ec9fcf5795b1419ead64d2185ac1127
|
||||
Reference in New Issue
Block a user