Compare commits

...

23 Commits

Author SHA1 Message Date
Ali Asadi 9454f3e4d1 Add TopCrop option to ScaleType (#372) 2021-12-10 10:53:46 +01:00
Ilia Voitcekhovskii 7094f9850a Fix unused inSampleSize and add scaling to decoding step (#359)
* Fix bitmap decoding to use inSampleSize

* Add scaling (by max side) while decoding bitmap
2021-12-10 10:51:01 +01:00
John 7039cf2059 Pinch to zoom (#388)
* Update gradle and libs

* Implemented pinch to zoom in example app
2021-12-10 10:50:19 +01:00
zakimiiiii 09a551635c Modified file to correct name (#378) 2019-07-02 14:15:09 +02:00
Dionysis 72c7c81a42 Release v2.7.0 2019-03-02 16:44:27 +01:00
Dionysis a7287e363c Remove wrong imports 2019-03-02 16:41:39 +01:00
Dionysis 6156f4c897 Merge remote-tracking branch 'origin/master' 2019-03-02 16:40:48 +01:00
Dionysis 29be38b88b Fixes #191 2019-03-02 16:40:38 +01:00
Dionysis 69d99e1aea Fixes #191 2019-03-02 16:40:12 +01:00
Nicklas Ansman Giertz af9e8158b8 Remove lazy delegates where they aren't needed (#349) 2019-03-02 14:21:53 +01:00
Dionysis 4494d76bb1 Merge branch 'master' of github.com:RedApparat/Fotoapparat 2019-03-02 14:20:11 +01:00
Dionysis f1830853c3 Merge branch 'ansman-fix/pending-result-notify' 2019-03-02 14:19:32 +01:00
Dionysis 75181ecb92 Merge branch 'fix/pending-result-notify' of https://github.com/ansman/Fotoapparat into ansman-fix/pending-result-notify 2019-03-02 14:19:13 +01:00
Nicklas Ansman Giertz 697f996d17 Make Photo.toString not include the entire byte array (#351) 2019-03-02 14:14:31 +01:00
Dionysis Lorentzos 1783058ade Fixes orientation issues (#354)
* fix: preview orientation in sensorLandscape

* Fix orientation issues.

Fixes #262
Fixes #260
Fixes #253
Fixes #193
2019-03-02 14:13:18 +01:00
Alaeri f78c324f23 fix: preview orientation in sensorLandscape (#352) 2019-03-02 14:10:50 +01:00
Nicklas Ansman Giertz b1f6f491cd Make all the configuration objects have nicer toString values (#348) 2019-03-02 12:32:19 +01:00
Nicklas Ansman Giertz 46fe442a4e Always notify the callback in PendingResult
Previously it wouldn't be notified in some cases and in some cases
it would be notified off the main thread.
2019-02-21 13:06:30 -05:00
Wolfgang Schreurs 27c9a7a910 Set source and target compatability to Java 1.8 for library and dependencies (#343) 2019-02-20 18:33:08 +01:00
Nicklas Ansman Giertz f05fc12039 Fix a crash when trying to access with size of a Photo (#347)
* Fix a crash when trying to access the size of a Photo
2019-02-20 18:32:01 +01:00
Santi Aguilera b37c3df8c9 Remove app_name from library (#334) 2019-01-24 21:02:29 +01:00
Dick Lucas 353e8a326b Update gradle version (#338)
Using this library in our project forces us to use Gradle version 3.2.1 in order to match Fotoapparat. This is because if they are not matching, it causes an issue generating the R file.
2019-01-24 21:01:51 +01:00
Dionysis Lorentzos 9021507995 Auto upload on tags (#317)
* Deploy on tags

* Bump version to 2.6.1
2018-11-08 16:25:48 +01:00
42 changed files with 465 additions and 226 deletions
+2
View File
@@ -6,4 +6,6 @@
build
/captures
.externalNativeBuild
.project
.settings
+13 -7
View File
@@ -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
+22 -25
View File
@@ -2,19 +2,18 @@
![Build status](https://travis-ci.org/RedApparat/Fotoapparat.svg?branch=master)
![ ](sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png)
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
View File
@@ -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
View File
@@ -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 {
+4 -2
View File
@@ -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}"
}
+4 -2
View File
@@ -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}"
}
+5 -2
View File
@@ -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"
}
}
}
@@ -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)
}
}
}
@@ -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>
@@ -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)
}
}
@@ -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
View File
@@ -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
-1
View File
@@ -25,6 +25,5 @@ android {
dependencies {
implementation project(':fotoapparat')
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation "androidx.appcompat:appcompat:${versions.android.appcompat}"
}
+7 -5
View File
@@ -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
}
}
}
+1 -8
View File
@@ -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" />