اگر برنامه اندروید شما از دوربین استفاده میکند، هنگام مدیریت جهتها ملاحظات خاصی وجود دارد. این سند فرض میکند که شما مفاهیم اولیه رابط برنامهنویسی کاربردی دوربین اندروید (Android camera2 API) را درک میکنید. میتوانید پست وبلاگ یا خلاصه ما را برای مرور کلی دوربین 2 مطالعه کنید. همچنین توصیه میکنیم قبل از پرداختن به این سند، ابتدا نوشتن یک برنامه دوربین را امتحان کنید.
پیشینه
مدیریت جهتها در برنامههای دوربین اندروید کمی پیچیده است و باید عوامل زیر را در نظر گرفت:
- جهت طبیعی: جهت نمایشگر زمانی که دستگاه در موقعیت «عادی» طراحی دستگاه قرار دارد - معمولاً جهت عمودی برای تلفنهای همراه و جهت افقی برای لپتاپها.
- جهت حسگر: جهت حسگر که به صورت فیزیکی روی دستگاه نصب شده است.
- چرخش صفحه نمایش: میزان چرخش فیزیکی دستگاه نسبت به جهت طبیعی آن.
- اندازه منظرهیاب: اندازهی منظرهیابی که برای نمایش پیشنمایش دوربین استفاده میشود.
- اندازه تصویر خروجی توسط دوربین.
این عوامل در کنار هم، تعداد زیادی از تنظیمات رابط کاربری و پیشنمایش ممکن را برای برنامههای دوربین معرفی میکنند. این سند برای نشان دادن چگونگی پیمایش این تنظیمات و مدیریت صحیح جهتگیری دوربین در برنامههای اندروید توسط توسعهدهندگان تهیه شده است.
برای سادهتر کردن موضوع، فرض کنید همه مثالها شامل دوربین عقب هستند، مگر اینکه خلاف آن ذکر شده باشد. علاوه بر این، تمام عکسهای زیر شبیهسازی شدهاند تا تصاویر از نظر بصری واضحتر باشند.
همه چیز درباره جهت گیری ها
جهت گیری طبیعی
جهت طبیعی به عنوان جهت صفحه نمایش زمانی تعریف میشود که دستگاه در موقعیتی قرار دارد که معمولاً انتظار میرود در آن باشد. برای تلفنها، جهت طبیعی آنها اغلب عمودی است. به عبارت دیگر، تلفنها عرض کوتاهتر و ارتفاع بلندتری دارند. برای لپتاپها، جهت طبیعی آنها افقی است، به این معنی که عرض بلندتر و ارتفاع کوتاهتری دارند. تبلتها کمی پیچیدهتر از این هستند - آنها میتوانند عمودی یا افقی باشند.

جهت سنسور
به طور رسمی، جهت سنسور با درجهای که تصویر خروجی از سنسور باید در جهت عقربههای ساعت بچرخد تا با جهت طبیعی دستگاه مطابقت داشته باشد، اندازهگیری میشود. به عبارت دیگر، جهت سنسور تعداد درجههایی است که یک سنسور قبل از نصب روی دستگاه، خلاف جهت عقربههای ساعت چرخانده میشود. هنگام نگاه کردن به صفحه نمایش، چرخش در جهت عقربههای ساعت به نظر میرسد، زیرا سنسور دوربین عقب در قسمت "پشت" دستگاه نصب شده است.
طبق تعریف سازگاری اندروید ۱۰ نسخه ۷.۵.۵ در مورد جهت دوربین ، دوربینهای جلو و عقب «باید طوری جهتگیری شوند که طول دوربین با طول صفحه نمایش همتراز باشد».
بافرهای خروجی از دوربینها به صورت افقی (landscape) هستند. از آنجایی که جهت طبیعی تلفنها معمولاً عمودی است، جهت سنسور معمولاً ۹۰ یا ۲۷۰ درجه از جهت طبیعی است تا قسمت طولانی بافر خروجی با قسمت طولانی صفحه نمایش مطابقت داشته باشد. جهت سنسور برای دستگاههایی که جهت طبیعی آنها افقی است، مانند کرومبوکها، متفاوت است. در این دستگاهها، سنسورهای تصویر دوباره قرار میگیرند تا قسمت طولانی بافر خروجی با قسمت طولانی صفحه نمایش مطابقت داشته باشد. از آنجایی که هر دو به صورت افقی هستند، جهتها مطابقت دارند و جهت سنسور ۰ یا ۱۸۰ درجه است.

تصاویر زیر نشان میدهند که اشیاء از دیدگاه یک ناظر که به صفحه دستگاه نگاه میکند، چگونه به نظر میرسند:

صحنه زیر را در نظر بگیرید:

| تلفن | لپتاپ |
|---|---|
![]() | ![]() |
از آنجا که جهت سنسور معمولاً در تلفنها ۹۰ یا ۲۷۰ درجه است، بدون در نظر گرفتن جهت سنسور، تصاویری که دریافت خواهید کرد به این شکل خواهند بود:
| تلفن | لپتاپ |
|---|---|
![]() | ![]() |
فرض کنید جهت سنسور در جهت خلاف عقربههای ساعت در متغیر sensorOrientation ذخیره شده است. برای جبران جهت سنسور، باید بافرهای خروجی را به اندازه `sensorOrientation` در جهت عقربههای ساعت بچرخانید تا جهت گیری با جهت طبیعی دستگاه هماهنگ شود.
در اندروید، برنامهها میتوانند از TextureView یا SurfaceView برای نمایش پیشنمایش دوربین خود استفاده کنند. اگر برنامهها به درستی از هر دو استفاده کنند، میتوانند جهتگیری حسگر را مدیریت کنند. در بخشهای بعدی به تفصیل توضیح خواهیم داد که چگونه باید جهتگیری حسگر را در نظر بگیرید.
چرخش صفحه نمایش
چرخش نمایشگر به طور رسمی با چرخش گرافیکهای رسم شده روی صفحه نمایش تعریف میشود، که جهت مخالف چرخش فیزیکی دستگاه از جهت طبیعی آن است. بخشهای زیر فرض میکنند که چرخشهای نمایشگر همگی مضربی از ۹۰ هستند. اگر چرخش نمایشگر را با درجه مطلق آن بازیابی میکنید، آن را به نزدیکترین {۰، ۹۰، ۱۸۰، ۲۷۰} گرد کنید.
«جهت صفحه نمایش» در بخشهای بعدی به این اشاره دارد که آیا دستگاه از نظر فیزیکی در حالت افقی یا عمودی قرار گرفته است و با «چرخش صفحه نمایش» متفاوت است.
فرض کنید دستگاهها را مطابق شکل زیر، نسبت به موقعیت قبلیشان، ۹۰ درجه در خلاف جهت عقربههای ساعت بچرخانید:

با فرض اینکه بافرهای خروجی از قبل بر اساس جهت سنسور چرخانده شدهاند، بافرهای خروجی زیر را خواهید داشت:
| تلفن | لپتاپ |
|---|---|
![]() | ![]() |
اگر چرخش صفحه نمایش در متغیر displayRotation ذخیره شده باشد، برای دریافت تصویر صحیح، باید بافرهای خروجی را به اندازه displayRotation در خلاف جهت عقربههای ساعت بچرخانید.
برای دوربینهای جلو، چرخش صفحه نمایش روی بافرهای تصویر در جهت مخالف نسبت به صفحه نمایش عمل میکند. اگر با دوربین جلو سر و کار دارید، باید بافرها را با استفاده از displayRotation در جهت عقربههای ساعت بچرخانید.
هشدارها
چرخش نمایشگر، چرخش دستگاه را در خلاف جهت عقربههای ساعت اندازهگیری میکند. این موضوع برای همه APIهای جهتگیری/چرخش صادق نیست.
برای مثال،
- اگر
Display#getRotation()استفاده کنید، چرخش در خلاف جهت عقربههای ساعت را همانطور که در این سند ذکر شده است، دریافت خواهید کرد. - اگر از OrientationEventListener#onOrientationChanged(int) استفاده کنید، چرخش در جهت عقربههای ساعت را دریافت خواهید کرد.
نکتهی مهمی که باید در اینجا به آن توجه داشت این است که چرخش صفحه نمایش نسبت به جهت طبیعی آن است. برای مثال، اگر یک تلفن را به صورت فیزیکی ۹۰ یا ۲۷۰ درجه بچرخانید، صفحه نمایش به شکل افقی (landscape) خواهد بود. در مقابل، اگر یک لپتاپ را به همان میزان بچرخانید، صفحه نمایش به شکل عمودی (portrait) خواهد بود. برنامهها همیشه باید این نکته را در نظر داشته باشند و هرگز در مورد جهت طبیعی دستگاه فرضیهسازی نکنند.
مثالها
بیایید از شکلهای قبلی برای نشان دادن جهتها و چرخشها استفاده کنیم.

| تلفن | لپتاپ |
|---|---|
| جهت گیری طبیعی = پرتره | جهت گیری طبیعی = منظره |
| جهت سنسور = ۹۰ | جهت سنسور = ۰ |
| چرخش نمایشگر = ۰ | چرخش نمایشگر = ۰ |
| جهت نمایش = عمودی | جهت نمایش = افقی |

| تلفن | لپتاپ |
|---|---|
| جهت گیری طبیعی = پرتره | جهت گیری طبیعی = منظره |
| جهت سنسور = ۹۰ | جهت سنسور = ۰ |
| چرخش نمایشگر = ۹۰ | چرخش نمایشگر = ۹۰ |
| جهت نمایش = افقی | جهت نمایش = عمودی |
اندازه منظره یاب
برنامهها همیشه باید اندازه منظرهیاب را بر اساس جهت، چرخش و وضوح صفحه تغییر دهند. به طور کلی، برنامهها باید جهت منظرهیاب را با جهت فعلی صفحه نمایش یکسان کنند. به عبارت دیگر، برنامهها باید لبهی بلند منظرهیاب را با لبهی بلند صفحه نمایش تراز کنند.
اندازه خروجی تصویر بر اساس دوربین
هنگام انتخاب اندازه خروجی تصویر برای پیشنمایش، باید اندازهای را انتخاب کنید که برابر یا کمی بزرگتر از اندازه منظرهیاب باشد، در صورت امکان. معمولاً نمیخواهید بافرهای خروجی بزرگ شوند که باعث پیکسلی شدن تصویر میشود. همچنین نمیخواهید اندازهای خیلی بزرگ انتخاب کنید که میتواند عملکرد را کاهش داده و باتری بیشتری مصرف کند.
جهت JPEG
بیایید با یک موقعیت رایج شروع کنیم - گرفتن یک عکس JPEG. در API دوربین2، میتوانید JPEG_ORIENTATION را در درخواست ضبط ارسال کنید تا مشخص کنید که میخواهید فایلهای JPEG خروجی شما چقدر در جهت عقربههای ساعت بچرخند.
خلاصهای سریع از آنچه ذکر کردیم:
- برای مدیریت جهت سنسور، باید بافر تصویر را با استفاده از
sensorOrientationدر جهت عقربههای ساعت بچرخانید. - برای مدیریت چرخش صفحه نمایش، باید یک بافر را با استفاده از
displayRotationبرای دوربینهای عقب در خلاف جهت عقربههای ساعت و برای دوربینهای جلو در جهت عقربههای ساعت بچرخانید.
با جمع کردن دو عامل، مقداری که میخواهید در جهت عقربههای ساعت بچرخانید برابر است با
-
sensorOrientation - displayRotationبرای دوربینهای عقب. -
sensorOrientation + displayRotationبرای دوربینهای جلو.
میتوانید نمونه کد این منطق را در مستندات JPEG_ORIENTATION مشاهده کنید. توجه داشته باشید که deviceOrientation در کد نمونه مستندات، از چرخش ساعتگرد دستگاه استفاده میکند. از این رو، علائم چرخش نمایشگر معکوس شدهاند.
پیشنمایش
در مورد پیشنمایش دوربین چطور؟ دو روش اصلی وجود دارد که یک برنامه میتواند پیشنمایش دوربین را نمایش دهد: SurfaceView و TextureView. هر کدام از آنها برای مدیریت صحیح جهتگیری به رویکردهای متفاوتی نیاز دارند.
سرفیس ویو
SurfaceView معمولاً برای پیشنمایش دوربین توصیه میشود، البته به شرطی که نیازی به پردازش یا متحرکسازی بافرهای پیشنمایش نداشته باشید. این نرمافزار نسبت به TextureView عملکرد بهتری دارد و منابع کمتری مصرف میکند.
طرحبندی SurfaceView نیز نسبتاً آسانتر است. فقط باید نگران نسبت ابعاد SurfaceView باشید که پیشنمایش دوربین را روی آن نمایش میدهید.
منبع
در زیر SurfaceView، پلتفرم اندروید بافرهای خروجی را میچرخاند تا با جهت صفحه نمایش دستگاه مطابقت داشته باشد. به عبارت دیگر، هم جهت حسگر و هم چرخش صفحه نمایش را در نظر میگیرد. به عبارت سادهتر، وقتی صفحه نمایش ما افقی است، پیشنمایشی دریافت میکنیم که آن هم افقی است و برعکس برای حالت عمودی.
این موضوع در جدول زیر نشان داده شده است. نکته مهمی که باید در نظر داشته باشید این است که چرخش صفحه نمایش به تنهایی جهت منبع را تعیین نمیکند.
| چرخش صفحه نمایش | تلفن (جهت طبیعی = عمودی) | لپتاپ (جهت طبیعی = افقی) |
|---|---|---|
| 0 | ![]() | ![]() |
| ۹۰ | ![]() | ![]() |
| ۱۸۰ | ![]() | ![]() |
| ۲۷۰ | ![]() | ![]() |
طرح بندی
همانطور که میبینید، SurfaceView از قبل برخی از کارهای دشوار را برای ما انجام میدهد. اما اکنون باید اندازه منظرهیاب یا اینکه میخواهید پیشنمایش شما روی صفحه چقدر بزرگ باشد را در نظر بگیرید. SurfaceView به طور خودکار بافر منبع را متناسب با ابعاد آن مقیاسبندی میکند. باید مطمئن شوید که نسبت ابعاد منظرهیاب با نسبت ابعاد بافر منبع یکسان است. به عنوان مثال، اگر سعی کنید یک پیشنمایش به شکل عمودی را در یک SurfaceView به شکل افقی قرار دهید، چیزی شبیه به این تحریف خواهید داشت:

شما معمولاً میخواهید نسبت ابعاد (یعنی عرض/ارتفاع) منظرهیاب با نسبت ابعاد منبع یکسان باشد . اگر نمیخواهید تصویر را در منظرهیاب برش دهید - یعنی برخی از پیکسلها را برای اصلاح نمایش حذف کنید - دو حالت وجود دارد که باید در نظر بگیرید، زمانی که aspectRatioActivity بزرگتر از aspectRatioSource است و زمانی که کوچکتر یا مساوی aspectRatioSource است.
aspectRatioActivity > aspectRatioSource
میتوانید این مورد را به این صورت در نظر بگیرید که فعالیت «گستردهتر» است. در زیر مثالی را در نظر میگیریم که در آن یک فعالیت ۱۶:۹ و یک منبع ۴:۳ دارید.
aspectRatioActivity = 16/9 ≈ 1.78 aspectRatioSource = 4/3 ≈ 1.33
اول اینکه میخواهید نسبت تصویر منظرهیاب شما هم ۴:۳ باشد. سپس باید منبع و منظرهیاب را به این صورت در اکتیویتی قرار دهید:

در این حالت، باید ارتفاع منظرهیاب را با ارتفاع فعالیت مطابقت دهید و در عین حال نسبت ابعاد منظرهیاب را با نسبت ابعاد منبع یکسان کنید. شبه کد به شرح زیر است:
viewfinderHeight = activityHeight; viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource
حالت دیگر زمانی است که فعالیت «باریکتر» یا «بلندتر» باشد. میتوانیم از مثال قبلی دوباره استفاده کنیم، با این تفاوت که در مثال زیر دستگاه را ۹۰ درجه میچرخانید و فعالیت را ۹:۱۶ و منبع را ۳:۴ میکنید.
aspectRatioActivity = 9/16 = 0.5625 aspectRatioSource = 3/4 = 0.75
در این حالت، شما میخواهید منبع و منظرهیاب را به این صورت در activity قرار دهید:

شما باید عرض منظرهیاب را با عرض فعالیت (برخلاف ارتفاع در مورد قبلی) مطابقت دهید، در حالی که نسبت ابعاد منظرهیاب با نسبت ابعاد منبع یکسان باشد. شبه کد:
viewfinderWidth = activityWidth; viewfinderHeight = activityWidth / aspectRatioSource;
کلیپینگ
AutoFitSurfaceView.kt (github) از نمونههای Camera2، SurfaceView را نادیده میگیرد و نسبتهای ابعاد نامتناسب را با استفاده از تصویری که در هر دو بعد برابر یا "فقط بزرگتر" از فعالیت است، مدیریت میکند و سپس محتوایی را که سرریز میکند، برش میدهد. این برای برنامههایی مفید است که میخواهند پیشنمایش کل فعالیت را پوشش دهد یا نمای با ابعاد ثابت را به طور کامل پر کند، بدون اینکه تصویر را تحریف کند.
هشدار
نمونه قبلی سعی میکند با بزرگتر کردن پیشنمایش از اکتیویتی، فضای صفحه نمایش را به حداکثر برساند تا هیچ فضایی پر نشده باقی نماند. این امر به این واقعیت متکی است که بخشهای سرریز شده به طور پیشفرض توسط طرحبندی والد (یا ViewGroup) برش داده میشوند. این رفتار با RelativeLayout و LinearLayout سازگار است اما با ConstraintLayout سازگار نیست. یک ConstraintLayout ممکن است اندازه نماهای فرزند را تغییر دهد تا آنها را در داخل طرحبندی قرار دهد، که این امر باعث میشود جلوه "برش مرکزی" مورد نظر از بین برود و باعث ایجاد پیشنمایشهای کشیده شود. میتوانید به این کامیت به عنوان مرجع مراجعه کنید.
نمای بافت
TextureView حداکثر کنترل را بر محتوای پیشنمایش دوربین ارائه میدهد، اما از نظر عملکرد هزینهبر است. همچنین برای نمایش صحیح پیشنمایش دوربین به کار بیشتری نیاز دارد.
منبع
در زیر TextureView، پلتفرم اندروید بافرهای خروجی را مطابق با جهت حسگر میچرخاند تا با جهت طبیعی دستگاه مطابقت داشته باشد. در حالی که TextureView جهت حسگر را مدیریت میکند، چرخشهای صفحه نمایش را مدیریت نمیکند. این بافرهای خروجی را با جهت طبیعی دستگاه همتراز میکند، به این معنی که شما باید خودتان چرخشهای صفحه نمایش را مدیریت کنید.
این موضوع در جدول زیر نشان داده شده است. سعی کنید شکلها را با چرخش نمایشگر مربوطه بچرخانید، در واقع همان شکلها را در SurfaceView دریافت خواهید کرد.
| چرخش صفحه نمایش | تلفن (جهت طبیعی = عمودی) | لپتاپ (جهت طبیعی = افقی) |
|---|---|---|
| 0 | ![]() | ![]() |
| ۹۰ | ![]() | ![]() |
| ۱۸۰ | ![]() | ![]() |
| ۲۷۰ | ![]() | ![]() |
طرح بندی
طرحبندی برای TextureView کمی پیچیده است. قبلاً پیشنهاد شده بود که از ماتریس تبدیل برای TextureView استفاده شود، اما این روش برای همه دستگاهها کار نمیکند. پیشنهاد میکنیم به جای آن، مراحل شرح داده شده در اینجا را دنبال کنید.
فرآیند سه مرحلهای برای طرحبندی صحیح پیشنمایشها در TextureView:
- اندازه TextureView را طوری تنظیم کنید که با اندازه پیشنمایش انتخابشده یکسان باشد.
- TextureView که احتمالاً کشیده شده است را به ابعاد اصلی پیشنمایش برگردانید.
- TextureView را با استفاده از
displayRotationدر خلاف جهت عقربههای ساعت بچرخانید.
فرض کنید گوشی شما قابلیت چرخش ۹۰ درجهای صفحه نمایش را دارد.

۱. اندازه TextureView را طوری تنظیم کنید که با اندازه پیشنمایش انتخابشده یکسان باشد.
فرض کنید اندازه پیشنمایشی که انتخاب کردهاید previewWidth × previewHeight است که در آن previewWidth > previewHeight (خروجی حسگر ذاتاً به شکل افقی است). هنگام پیکربندی یک جلسه ضبط، باید SurfaceTexture#setDefaultBufferSize(int width, height) را برای تعیین اندازه پیشنمایش ( previewWidth × previewHeight ) فراخوانی کرد.
قبل از فراخوانی تابع setDefaultBufferSize، مهم است که اندازه TextureView را نیز با استفاده از View#setLayoutParams(android.view.ViewGroup.LayoutParams) روی `previewWidth × previewHeight` تنظیم کنید. دلیل این امر این است که TextureView تابع SurfaceTexture#setDefaultBufferSize(int width, height) را با عرض و ارتفاع اندازهگیری شده خود فراخوانی میکند. اگر اندازه TextureView از قبل به صراحت تنظیم نشده باشد، میتواند باعث ایجاد شرایط رقابتی شود. این مشکل با تنظیم صریح اندازه TextureView در ابتدا کاهش مییابد.
حالا ممکن است TextureView با ابعاد منبع مطابقت نداشته باشد. در مورد تلفنها، منبع به شکل عمودی است، اما TextureView به دلیل layoutParams که تنظیم کردهاید، به شکل افقی است. این منجر به پیشنمایشهای کشیده میشود، همانطور که در اینجا نشان داده شده است:

۲. مقیاس TextureView که احتمالاً کشیده شده است را به ابعاد اصلی پیشنمایش برگردانید.
برای اینکه پیشنمایش کشیدهشده را به ابعاد منبع برگردانید، موارد زیر را در نظر بگیرید.
ابعاد منبع ( sourceWidth × sourceHeight ) برابر است با:
-
previewHeight × previewWidth، اگر جهت طبیعی عمودی یا عمودی معکوس باشد (جهت حسگر ۹۰ یا ۲۷۰ درجه باشد) -
previewWidth × previewHeight، اگر جهت طبیعی افقی یا افقی معکوس باشد (جهت حسگر ۰ یا ۱۸۰ درجه باشد)
رفع مشکل کشیدگی با استفاده از View#setScaleX(float) و View#setScaleY(float)
- تنظیم مقیاس X (
sourceWidth / previewWidth) - تنظیم مقیاسY(
sourceHeight / previewHeight)

۳. پیشنمایش را با استفاده از `displayRotation` در خلاف جهت عقربههای ساعت بچرخانید
همانطور که قبلاً گفته شد، باید پیشنمایش را با استفاده از displayRotation در خلاف جهت عقربههای ساعت بچرخانید تا چرخش صفحه نمایش جبران شود.
شما میتوانید این کار را با استفاده از View#setRotation(float) انجام دهید.
- setRotation(
-displayRotation)، زیرا چرخش در جهت عقربههای ساعت انجام میدهد.

نمونه
-
PreviewViewاز camerax در Jetpack طرحبندی TextureView را همانطور که قبلاً توضیح داده شد، مدیریت میکند. این تبدیل را با PreviewCorrector پیکربندی میکند.
توجه: اگر قبلاً در کد خود از ماتریس تبدیل برای TextureView استفاده کردهاید، ممکن است پیشنمایش در دستگاههایی با حالت افقی طبیعی مانند کرومبوکها درست به نظر نرسد. احتمالاً ماتریس تبدیل شما به اشتباه جهت حسگر را ۹۰ یا ۲۷۰ درجه فرض میکند. میتوانید برای یافتن راهحل به این کامیت در گیتهاب مراجعه کنید، اما اکیداً توصیه میکنیم که برنامه خود را به روشی که در اینجا توضیح داده شده است، منتقل کنید.





















