Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9454f3e4d1 | |||
| 7094f9850a | |||
| 7039cf2059 | |||
| 09a551635c | |||
| 72c7c81a42 | |||
| a7287e363c | |||
| 6156f4c897 | |||
| 29be38b88b | |||
| 69d99e1aea | |||
| af9e8158b8 | |||
| 4494d76bb1 | |||
| f1830853c3 | |||
| 75181ecb92 | |||
| 697f996d17 | |||
| 1783058ade | |||
| f78c324f23 | |||
| b1f6f491cd | |||
| 46fe442a4e | |||
| 27c9a7a910 | |||
| f05fc12039 | |||
| b37c3df8c9 | |||
| 353e8a326b | |||
| 9021507995 |
@@ -6,4 +6,6 @@
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.project
|
||||
.settings
|
||||
|
||||
|
||||
+13
-7
@@ -3,15 +3,21 @@ jdk: oraclejdk8
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-m2repository
|
||||
- tools
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-m2repository
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
|
||||
script:
|
||||
- ./gradlew build test
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: ./gradlew bintrayUpload
|
||||
on:
|
||||
tags: true
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
Camera API in Android is hard. Having 2 different API for new and old Camera does not make things any easier. But fret not, that is your lucky day! After several years of working with Camera, we came up with Fotoapparat.
|
||||
|
||||
What it provides:
|
||||
|
||||
- Camera API which does not allow you to shoot yourself in the foot.
|
||||
- Simple yet powerful parameters customization.
|
||||
- Standalone custom `CameraView` which can be integrated into any `Activity`.
|
||||
- Fixes and workarounds for device-specific problems.
|
||||
- Both Kotlin and Java friendly configurations.
|
||||
- Last, but not least, non 0% test coverage.
|
||||
|
||||
- Last, but not least, non 0% test coverage.
|
||||
|
||||
Taking picture becomes as simple as:
|
||||
|
||||
@@ -23,9 +22,9 @@ val fotoapparat = Fotoapparat(
|
||||
context = this,
|
||||
view = cameraView
|
||||
)
|
||||
|
||||
|
||||
fotoapparat.start()
|
||||
|
||||
|
||||
fotoapparat
|
||||
.takePicture()
|
||||
.saveToFile(someFile)
|
||||
@@ -48,7 +47,7 @@ Add `CameraView` to your layout
|
||||
|
||||
Configure `Fotoapparat` instance.
|
||||
|
||||
```kotlin
|
||||
```kotlin
|
||||
Fotoapparat(
|
||||
context = this,
|
||||
view = cameraView, // view which will draw the camera preview
|
||||
@@ -61,12 +60,12 @@ Fotoapparat(
|
||||
),
|
||||
cameraErrorCallback = { error -> } // (optional) log fatal errors
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
Check the [wiki for the `configuration` options e.g. change iso](https://github.com/Fotoapparat/Fotoapparat/wiki/Configuration-Kotlin)
|
||||
|
||||
Are you using Java only? See our [wiki for the java-friendly configuration](https://github.com/Fotoapparat/Fotoapparat/wiki/Configuration-Java).
|
||||
|
||||
|
||||
### Step Three
|
||||
|
||||
Call `start()` and `stop()`. No rocket science here.
|
||||
@@ -76,7 +75,7 @@ override fun onStart() {
|
||||
super.onStart()
|
||||
fotoapparat.start()
|
||||
}
|
||||
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
fotoapparat.stop()
|
||||
@@ -89,30 +88,30 @@ Finally, we are ready to take a picture. You have various options.
|
||||
|
||||
```kotlin
|
||||
val photoResult = fotoapparat.takePicture()
|
||||
|
||||
|
||||
// Asynchronously saves photo to file
|
||||
photoResult.saveToFile(someFile)
|
||||
|
||||
|
||||
// Asynchronously converts photo to bitmap and returns the result on the main thread
|
||||
photoResult
|
||||
.toBitmap()
|
||||
.whenAvailable { bitmapPhoto ->
|
||||
val imageView = (ImageView) findViewById(R.id.result)
|
||||
|
||||
|
||||
imageView.setImageBitmap(bitmapPhoto.bitmap)
|
||||
imageView.setRotation(-bitmapPhoto.rotationDegrees)
|
||||
}
|
||||
|
||||
|
||||
// Of course, you can also get a photo in a blocking way. Do not do it on the main thread though.
|
||||
val result = photoResult.toBitmap().await()
|
||||
|
||||
// Convert asynchronous events to RxJava 1.x/2.x types.
|
||||
// See /fotoapparat-adapters/ module
|
||||
|
||||
// Convert asynchronous events to RxJava 1.x/2.x types.
|
||||
// See /fotoapparat-adapters/ module
|
||||
photoResult
|
||||
.toBitmap()
|
||||
.toSingle()
|
||||
.subscribe { bitmapPhoto ->
|
||||
|
||||
.subscribe { bitmapPhoto ->
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -125,23 +124,23 @@ fotoapparat.updateConfiguration(
|
||||
UpdateConfiguration(
|
||||
flashMode = if (isChecked) torch() else off()
|
||||
// ...
|
||||
// all the parameters available in CameraConfiguration
|
||||
// all the parameters available in CameraConfiguration
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Or alternatively, you may provide updates on an existing full configuration.
|
||||
Or alternatively, you may provide updates on an existing full configuration.
|
||||
|
||||
```kotlin
|
||||
val configuration = CameraConfiguration(
|
||||
// A full configuration
|
||||
// ...
|
||||
)
|
||||
|
||||
|
||||
fotoapparat.updateConfiguration(
|
||||
configuration.copy(
|
||||
flashMode = if (isChecked) torch() else off()
|
||||
// all the parameters available in CameraConfiguration
|
||||
// all the parameters available in CameraConfiguration
|
||||
)
|
||||
)
|
||||
```
|
||||
@@ -162,7 +161,7 @@ fotoapparat.switchTo(
|
||||
Add dependency to your `build.gradle`
|
||||
|
||||
```groovy
|
||||
implementation 'io.fotoapparat:fotoapparat:2.6.0'
|
||||
implementation 'io.fotoapparat:fotoapparat:2.7.0'
|
||||
```
|
||||
|
||||
Camera permission will be automatically added to your `AndroidManifest.xml`. Do not forget to request this permission on Marshmallow and higher.
|
||||
@@ -171,14 +170,12 @@ Camera permission will be automatically added to your `AndroidManifest.xml`. Do
|
||||
|
||||
Optionally, you can check out our other library which adds face detection capabilities - [FaceDetector](https://github.com/Fotoapparat/FaceDetector).
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
We want to say thanks to [Mark Murphy](https://github.com/commonsguy) for the awesome job he did with [CWAC-Camera](https://github.com/commonsguy/cwac-camera). We were using his library for a couple of years and now we feel that Fotoapparat is a next step in the right direction.
|
||||
|
||||
We also want to say many thanks to [Leander Lenzing](http://leanderlenzing.com/) for the amazing icon. Don't forget to follow his work in [dribbble](https://dribbble.com/leanderlenzing).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
|
||||
+6
-10
@@ -2,15 +2,14 @@
|
||||
|
||||
subprojects {
|
||||
ext {
|
||||
artifactVersion = '2.6.0'
|
||||
artifactVersion = '2.7.0'
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
versions = [
|
||||
gradle : '4.10.2',
|
||||
kotlin : '1.3.0',
|
||||
kotlin : '1.3.50',
|
||||
code : 1,
|
||||
name : '1.0.0',
|
||||
sdk : [
|
||||
@@ -19,13 +18,13 @@ buildscript {
|
||||
],
|
||||
android: [
|
||||
buildTools: '28.0.3',
|
||||
appcompat : '1.0.1',
|
||||
annotation : '1.0.0',
|
||||
appcompat : '1.1.0',
|
||||
annotation : '1.1.0',
|
||||
exifinterface : '1.0.0'
|
||||
],
|
||||
rx : [
|
||||
rxJava1: '1.3.8',
|
||||
rxJava2: '2.2.3'
|
||||
rxJava2: '2.2.12'
|
||||
],
|
||||
test : [
|
||||
junit : '4.12',
|
||||
@@ -38,7 +37,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
|
||||
|
||||
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
|
||||
@@ -57,6 +56,3 @@ task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
task wrapper(type: Wrapper) {
|
||||
gradleVersion = versions.gradle
|
||||
}
|
||||
|
||||
+2
-2
@@ -77,8 +77,8 @@ artifacts {
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = project.properties["bintray.user"]
|
||||
key = project.properties["bintray.apikey"]
|
||||
user = project.properties["bintray.user"] ?: System.getenv('BINTRAY_USER')
|
||||
key = project.properties["bintray.apikey"] ?: System.getenv('BINTRAY_API_KEY')
|
||||
|
||||
configurations = ['archives']
|
||||
pkg {
|
||||
|
||||
@@ -14,14 +14,16 @@ android {
|
||||
}
|
||||
|
||||
archivesBaseName = 'adapter-rxjava'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':fotoapparat')
|
||||
compileOnly "io.reactivex:rxjava:${versions.rx.rxJava1}"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
|
||||
|
||||
testImplementation "io.reactivex:rxjava:${versions.rx.rxJava1}"
|
||||
testImplementation "junit:junit:${versions.test.junit}"
|
||||
}
|
||||
|
||||
@@ -14,14 +14,16 @@ android {
|
||||
}
|
||||
|
||||
archivesBaseName = 'adapter-rxjava2'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':fotoapparat')
|
||||
compileOnly "io.reactivex.rxjava2:rxjava:${versions.rx.rxJava2}"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
|
||||
|
||||
testImplementation "io.reactivex.rxjava2:rxjava:${versions.rx.rxJava2}"
|
||||
testImplementation "junit:junit:${versions.test.junit}"
|
||||
}
|
||||
|
||||
@@ -22,14 +22,17 @@ android {
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:${versions.android.annotation}"
|
||||
implementation "androidx.exifinterface:exifinterface:${versions.android.exifinterface}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
|
||||
testImplementation "junit:junit:${versions.test.junit}"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${versions.kotlin}"
|
||||
testImplementation "org.mockito:mockito-core:${versions.test.mockito}"
|
||||
|
||||
@@ -63,10 +63,12 @@ class Fotoapparat
|
||||
executor = executor
|
||||
)
|
||||
|
||||
private val orientationSensor = OrientationSensor(
|
||||
context = context,
|
||||
device = device
|
||||
)
|
||||
private val orientationSensor by lazy {
|
||||
OrientationSensor(
|
||||
context = context,
|
||||
device = device
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
logger.recordMethod()
|
||||
@@ -220,6 +222,7 @@ class Fotoapparat
|
||||
|
||||
executor.execute(Operation(cancellable = true) {
|
||||
device.switchCamera(
|
||||
orientationSensor = orientationSensor,
|
||||
newLensPositionSelector = lensPosition,
|
||||
newConfiguration = cameraConfiguration,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
|
||||
@@ -8,16 +8,22 @@ sealed class LensPosition : Characteristic {
|
||||
/**
|
||||
* The back camera.
|
||||
*/
|
||||
object Back : LensPosition()
|
||||
object Back : LensPosition() {
|
||||
override fun toString(): String = "LensPosition.Back"
|
||||
}
|
||||
|
||||
/**
|
||||
* The front camera.
|
||||
*/
|
||||
object Front : LensPosition()
|
||||
object Front : LensPosition() {
|
||||
override fun toString(): String = "LensPosition.Front"
|
||||
}
|
||||
|
||||
/**
|
||||
* An external camera.
|
||||
*/
|
||||
object External : LensPosition()
|
||||
object External : LensPosition() {
|
||||
override fun toString(): String = "LensPosition.External"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.fotoapparat.concurrent
|
||||
|
||||
import android.os.Looper
|
||||
|
||||
/**
|
||||
* Throws [IllegalThreadStateException] if called from main thread.
|
||||
*/
|
||||
fun ensureBackgroundThread() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
throw IllegalThreadStateException("Operation should not run from main thread.")
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package io.fotoapparat.coroutines
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
|
||||
/**
|
||||
* A [ConflatedBroadcastChannel] which exposes a [getValue] which will [await] for at least one value.
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
internal class AwaitBroadcastChannel<T>(
|
||||
private val channel: ConflatedBroadcastChannel<T> = ConflatedBroadcastChannel(),
|
||||
private val deferred: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||
@@ -31,7 +31,13 @@ internal class AwaitBroadcastChannel<T>(
|
||||
channel.send(element)
|
||||
}
|
||||
|
||||
override fun cancel(cause: CancellationException?) {
|
||||
channel.cancel(cause)
|
||||
deferred.cancel(cause)
|
||||
}
|
||||
|
||||
override fun cancel(cause: Throwable?): Boolean {
|
||||
return channel.cancel(cause) && deferred.cancel(cause)
|
||||
deferred.cancel(cause?.message ?:"", cause)
|
||||
return channel.close(cause)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import io.fotoapparat.exception.camera.CameraException
|
||||
import io.fotoapparat.hardware.metering.FocalRequest
|
||||
import io.fotoapparat.hardware.metering.convert.toFocusAreas
|
||||
import io.fotoapparat.hardware.orientation.*
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
|
||||
import io.fotoapparat.log.Logger
|
||||
import io.fotoapparat.parameter.FocusMode
|
||||
import io.fotoapparat.parameter.Resolution
|
||||
@@ -51,9 +50,9 @@ internal open class CameraDevice(
|
||||
private lateinit var camera: Camera
|
||||
|
||||
private var cachedCameraParameters: Camera.Parameters? = null
|
||||
private var displayOrientation: Orientation = Portrait
|
||||
private var imageOrientation: Orientation = Portrait
|
||||
private var previewOrientation: Orientation = Portrait
|
||||
private lateinit var displayOrientation: Orientation
|
||||
private lateinit var imageOrientation: Orientation
|
||||
private lateinit var previewOrientation: Orientation
|
||||
|
||||
/**
|
||||
* Opens a connection to a camera.
|
||||
@@ -206,9 +205,16 @@ internal open class CameraDevice(
|
||||
cameraIsMirrored = characteristics.isMirrored
|
||||
)
|
||||
|
||||
logger.log("Image orientation is: $imageOrientation. " + lineSeparator +
|
||||
"Display orientation is: $displayOrientation. " + lineSeparator +
|
||||
"Preview orientation is: $previewOrientation."
|
||||
logger.log("Orientations: $lineSeparator" +
|
||||
"Screen orientation (preview) is: ${orientationState.screenOrientation}. " + lineSeparator +
|
||||
"Camera sensor orientation is always at: ${characteristics.cameraOrientation}. " + lineSeparator +
|
||||
"Camera is " + if (characteristics.isMirrored) "mirrored." else "not mirrored."
|
||||
)
|
||||
|
||||
logger.log("Orientation adjustments: $lineSeparator" +
|
||||
"Image orientation will be adjusted by: ${imageOrientation.degrees} degrees. " + lineSeparator +
|
||||
"Display orientation will be adjusted by: ${displayOrientation.degrees} degrees. " + lineSeparator +
|
||||
"Preview orientation will be adjusted by: ${previewOrientation.degrees} degrees."
|
||||
)
|
||||
|
||||
previewStream.frameOrientation = previewOrientation
|
||||
@@ -288,7 +294,6 @@ internal open class CameraDevice(
|
||||
return previewResolution
|
||||
}
|
||||
|
||||
|
||||
private fun setZoomSafely(@FloatRange(from = 0.0, to = 1.0) level: Float) {
|
||||
try {
|
||||
setZoomUnsafe(level)
|
||||
@@ -358,16 +363,13 @@ internal open class CameraDevice(
|
||||
|
||||
private fun Camera.clearFocusingAreas() {
|
||||
parameters = parameters.apply {
|
||||
with(capabilities) {
|
||||
meteringAreas = null
|
||||
focusAreas = null
|
||||
}
|
||||
meteringAreas = null
|
||||
focusAreas = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private const val AUTOFOCUS_TIMEOUT_SECONDS = 3L
|
||||
|
||||
private fun Camera.takePhoto(imageRotation: Int): Photo {
|
||||
|
||||
@@ -155,6 +155,7 @@ internal open class Device(
|
||||
* @return The desired from the user camera lens position.
|
||||
*/
|
||||
open fun getLensPositionSelector(): LensPositionSelector = lensPositionSelector
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +167,8 @@ internal fun updateConfiguration(
|
||||
) = CameraConfiguration(
|
||||
flashMode = newConfiguration.flashMode ?: savedConfiguration.flashMode,
|
||||
focusMode = newConfiguration.focusMode ?: savedConfiguration.focusMode,
|
||||
exposureCompensation = newConfiguration.exposureCompensation ?: savedConfiguration.exposureCompensation,
|
||||
exposureCompensation = newConfiguration.exposureCompensation
|
||||
?: savedConfiguration.exposureCompensation,
|
||||
frameProcessor = newConfiguration.frameProcessor ?: savedConfiguration.frameProcessor,
|
||||
previewFpsRange = newConfiguration.previewFpsRange ?: savedConfiguration.previewFpsRange,
|
||||
sensorSensitivity = newConfiguration.sensorSensitivity
|
||||
|
||||
@@ -18,12 +18,16 @@ sealed class Orientation(
|
||||
/**
|
||||
* A vertical, normal orientation.
|
||||
*/
|
||||
object Portrait : Vertical(0)
|
||||
object Portrait : Vertical(0) {
|
||||
override fun toString(): String = "Orientation.Vertical.Portrait"
|
||||
}
|
||||
|
||||
/**
|
||||
* A reversed (flipped phone) orientation.
|
||||
*/
|
||||
object ReversePortrait : Vertical(180)
|
||||
object ReversePortrait : Vertical(180) {
|
||||
override fun toString(): String = "Orientation.Vertical.ReversePortrait"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,12 +39,16 @@ sealed class Orientation(
|
||||
/**
|
||||
* A 90 degrees clockwise from "normal", orientation.
|
||||
*/
|
||||
object Landscape : Horizontal(90)
|
||||
object Landscape : Horizontal(90) {
|
||||
override fun toString(): String = "Orientation.Horizontal.Landscape"
|
||||
}
|
||||
|
||||
/**
|
||||
* A 90 degrees counter-clockwise from "normal", orientation.
|
||||
*/
|
||||
object ReverseLandscape : Horizontal(270)
|
||||
object ReverseLandscape : Horizontal(270) {
|
||||
override fun toString(): String = "Orientation.Horizontal.ReverseLandscape"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+15
-10
@@ -12,23 +12,28 @@ internal open class OrientationSensor(
|
||||
private val device: Device
|
||||
) {
|
||||
|
||||
private lateinit var listener: (OrientationState) -> Unit
|
||||
private val onOrientationChanged: (DeviceRotationDegrees) -> Unit = { deviceRotation ->
|
||||
deviceRotation.toClosestRightAngle()
|
||||
.toOrientation()
|
||||
.takeIf { it != lastKnownDeviceOrientation }
|
||||
?.let {
|
||||
val state = OrientationState(
|
||||
deviceOrientation = it,
|
||||
screenOrientation = device.getScreenOrientation()
|
||||
.let { deviceOrientation ->
|
||||
val screenOrientation = device.getScreenOrientation()
|
||||
|
||||
val newState = OrientationState(
|
||||
deviceOrientation = deviceOrientation,
|
||||
screenOrientation = screenOrientation
|
||||
)
|
||||
|
||||
lastKnownDeviceOrientation = state.deviceOrientation
|
||||
listener(state)
|
||||
if (newState != lastKnownOrientationState) {
|
||||
lastKnownOrientationState = newState
|
||||
listener(newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var listener: (OrientationState) -> Unit
|
||||
private var lastKnownDeviceOrientation: Orientation = Portrait
|
||||
open var lastKnownOrientationState: OrientationState = OrientationState(
|
||||
deviceOrientation = Portrait,
|
||||
screenOrientation = device.getScreenOrientation()
|
||||
)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
|
||||
@@ -8,21 +8,29 @@ sealed class AntiBandingMode : Parameter {
|
||||
/**
|
||||
* Auto adjust. This should be the default.
|
||||
*/
|
||||
object Auto : AntiBandingMode()
|
||||
object Auto : AntiBandingMode() {
|
||||
override fun toString(): String = "AntiBandingMode.Auto"
|
||||
}
|
||||
|
||||
/**
|
||||
* Anti Banding is set to 50Hz light frequency.
|
||||
*/
|
||||
object HZ50 : AntiBandingMode()
|
||||
object HZ50 : AntiBandingMode() {
|
||||
override fun toString(): String = "AntiBandingMode.HZ50"
|
||||
}
|
||||
|
||||
/**
|
||||
* Anti Banding is set to 60Hz light frequency.
|
||||
*/
|
||||
object HZ60 : AntiBandingMode()
|
||||
object HZ60 : AntiBandingMode() {
|
||||
override fun toString(): String = "AntiBandingMode.HZ60"
|
||||
}
|
||||
|
||||
/**
|
||||
* Anti Banding is not supported or ignored.
|
||||
*/
|
||||
object None : AntiBandingMode()
|
||||
object None : AntiBandingMode() {
|
||||
override fun toString(): String = "AntiBandingMode.None"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,27 +8,37 @@ sealed class Flash : Parameter {
|
||||
/**
|
||||
* Camera flash will not fire.
|
||||
*/
|
||||
object Off : Flash()
|
||||
object Off : Flash() {
|
||||
override fun toString(): String = "Flash.Off"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera flash will always fire regardless light conditions.
|
||||
*/
|
||||
object On : Flash()
|
||||
object On : Flash() {
|
||||
override fun toString(): String = "Flash.On"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera flash will fire only in low light conditions.
|
||||
*/
|
||||
object Auto : Flash()
|
||||
object Auto : Flash() {
|
||||
override fun toString(): String = "Flash.Auto"
|
||||
}
|
||||
|
||||
/**
|
||||
* If deemed necessary by the camera device, a red eye reduction flash will fire during the
|
||||
* precapture sequence in low light conditions.
|
||||
*/
|
||||
object AutoRedEye : Flash()
|
||||
object AutoRedEye : Flash() {
|
||||
override fun toString(): String = "Flash.AutoRedEye"
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition flash to continuously on.
|
||||
*/
|
||||
object Torch : Flash()
|
||||
object Torch : Flash() {
|
||||
override fun toString(): String = "Flash.Torch"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,40 +8,54 @@ sealed class FocusMode : Parameter {
|
||||
/**
|
||||
* Focus is not adjustable. It is always used by devices which do not support auto-focus.
|
||||
*/
|
||||
object Fixed : FocusMode()
|
||||
object Fixed : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.Fixed"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera is focused at infinity.
|
||||
*/
|
||||
object Infinity : FocusMode()
|
||||
object Infinity : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.Infinity"
|
||||
}
|
||||
|
||||
/**
|
||||
* Macro focus mode.
|
||||
*/
|
||||
object Macro : FocusMode()
|
||||
object Macro : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.Macro"
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto focus. Camera is trying to focus automatically when manually requested.
|
||||
*/
|
||||
object Auto : FocusMode()
|
||||
object Auto : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.Auto"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera is constantly trying to stay in focus.
|
||||
*
|
||||
* The speed of focus change is more aggressive than [ContinuousFocusVideo].
|
||||
*/
|
||||
object ContinuousFocusPicture : FocusMode()
|
||||
object ContinuousFocusPicture : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.ContinuousFocusPicture"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera is constantly trying to stay in focus.
|
||||
*
|
||||
* The speed of focus change is smoother than [ContinuousFocusPicture].
|
||||
*/
|
||||
object ContinuousFocusVideo : FocusMode()
|
||||
object ContinuousFocusVideo : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.ContinuousFocusVideo"
|
||||
}
|
||||
|
||||
/**
|
||||
* The camera device will produce images with an extended depth of field.
|
||||
*/
|
||||
object Edof : FocusMode()
|
||||
object Edof : FocusMode() {
|
||||
override fun toString(): String = "FocusMode.Edof"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,5 @@ data class FpsRange(
|
||||
/**
|
||||
* `true` if the current range is fixed (min == max).
|
||||
*/
|
||||
val isFixed by lazy {
|
||||
max == min
|
||||
}
|
||||
val isFixed get() = max == min
|
||||
}
|
||||
@@ -13,18 +13,17 @@ data class Resolution(
|
||||
/**
|
||||
* The total area this [Resolution] is covering.
|
||||
*/
|
||||
val area: Int by lazy { width * height }
|
||||
val area: Int get() = width * height
|
||||
|
||||
/**
|
||||
* The aspect ratio for this size. [Float.NaN] if invalid dimensions.
|
||||
*/
|
||||
val aspectRatio: Float by lazy {
|
||||
when {
|
||||
val aspectRatio: Float
|
||||
get() = when {
|
||||
width == 0 -> Float.NaN
|
||||
height == 0 -> Float.NaN
|
||||
else -> width.toFloat() / height
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return new instance of [Resolution] with width and height being swapped.
|
||||
|
||||
@@ -15,6 +15,13 @@ enum class ScaleType {
|
||||
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
|
||||
* smaller than the corresponding dimension of the view
|
||||
*/
|
||||
CenterInside
|
||||
CenterInside,
|
||||
|
||||
|
||||
/**
|
||||
* The preview will be scaled so as its one dimensions will be equal and the other one equal or
|
||||
* larger than the corresponding dimension of the view with focus on the top part
|
||||
*/
|
||||
TopCrop,
|
||||
|
||||
}
|
||||
@@ -8,7 +8,9 @@ sealed class Zoom {
|
||||
/**
|
||||
* The camera can only support one, fixed zoom level.
|
||||
*/
|
||||
object FixedZoom : Zoom()
|
||||
object FixedZoom : Zoom() {
|
||||
override fun toString(): String = "Zoom.FixedZoom"
|
||||
}
|
||||
|
||||
/**
|
||||
* The camera can only support a variable zoom level between (and including) 0 and [maxZoom] values.
|
||||
@@ -16,6 +18,8 @@ sealed class Zoom {
|
||||
* 2.7x is returned as 270. The number of elements is [maxZoom] + 1. List is sorted from small to
|
||||
* large. First element is always 100. The last element is the zoom ratio of the maximum zoom value.
|
||||
*/
|
||||
data class VariableZoom(val maxZoom: Int, val zoomRatios: List<Int>) : Zoom()
|
||||
data class VariableZoom(val maxZoom: Int, val zoomRatios: List<Int>) : Zoom() {
|
||||
override fun toString(): String = "Zoom.VariableZoom(maxZoom=$maxZoom, zoomRatios=$zoomRatios)"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,11 +8,15 @@ sealed class FocusResult {
|
||||
/**
|
||||
* Camera is unable to focus for some reason.
|
||||
*/
|
||||
object UnableToFocus : FocusResult()
|
||||
object UnableToFocus : FocusResult() {
|
||||
override fun toString(): String = "FocusResult.UnableToFocus"
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera is focused successfully.
|
||||
*/
|
||||
object Focused : FocusResult()
|
||||
object Focused : FocusResult() {
|
||||
override fun toString(): String = "FocusResult.Focused"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.fotoapparat.result
|
||||
|
||||
import io.fotoapparat.capability.Capabilities
|
||||
import io.fotoapparat.concurrent.ensureBackgroundThread
|
||||
import io.fotoapparat.exception.UnableToDecodeBitmapException
|
||||
import io.fotoapparat.hardware.executeMainThread
|
||||
import io.fotoapparat.hardware.pendingResultExecutor
|
||||
@@ -19,7 +20,10 @@ internal constructor(
|
||||
private val executor: Executor
|
||||
) {
|
||||
private val resultUnsafe: T
|
||||
get() = future.get()
|
||||
get() {
|
||||
ensureBackgroundThread()
|
||||
return future.get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms result from one type to another.
|
||||
@@ -68,16 +72,24 @@ internal constructor(
|
||||
fun whenAvailable(callback: (T?) -> Unit) {
|
||||
executor.execute {
|
||||
try {
|
||||
resultUnsafe.notifyCallbackOnMainThread(callback)
|
||||
val result = resultUnsafe
|
||||
notifyOnMainThread {
|
||||
callback(result)
|
||||
}
|
||||
} catch (e: UnableToDecodeBitmapException) {
|
||||
logger.log("Couldn't decode bitmap from byte array")
|
||||
notifyOnMainThread {
|
||||
callback(null)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
logger.log("Couldn't deliver pending result: Camera stopped before delivering result.")
|
||||
} catch (e: CancellationException) {
|
||||
logger.log("Couldn't deliver pending result: Camera operation was cancelled.")
|
||||
} catch (e: ExecutionException) {
|
||||
logger.log("Couldn't deliver pending result: Operation failed internally.")
|
||||
callback(null)
|
||||
notifyOnMainThread {
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,9 +117,9 @@ internal constructor(
|
||||
|
||||
}
|
||||
|
||||
private fun <T> T.notifyCallbackOnMainThread(callback: (T) -> Unit) {
|
||||
private fun notifyOnMainThread(function: () -> Unit) {
|
||||
executeMainThread {
|
||||
callback(this)
|
||||
function()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.fotoapparat.result
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import java.util.*
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* Taken photo.
|
||||
@@ -27,22 +27,20 @@ data class Photo(
|
||||
/**
|
||||
* The height of the photo.
|
||||
*/
|
||||
val height by lazy { decodedBounds.height }
|
||||
val height: Int
|
||||
get() = decodedBounds.outHeight
|
||||
|
||||
/**
|
||||
* The width of the photo.
|
||||
*/
|
||||
val width by lazy { decodedBounds.width }
|
||||
val width: Int
|
||||
get() = decodedBounds.outWidth
|
||||
|
||||
private val decodedBounds by lazy {
|
||||
BitmapFactory.decodeByteArray(
|
||||
encodedImage,
|
||||
0,
|
||||
encodedImage.size,
|
||||
BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
)
|
||||
BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
BitmapFactory.decodeByteArray(encodedImage, 0, encodedImage.size, this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -63,14 +61,14 @@ data class Photo(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"Photo(encodedImage=ByteArray(${encodedImage.size}) rotationDegrees=$rotationDegrees)"
|
||||
|
||||
companion object {
|
||||
|
||||
private val EMPTY by lazy { Photo(encodedImage = ByteArray(0), rotationDegrees = 0) }
|
||||
|
||||
/**
|
||||
* @return empty [Photo].
|
||||
*/
|
||||
internal fun empty(): Photo = EMPTY
|
||||
internal fun empty(): Photo = Photo(encodedImage = ByteArray(0), rotationDegrees = 0)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -23,7 +23,8 @@ internal class BitmapPhotoTransformer(
|
||||
desiredResolution = desiredResolution
|
||||
)
|
||||
|
||||
val decodedBitmap = input.decodeBitmap(scaleFactor) ?: throw UnableToDecodeBitmapException()
|
||||
val decodedBitmap = input.decodeBitmap(scaleFactor, originalResolution, desiredResolution)
|
||||
?: throw UnableToDecodeBitmapException()
|
||||
|
||||
val bitmap = if (decodedBitmap.width == desiredResolution.width && decodedBitmap.height == desiredResolution.height) {
|
||||
decodedBitmap
|
||||
@@ -44,14 +45,28 @@ internal class BitmapPhotoTransformer(
|
||||
|
||||
}
|
||||
|
||||
private fun Photo.decodeBitmap(scaleFactor: Float): Bitmap? {
|
||||
private fun Photo.decodeBitmap(
|
||||
scaleFactor: Float,
|
||||
originalResolution: Resolution,
|
||||
desiredResolution: Resolution
|
||||
): Bitmap? {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inSampleSize = scaleFactor.toInt()
|
||||
options.inScaled = true
|
||||
|
||||
if (desiredResolution.width > desiredResolution.height) {
|
||||
options.inDensity = originalResolution.width
|
||||
options.inTargetDensity = desiredResolution.width * options.inSampleSize
|
||||
} else {
|
||||
options.inDensity = originalResolution.height
|
||||
options.inTargetDensity = desiredResolution.height * options.inSampleSize
|
||||
}
|
||||
|
||||
return BitmapFactory.decodeByteArray(
|
||||
encodedImage,
|
||||
0,
|
||||
encodedImage.size
|
||||
encodedImage.size,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ import io.fotoapparat.concurrent.CameraExecutor.Operation
|
||||
import io.fotoapparat.error.CameraErrorCallback
|
||||
import io.fotoapparat.exception.camera.CameraException
|
||||
import io.fotoapparat.hardware.Device
|
||||
import io.fotoapparat.hardware.orientation.Orientation
|
||||
import io.fotoapparat.hardware.orientation.OrientationSensor
|
||||
import io.fotoapparat.hardware.orientation.OrientationState
|
||||
import io.fotoapparat.routine.focus.focusOnPoint
|
||||
import io.fotoapparat.routine.orientation.startOrientationMonitoring
|
||||
import java.io.IOException
|
||||
@@ -23,7 +21,9 @@ internal fun Device.bootStart(
|
||||
}
|
||||
|
||||
try {
|
||||
start()
|
||||
start(
|
||||
orientationSensor = orientationSensor
|
||||
)
|
||||
startOrientationMonitoring(
|
||||
orientationSensor = orientationSensor
|
||||
)
|
||||
@@ -35,7 +35,7 @@ internal fun Device.bootStart(
|
||||
/**
|
||||
* Starts the camera.
|
||||
*/
|
||||
internal fun Device.start() {
|
||||
internal fun Device.start(orientationSensor: OrientationSensor) {
|
||||
selectCamera()
|
||||
|
||||
val cameraDevice = getSelectedCamera().apply {
|
||||
@@ -44,12 +44,7 @@ internal fun Device.start() {
|
||||
updateCameraConfiguration(
|
||||
cameraDevice = this
|
||||
)
|
||||
setDisplayOrientation(
|
||||
orientationState = OrientationState(
|
||||
deviceOrientation = Orientation.Vertical.Portrait,
|
||||
screenOrientation = getScreenOrientation()
|
||||
)
|
||||
)
|
||||
setDisplayOrientation(orientationSensor.lastKnownOrientationState)
|
||||
}
|
||||
|
||||
val previewResolution = cameraDevice.getPreviewResolution()
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.fotoapparat.error.CameraErrorCallback
|
||||
import io.fotoapparat.exception.camera.CameraException
|
||||
import io.fotoapparat.hardware.CameraDevice
|
||||
import io.fotoapparat.hardware.Device
|
||||
import io.fotoapparat.hardware.orientation.OrientationSensor
|
||||
import io.fotoapparat.selector.LensPositionSelector
|
||||
|
||||
/**
|
||||
@@ -16,7 +17,8 @@ import io.fotoapparat.selector.LensPositionSelector
|
||||
internal fun Device.switchCamera(
|
||||
newLensPositionSelector: LensPositionSelector,
|
||||
newConfiguration: CameraConfiguration,
|
||||
mainThreadErrorCallback: CameraErrorCallback
|
||||
mainThreadErrorCallback: CameraErrorCallback,
|
||||
orientationSensor: OrientationSensor
|
||||
) {
|
||||
val oldCameraDevice = try {
|
||||
getSelectedCamera()
|
||||
@@ -33,6 +35,7 @@ internal fun Device.switchCamera(
|
||||
|
||||
restartPreview(
|
||||
oldCameraDevice,
|
||||
orientationSensor,
|
||||
mainThreadErrorCallback
|
||||
)
|
||||
}
|
||||
@@ -43,12 +46,13 @@ internal fun Device.switchCamera(
|
||||
*/
|
||||
internal fun Device.restartPreview(
|
||||
oldCameraDevice: CameraDevice,
|
||||
orientationSensor: OrientationSensor,
|
||||
mainThreadErrorCallback: CameraErrorCallback
|
||||
) {
|
||||
stop(oldCameraDevice)
|
||||
|
||||
try {
|
||||
start()
|
||||
start(orientationSensor)
|
||||
} catch (e: CameraException) {
|
||||
mainThreadErrorCallback(e)
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ private fun ViewGroup.layoutTextureView(
|
||||
) = when (scaleType) {
|
||||
ScaleType.CenterInside -> previewResolution?.centerInside(this)
|
||||
ScaleType.CenterCrop -> previewResolution?.centerCrop(this)
|
||||
ScaleType.TopCrop -> previewResolution?.topCrop(this)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -129,6 +130,28 @@ private fun Resolution.centerCrop(view: ViewGroup) {
|
||||
view.layoutChildrenAt(rect)
|
||||
}
|
||||
|
||||
private fun Resolution.topCrop(view: ViewGroup) {
|
||||
val scale = Math.max(
|
||||
view.measuredWidth / width.toFloat(),
|
||||
view.measuredHeight / height.toFloat()
|
||||
)
|
||||
|
||||
val width = (width * scale).toInt()
|
||||
val height = (height * scale).toInt()
|
||||
|
||||
val extraX = Math.max(0, width - view.measuredWidth)
|
||||
|
||||
val rect = Rect(
|
||||
-extraX / 2,
|
||||
0,
|
||||
width - extraX / 2,
|
||||
height
|
||||
)
|
||||
|
||||
view.layoutChildrenAt(rect)
|
||||
}
|
||||
|
||||
|
||||
private fun ViewGroup.layoutChildrenAt(rect: Rect) {
|
||||
(0 until childCount).forEach {
|
||||
getChildAt(it).layout(
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.widget.FrameLayout
|
||||
import io.fotoapparat.hardware.metering.FocalRequest
|
||||
import io.fotoapparat.hardware.metering.PointF
|
||||
@@ -15,7 +16,7 @@ import io.fotoapparat.parameter.Resolution
|
||||
*
|
||||
* If the camera doesn't support focus metering on specific area this will only display a visual feedback.
|
||||
*/
|
||||
class FocusView
|
||||
open class FocusView
|
||||
@JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -25,6 +26,15 @@ class FocusView
|
||||
private val visualFeedbackCircle = FeedbackCircleView(context, attrs, defStyleAttr)
|
||||
private var focusMeteringListener: ((FocalRequest) -> Unit)? = null
|
||||
|
||||
var scaleListener: ((Float) -> Unit)? = null
|
||||
var ptrListener: ((Int) -> Unit)? = null
|
||||
|
||||
private var mPtrCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
ptrListener?.invoke(value)
|
||||
}
|
||||
|
||||
init {
|
||||
clipToPadding = false
|
||||
clipChildren = false
|
||||
@@ -38,6 +48,14 @@ class FocusView
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
tapDetector.onTouchEvent(event)
|
||||
scaleDetector.onTouchEvent(event)
|
||||
|
||||
when (event.action and MotionEvent.ACTION_MASK) {
|
||||
MotionEvent.ACTION_POINTER_DOWN -> mPtrCount++
|
||||
MotionEvent.ACTION_POINTER_UP -> mPtrCount--
|
||||
MotionEvent.ACTION_DOWN -> mPtrCount++
|
||||
MotionEvent.ACTION_UP -> mPtrCount--
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -64,4 +82,15 @@ class FocusView
|
||||
}
|
||||
|
||||
private val tapDetector = GestureDetector(context, gestureDetectorListener)
|
||||
|
||||
private val scaleGestureDetector = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
return scaleListener
|
||||
?.let {
|
||||
it(detector.scaleFactor)
|
||||
true
|
||||
}?: super.onScale(detector)
|
||||
}
|
||||
}
|
||||
private val scaleDetector = ScaleGestureDetector(context, scaleGestureDetector)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Fotoapparat</string>
|
||||
</resources>
|
||||
+3
-1
@@ -2,6 +2,7 @@ package io.fotoapparat.hardware.orientation
|
||||
|
||||
import io.fotoapparat.hardware.Device
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Vertical.ReversePortrait
|
||||
import io.fotoapparat.test.willReturn
|
||||
import org.junit.Before
|
||||
@@ -25,6 +26,7 @@ internal class OrientationSensorTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
device.getScreenOrientation() willReturn Portrait
|
||||
testee = OrientationSensor(
|
||||
rotationListener,
|
||||
device
|
||||
@@ -36,7 +38,7 @@ internal class OrientationSensorTest {
|
||||
// Given
|
||||
|
||||
// When
|
||||
testee.start({})
|
||||
testee.start {}
|
||||
testee.stop()
|
||||
|
||||
// Then
|
||||
|
||||
@@ -4,10 +4,7 @@ import android.graphics.SurfaceTexture
|
||||
import io.fotoapparat.exception.camera.CameraException
|
||||
import io.fotoapparat.hardware.CameraDevice
|
||||
import io.fotoapparat.hardware.Device
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Vertical.Portrait
|
||||
import io.fotoapparat.hardware.orientation.OrientationSensor
|
||||
import io.fotoapparat.hardware.orientation.OrientationState
|
||||
import io.fotoapparat.log.Logger
|
||||
import io.fotoapparat.parameter.ScaleType
|
||||
import io.fotoapparat.test.testResolution
|
||||
@@ -55,7 +52,6 @@ internal class StartRoutineTest {
|
||||
// Given
|
||||
device.apply {
|
||||
getSelectedCamera() willReturn cameraDevice
|
||||
getScreenOrientation() willReturn Landscape
|
||||
cameraRenderer willReturn cameraViewRenderer
|
||||
scaleType willReturn ScaleType.CenterCrop
|
||||
}
|
||||
@@ -63,7 +59,7 @@ internal class StartRoutineTest {
|
||||
cameraViewRenderer.getPreview() willReturn preview
|
||||
|
||||
// When
|
||||
device.start()
|
||||
device.start(orientationSensor)
|
||||
|
||||
// Then
|
||||
val inOrder = inOrder(
|
||||
@@ -76,10 +72,7 @@ internal class StartRoutineTest {
|
||||
verify(device).selectCamera()
|
||||
verify(cameraDevice).open()
|
||||
verify(device).updateCameraConfiguration(cameraDevice)
|
||||
verify(cameraDevice).setDisplayOrientation(OrientationState(
|
||||
Portrait,
|
||||
Landscape
|
||||
))
|
||||
verify(cameraDevice).setDisplayOrientation(orientationSensor.lastKnownOrientationState)
|
||||
verify(cameraViewRenderer).setScaleType(ScaleType.CenterCrop)
|
||||
verify(cameraViewRenderer).setPreviewResolution(testResolution)
|
||||
verify(cameraDevice).setDisplaySurface(preview)
|
||||
@@ -92,7 +85,6 @@ internal class StartRoutineTest {
|
||||
// Given
|
||||
device.apply {
|
||||
getSelectedCamera() willReturn cameraDevice
|
||||
getScreenOrientation() willReturn Landscape
|
||||
cameraRenderer willReturn cameraViewRenderer
|
||||
scaleType willReturn ScaleType.CenterCrop
|
||||
logger willReturn mockLogger
|
||||
@@ -102,7 +94,7 @@ internal class StartRoutineTest {
|
||||
cameraViewRenderer.getPreview() willReturn preview
|
||||
|
||||
// When
|
||||
device.start()
|
||||
device.start(orientationSensor)
|
||||
|
||||
// Then
|
||||
verify(cameraDevice, never()).startPreview()
|
||||
@@ -115,8 +107,8 @@ internal class StartRoutineTest {
|
||||
|
||||
// When
|
||||
device.bootStart(
|
||||
orientationSensor,
|
||||
{}
|
||||
orientationSensor = orientationSensor,
|
||||
mainThreadErrorCallback = {}
|
||||
)
|
||||
|
||||
// Then
|
||||
@@ -131,8 +123,8 @@ internal class StartRoutineTest {
|
||||
|
||||
// When
|
||||
device.bootStart(
|
||||
orientationSensor,
|
||||
{ hasErrors.set(true) }
|
||||
orientationSensor = orientationSensor,
|
||||
mainThreadErrorCallback = { hasErrors.set(true) }
|
||||
)
|
||||
|
||||
// Then
|
||||
@@ -147,7 +139,6 @@ internal class StartRoutineTest {
|
||||
device.apply {
|
||||
hasSelectedCamera() willReturn false
|
||||
getSelectedCamera() willReturn cameraDevice
|
||||
getScreenOrientation() willReturn Landscape
|
||||
cameraRenderer willReturn cameraViewRenderer
|
||||
scaleType willReturn ScaleType.CenterCrop
|
||||
}
|
||||
@@ -156,14 +147,14 @@ internal class StartRoutineTest {
|
||||
|
||||
// When
|
||||
device.bootStart(
|
||||
orientationSensor,
|
||||
{ hasErrors = true }
|
||||
orientationSensor = orientationSensor,
|
||||
mainThreadErrorCallback = { hasErrors = true }
|
||||
)
|
||||
|
||||
// Then
|
||||
assertFalse(hasErrors)
|
||||
|
||||
verify(device).start()
|
||||
verify(device).start(orientationSensor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-15
@@ -4,11 +4,10 @@ import io.fotoapparat.characteristic.LensPosition
|
||||
import io.fotoapparat.error.CameraErrorCallback
|
||||
import io.fotoapparat.hardware.CameraDevice
|
||||
import io.fotoapparat.hardware.Device
|
||||
import io.fotoapparat.hardware.orientation.Orientation.Horizontal.Landscape
|
||||
import io.fotoapparat.hardware.orientation.OrientationSensor
|
||||
import io.fotoapparat.test.testConfiguration
|
||||
import io.fotoapparat.test.willReturn
|
||||
import io.fotoapparat.view.CameraRenderer
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.BDDMockito.given
|
||||
@@ -27,14 +26,11 @@ internal class SwitchCameraRoutineTest {
|
||||
lateinit var device: Device
|
||||
@Mock
|
||||
lateinit var cameraRenderer: CameraRenderer
|
||||
@Mock
|
||||
lateinit var orientationSensor: OrientationSensor
|
||||
|
||||
private val mainThreadErrorCallback: CameraErrorCallback = {}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
device.getScreenOrientation() willReturn Landscape
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switch camera, not started`() {
|
||||
// Given
|
||||
@@ -48,13 +44,14 @@ internal class SwitchCameraRoutineTest {
|
||||
device.switchCamera(
|
||||
newLensPositionSelector = lensPositionSelector,
|
||||
newConfiguration = testConfiguration,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
mainThreadErrorCallback = mainThreadErrorCallback,
|
||||
orientationSensor = orientationSensor
|
||||
)
|
||||
|
||||
// Then
|
||||
verify(device).updateLensPositionSelector(lensPositionSelector)
|
||||
verify(device).updateConfiguration(testConfiguration)
|
||||
verify(device, never()).restartPreview(oldCameraDevice, mainThreadErrorCallback)
|
||||
verify(device, never()).restartPreview(oldCameraDevice, orientationSensor, mainThreadErrorCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,7 +66,8 @@ internal class SwitchCameraRoutineTest {
|
||||
device.switchCamera(
|
||||
newLensPositionSelector = lensPositionSelector,
|
||||
newConfiguration = testConfiguration,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
mainThreadErrorCallback = mainThreadErrorCallback,
|
||||
orientationSensor = orientationSensor
|
||||
)
|
||||
|
||||
// Then
|
||||
@@ -80,7 +78,7 @@ internal class SwitchCameraRoutineTest {
|
||||
inOrder.apply {
|
||||
verify(device, never()).updateLensPositionSelector(lensPositionSelector)
|
||||
verify(device, never()).updateConfiguration(testConfiguration)
|
||||
verify(device, never()).restartPreview(oldCameraDevice, mainThreadErrorCallback)
|
||||
verify(device, never()).restartPreview(oldCameraDevice, orientationSensor, mainThreadErrorCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +94,8 @@ internal class SwitchCameraRoutineTest {
|
||||
device.switchCamera(
|
||||
newLensPositionSelector = lensPositionSelector,
|
||||
newConfiguration = testConfiguration,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
mainThreadErrorCallback = mainThreadErrorCallback,
|
||||
orientationSensor = orientationSensor
|
||||
)
|
||||
|
||||
// Then
|
||||
@@ -107,7 +106,7 @@ internal class SwitchCameraRoutineTest {
|
||||
inOrder.apply {
|
||||
verify(device).updateLensPositionSelector(lensPositionSelector)
|
||||
verify(device).updateConfiguration(testConfiguration)
|
||||
verify(device).restartPreview(oldCameraDevice, mainThreadErrorCallback)
|
||||
verify(device).restartPreview(oldCameraDevice, orientationSensor, mainThreadErrorCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +119,7 @@ internal class SwitchCameraRoutineTest {
|
||||
// When
|
||||
device.restartPreview(
|
||||
oldCameraDevice = oldCameraDevice,
|
||||
orientationSensor = orientationSensor,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ internal class SwitchCameraRoutineTest {
|
||||
|
||||
inOrder.apply {
|
||||
verify(device).stop(oldCameraDevice)
|
||||
verify(device).start()
|
||||
verify(device).start(orientationSensor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ internal class SwitchCameraRoutineTest {
|
||||
// When
|
||||
device.restartPreview(
|
||||
oldCameraDevice = oldCameraDevice,
|
||||
orientationSensor = orientationSensor,
|
||||
mainThreadErrorCallback = mainThreadErrorCallback
|
||||
)
|
||||
|
||||
@@ -148,7 +149,7 @@ internal class SwitchCameraRoutineTest {
|
||||
val inOrder = inOrder(device)
|
||||
inOrder.apply {
|
||||
verify(device).stop(oldCameraDevice)
|
||||
verify(device).start()
|
||||
verify(device).start(orientationSensor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||
|
||||
@@ -25,6 +25,5 @@ android {
|
||||
dependencies {
|
||||
implementation project(':fotoapparat')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:${versions.android.appcompat}"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="io.fotoapparat.sample"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.fotoapparat.sample">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<!--Set android:name to ".ActivityJava" for java example-->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="fullSensor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
@@ -5,23 +5,26 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import io.fotoapparat.Fotoapparat;
|
||||
import io.fotoapparat.capability.Capabilities;
|
||||
import io.fotoapparat.configuration.CameraConfiguration;
|
||||
import io.fotoapparat.configuration.UpdateConfiguration;
|
||||
import io.fotoapparat.error.CameraErrorListener;
|
||||
import io.fotoapparat.exception.camera.CameraException;
|
||||
import io.fotoapparat.parameter.ScaleType;
|
||||
import io.fotoapparat.parameter.Zoom;
|
||||
import io.fotoapparat.preview.Frame;
|
||||
import io.fotoapparat.preview.FrameProcessor;
|
||||
import io.fotoapparat.result.BitmapPhoto;
|
||||
@@ -29,6 +32,8 @@ import io.fotoapparat.result.PhotoResult;
|
||||
import io.fotoapparat.result.WhenDoneListener;
|
||||
import io.fotoapparat.view.CameraView;
|
||||
import io.fotoapparat.view.FocusView;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
import static io.fotoapparat.log.LoggersKt.fileLogger;
|
||||
import static io.fotoapparat.log.LoggersKt.logcat;
|
||||
@@ -57,9 +62,13 @@ public class ActivityJava extends AppCompatActivity {
|
||||
private boolean hasCameraPermission;
|
||||
private CameraView cameraView;
|
||||
private FocusView focusView;
|
||||
private TextView zoomLvl;
|
||||
private ImageView switchCamera;
|
||||
private View capture;
|
||||
|
||||
private Fotoapparat fotoapparat;
|
||||
private Zoom.VariableZoom cameraZoom;
|
||||
private float curZoom = 0f;
|
||||
|
||||
boolean activeCameraBack = true;
|
||||
|
||||
@@ -92,6 +101,8 @@ public class ActivityJava extends AppCompatActivity {
|
||||
cameraView = findViewById(R.id.cameraView);
|
||||
focusView = findViewById(R.id.focusView);
|
||||
capture = findViewById(R.id.capture);
|
||||
zoomLvl = findViewById(R.id.zoomLvl);
|
||||
switchCamera = findViewById(R.id.switchCamera);
|
||||
hasCameraPermission = permissionsDelegate.hasCameraPermission();
|
||||
|
||||
if (hasCameraPermission) {
|
||||
@@ -105,7 +116,6 @@ public class ActivityJava extends AppCompatActivity {
|
||||
takePictureOnClick();
|
||||
switchCameraOnClick();
|
||||
toggleTorchOnSwitch();
|
||||
zoomSeekBar();
|
||||
}
|
||||
|
||||
private Fotoapparat createFotoapparat() {
|
||||
@@ -129,15 +139,66 @@ public class ActivityJava extends AppCompatActivity {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void zoomSeekBar() {
|
||||
SeekBar seekBar = findViewById(R.id.zoomSeekBar);
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new OnProgressChanged() {
|
||||
private void adjustViewsVisibility() {
|
||||
fotoapparat.getCapabilities().whenAvailable(new Function1<Capabilities, Unit>() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
fotoapparat.setZoom(progress / (float) seekBar.getMax());
|
||||
public Unit invoke(Capabilities capabilities) {
|
||||
Zoom zoom = capabilities != null ? capabilities.getZoom() : null;
|
||||
if(zoom instanceof Zoom.VariableZoom){
|
||||
cameraZoom = (Zoom.VariableZoom) zoom;
|
||||
focusView.setScaleListener(new Function1<Float, Unit>() {
|
||||
@Override
|
||||
public Unit invoke(Float aFloat) {
|
||||
scaleZoom(aFloat);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
focusView.setPtrListener(new Function1<Integer, Unit>() {
|
||||
@Override
|
||||
public Unit invoke(Integer integer) {
|
||||
pointerChanged(integer);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
zoomLvl.setVisibility(View.GONE);
|
||||
focusView.setScaleListener(null);
|
||||
focusView.setPtrListener(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
if (fotoapparat.isAvailable(front())){
|
||||
switchCamera.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
switchCamera.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleZoom(float scaleFactor){
|
||||
float plusZoom = 0;
|
||||
if (scaleFactor < 1) plusZoom = -1 * (1 - scaleFactor);
|
||||
else plusZoom = scaleFactor - 1;
|
||||
|
||||
float newZoom = curZoom + plusZoom;
|
||||
if (newZoom < 0 || newZoom > 1) return;
|
||||
|
||||
curZoom = newZoom;
|
||||
fotoapparat.setZoom(curZoom);
|
||||
|
||||
int progress = Math.round (cameraZoom.getMaxZoom() * curZoom);
|
||||
int value = cameraZoom.getZoomRatios().get(progress);
|
||||
|
||||
float roundedValue = (float)(Math.round(((float)value) / 10f)) / 10f;
|
||||
|
||||
zoomLvl.setVisibility(View.VISIBLE);
|
||||
zoomLvl.setText(String.format(Locale.getDefault(), "%.1f×", roundedValue));
|
||||
}
|
||||
|
||||
private void pointerChanged(int fingerCount){
|
||||
if(fingerCount == 0) {
|
||||
zoomLvl.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void switchCameraOnClick() {
|
||||
@@ -180,6 +241,7 @@ public class ActivityJava extends AppCompatActivity {
|
||||
activeCameraBack ? back() : front(),
|
||||
cameraConfiguration
|
||||
);
|
||||
adjustViewsVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -223,6 +285,7 @@ public class ActivityJava extends AppCompatActivity {
|
||||
super.onStart();
|
||||
if (hasCameraPermission) {
|
||||
fotoapparat.start();
|
||||
adjustViewsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +305,7 @@ public class ActivityJava extends AppCompatActivity {
|
||||
if (permissionsDelegate.resultGranted(requestCode, permissions, grantResults)) {
|
||||
hasCameraPermission = true;
|
||||
fotoapparat.start();
|
||||
adjustViewsVisibility();
|
||||
cameraView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var fotoapparat: Fotoapparat
|
||||
private lateinit var cameraZoom: Zoom.VariableZoom
|
||||
|
||||
private var curZoom: Float = 0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
@@ -148,8 +150,16 @@ class MainActivity : AppCompatActivity() {
|
||||
capabilities
|
||||
?.let {
|
||||
(it.zoom as? Zoom.VariableZoom)
|
||||
?.let { zoom -> setupZoom(zoom) }
|
||||
?: run { zoomSeekBar.visibility = View.GONE }
|
||||
?.let {
|
||||
cameraZoom = it
|
||||
focusView.scaleListener = this::scaleZoom
|
||||
focusView.ptrListener = this::pointerChanged
|
||||
}
|
||||
?: run {
|
||||
zoomLvl?.visibility = View.GONE
|
||||
focusView.scaleListener = null
|
||||
focusView.ptrListener = null
|
||||
}
|
||||
|
||||
torchSwitch.visibility = if (it.flashModes.contains(Flash.Torch)) View.VISIBLE else View.GONE
|
||||
}
|
||||
@@ -159,19 +169,27 @@ class MainActivity : AppCompatActivity() {
|
||||
switchCamera.visibility = if (fotoapparat.isAvailable(front())) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setupZoom(zoom: Zoom.VariableZoom) {
|
||||
zoomSeekBar.max = zoom.maxZoom
|
||||
cameraZoom = zoom
|
||||
zoomSeekBar.visibility = View.VISIBLE
|
||||
zoomSeekBar onProgressChanged { updateZoom(zoomSeekBar.progress) }
|
||||
updateZoom(0)
|
||||
}
|
||||
//When zooming slowly, the values are approximately 0.9 ~ 1.1
|
||||
private fun scaleZoom(scaleFactor: Float) {
|
||||
//convert to -0.1 ~ 0.1
|
||||
val plusZoom = if (scaleFactor < 1) -1 * (1 - scaleFactor) else scaleFactor - 1
|
||||
val newZoom = curZoom + plusZoom
|
||||
if (newZoom < 0 || newZoom > 1) return
|
||||
|
||||
private fun updateZoom(progress: Int) {
|
||||
fotoapparat.setZoom(progress.toFloat() / zoomSeekBar.max)
|
||||
curZoom = newZoom
|
||||
fotoapparat.setZoom(curZoom)
|
||||
val progress = (cameraZoom.maxZoom * curZoom).roundToInt()
|
||||
val value = cameraZoom.zoomRatios[progress]
|
||||
val roundedValue = ((value.toFloat()) / 10).roundToInt().toFloat() / 10
|
||||
zoomLvl.text = String.format("%.1f ×", roundedValue)
|
||||
|
||||
zoomLvl.visibility = View.VISIBLE
|
||||
zoomLvl.text = String.format("%.1f×", roundedValue)
|
||||
}
|
||||
|
||||
private fun pointerChanged(fingerCount: Int){
|
||||
if(fingerCount == 0) {
|
||||
zoomLvl?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,12 +53,6 @@
|
||||
android:padding="20dp"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/zoomSeekBar"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/switchCamera"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -74,8 +68,7 @@
|
||||
android:id="@+id/zoomLvl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|top"
|
||||
android:layout_marginTop="50dp"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="20sp"
|
||||
tools:text="2.4" />
|
||||
|
||||
Reference in New Issue
Block a user