diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c33d16..5472c2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,8 +20,7 @@ android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen"> + android:configChanges="orientation|screenSize|keyboard|keyboardHidden"> @@ -35,12 +34,12 @@ + android:configChanges="orientation|screenSize"> + android:configChanges="orientation|screenSize"> diff --git a/app/src/main/java/com/dozingcatsoftware/util/images.kt b/app/src/main/java/com/dozingcatsoftware/util/images.kt index ad43bb2..cbfa02c 100644 --- a/app/src/main/java/com/dozingcatsoftware/util/images.kt +++ b/app/src/main/java/com/dozingcatsoftware/util/images.kt @@ -19,6 +19,11 @@ fun getDisplaySize(context: Context): Size { return Size(metrics.widthPixels, metrics.heightPixels) } +fun getLandscapeDisplaySize(context: Context): Size { + val ds = getDisplaySize(context) + return if (ds.width >= ds.height) ds else Size(ds.height, ds.width) +} + /** * Notifies the OS to index an image so it appears in apps that show media files. Allows optional * callback to notify client when the scan is completed, e.g. so it can access the "content" URI diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImage.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImage.kt index 5324976..8661e7b 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImage.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImage.kt @@ -19,6 +19,8 @@ data class CameraImage(val rs: RenderScript, val orientation: ImageOrientation, val status: CameraStatus, val timestamp: Long, val displaySize: Size = zeroSize) { + // width() and height() return the dimensions of the actual camera input, which is always + // landscape. In portrait orientation the rendered image will have the dimensioned swapped. fun width(): Int { return singleYuvAllocation?.type?.x ?: planarYuvAllocations!!.y.type.x } @@ -34,6 +36,11 @@ data class CameraImage(val rs: RenderScript, rs, singleYuvAllocation, planarYuvAllocations, orientation, status, timestamp, ds) } + fun withDisplaySizeAndOrientation(ds: Size, o: ImageOrientation): CameraImage { + return CameraImage( + rs, singleYuvAllocation, planarYuvAllocations, o, status, timestamp, ds) + } + fun resizedTo(size: Size): CameraImage { val resizeScript = ScriptIntrinsicResize.create(rs) diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImageGenerator.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImageGenerator.kt index b86fc5a..1d73570 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImageGenerator.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CameraImageGenerator.kt @@ -22,6 +22,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, var status = CameraStatus.CLOSED private var targetStatus = CameraStatus.CLOSED private var imageAllocationCallback: ((CameraImage) -> Unit)? = null + private var cameraClosedCallback: (() -> Unit)? = null private var handler = Handler() private val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) @@ -34,12 +35,11 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, val facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) val isFrontFacing = (facing == CameraMetadata.LENS_FACING_FRONT) val orientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) - if (isFrontFacing && orientation == 90 || !isFrontFacing && orientation == 270) { - ImageOrientation.ROTATED_180 - } - else { - ImageOrientation.NORMAL - } + val upsideDown = + (isFrontFacing && orientation == 90) || (!isFrontFacing && orientation == 270) + // X flipped if the camera front facing or the orientation is backwards, but not both. + Log.i(TAG, "isFrontFacing: ${isFrontFacing} upsideDown: ${upsideDown}") + ImageOrientation(xFlipped = (isFrontFacing != upsideDown), yFlipped = upsideDown) }() fun start(targetStatus: CameraStatus, targetSize: Size, @@ -61,19 +61,24 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, } } - fun stop() { + /** + * Stops the camera and invokes the callback after the camera is completely closed. This avoids + * errors that can occur when trying to open the camera while it's in the process of closing. + */ + fun stop(closedCallback: (() -> Unit)? = null) { this.targetStatus = CameraStatus.CLOSED + this.cameraClosedCallback = closedCallback updateStatus(this.status) } private fun updateStatus(status: CameraStatus) { - Log.i(TAG, "CameraStatus change: " + status) + Log.i(TAG, "CameraStatus change: ${status}, target=${targetStatus}") this.status = status if (this.targetStatus == CameraStatus.CLOSED) { if (this.status.isCapturing()) { this.status = CameraStatus.CLOSING - stopCaptureSession() + stopCamera() } } else if (this.targetStatus.isCapturing()) { @@ -100,7 +105,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, } } - fun openCamera() { + private fun openCamera() { val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager stopCamera() try { @@ -111,8 +116,15 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, updateStatus(CameraStatus.OPENED) } - override fun onDisconnected(cam: CameraDevice) { + override fun onClosed(cam: CameraDevice) { + Log.i(TAG, "camera onClosed") updateStatus(CameraStatus.CLOSED) + cameraClosedCallback?.invoke() + } + + override fun onDisconnected(cam: CameraDevice) { + Log.i(TAG, "onDisconnected") + cam.close() } override fun onError(camera: CameraDevice, error: Int) { @@ -157,6 +169,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript, } override fun onClosed(session: CameraCaptureSession?) { + Log.i(TAG, "capture session onClosed, status=${status}") super.onClosed(session) updateStatus(CameraStatus.OPENED) } diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CreateVideoZipFileAsyncTask.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CreateVideoZipFileAsyncTask.kt index 61de37b..4c7bc9a 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/CreateVideoZipFileAsyncTask.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/CreateVideoZipFileAsyncTask.kt @@ -31,8 +31,8 @@ class CreateVideoZipFileAsyncTask( val videoReader = params[0]!!.videoReader val mediaLibrary = params[0]!!.mediaLibrary val videoId = params[0]!!.videoId - val videoWidth = videoReader.videoWidth() - val videoHeight = videoReader.videoHeight() + val videoWidth = videoReader.landscapeVideoWidth() + val videoHeight = videoReader.landscapeVideoHeight() // Frames are JPEGs because PNG encoding is very slow. zipFile = mediaLibrary.videoFramesArchiveForItemId(videoId) diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ImageOrientation.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ImageOrientation.kt index 102d4ec..b481007 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ImageOrientation.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ImageOrientation.kt @@ -1,24 +1,11 @@ package com.dozingcatsoftware.vectorcamera -enum class ImageOrientation { - NORMAL, - ROTATED_180, - ; +class ImageOrientation( + val xFlipped: Boolean, val yFlipped: Boolean, val portrait: Boolean = false) { - fun isXFlipped(): Boolean { - return this == ROTATED_180 - } - - fun isYFlipped(): Boolean { - return this == ROTATED_180 - } + fun withPortrait(newPortrait: Boolean) = ImageOrientation(xFlipped, yFlipped, newPortrait) companion object { - fun withXYFlipped(xFlipped: Boolean, yFlipped: Boolean): ImageOrientation { - if (xFlipped != yFlipped) { - throw IllegalArgumentException("Only 0 and 180 degree rotations supported") - } - return if (xFlipped) ImageOrientation.ROTATED_180 else ImageOrientation.NORMAL - } + val NORMAL = ImageOrientation(false, false) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/MainActivity.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/MainActivity.kt index 71e2760..0ceb0f7 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/MainActivity.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/MainActivity.kt @@ -2,6 +2,7 @@ package com.dozingcatsoftware.vectorcamera import android.app.Activity import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.preference.PreferenceManager @@ -9,15 +10,17 @@ import android.provider.MediaStore import android.renderscript.RenderScript import android.util.Log import android.util.Size -import android.view.MotionEvent -import android.view.View -import android.view.Window import com.dozingcatsoftware.vectorcamera.effect.CombinationEffect import com.dozingcatsoftware.vectorcamera.effect.Effect import com.dozingcatsoftware.vectorcamera.effect.EffectRegistry -import com.dozingcatsoftware.util.getDisplaySize +import com.dozingcatsoftware.util.getLandscapeDisplaySize import kotlinx.android.synthetic.main.activity_main.* import java.io.FileOutputStream +import android.view.* +import android.widget.FrameLayout +import android.widget.LinearLayout + + enum class ShutterMode {IMAGE, VIDEO} @@ -34,7 +37,6 @@ class MainActivity : Activity() { private val photoLibrary = PhotoLibrary.defaultLibrary() private lateinit var rs: RenderScript - private lateinit var displaySize: Size private val allEffectFactories = EffectRegistry.defaultEffectFactories() private var currentEffect: Effect? = null private var previousEffect: Effect? = null @@ -48,13 +50,14 @@ class MainActivity : Activity() { private lateinit var previousImageSize: ImageSize private var shutterMode = ShutterMode.IMAGE + private var layoutIsPortrait = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE) setContentView(R.layout.activity_main) PreferenceManager.setDefaultValues(this.baseContext, R.xml.preferences, false) - displaySize = getDisplaySize(this) // Use PROFILE type only on first run? rs = RenderScript.create(this, RenderScript.ContextType.NORMAL) imageProcessor = CameraImageProcessor(rs) @@ -92,9 +95,16 @@ class MainActivity : Activity() { override fun onResume() { super.onResume() - // TODO: Read high quality flag from preferences. checkPermissionAndStartCamera() overlayView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE + + // View size is zero in onResume, have to wait for layout notification. + var listener: ViewTreeObserver.OnGlobalLayoutListener? = null + listener = ViewTreeObserver.OnGlobalLayoutListener { + updateLayout(isPortraitOrientation()) + overlayView.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + overlayView.viewTreeObserver.addOnGlobalLayoutListener(listener) } override fun onPause() { @@ -111,6 +121,15 @@ class MainActivity : Activity() { super.onPause() } + override fun onConfigurationChanged(newConfig: Configuration) { + Log.i(TAG, "configurationChanged: ${newConfig.orientation}") + super.onConfigurationChanged(newConfig) + val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + if (isPortrait != layoutIsPortrait) { + updateLayout(isPortrait) + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) @@ -156,12 +175,47 @@ class MainActivity : Activity() { if (preferredImageSize == ImageSize.FULL_SCREEN) 1.0f else 0.5f } + fun updateLayout(isPortrait: Boolean) { + Log.i(TAG, "updateLayout: ${isPortrait}") + layoutIsPortrait = isPortrait + val layoutWidth = + if (isPortrait) FrameLayout.LayoutParams.MATCH_PARENT + else FrameLayout.LayoutParams.WRAP_CONTENT + val layoutHeight = + if (isPortrait) FrameLayout.LayoutParams.WRAP_CONTENT + else FrameLayout.LayoutParams.MATCH_PARENT + val orientation = if (isPortrait) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL + val direction = + if (isPortrait) LinearLayout.LAYOUT_DIRECTION_RTL + else LinearLayout.LAYOUT_DIRECTION_LTR + + run { + val params = FrameLayout.LayoutParams(layoutWidth, layoutHeight) + params.gravity = if (isPortrait) Gravity.TOP else Gravity.LEFT + leftTopControlBar.layoutParams = params + leftTopControlBar.orientation = orientation + leftTopControlBar.layoutDirection = direction + } + run { + val params = FrameLayout.LayoutParams(layoutWidth, layoutHeight) + params.gravity = if (isPortrait) Gravity.BOTTOM else Gravity.RIGHT + rightBottomControlBar.layoutParams = params + rightBottomControlBar.orientation = orientation + rightBottomControlBar.layoutDirection = direction + } + } + + private fun isPortraitOrientation(): Boolean { + return overlayView.height > overlayView.width + } + private fun targetCameraImageSize(): Size { + val ds = getLandscapeDisplaySize(this) return when (preferredImageSize) { - ImageSize.FULL_SCREEN -> displaySize - ImageSize.HALF_SCREEN -> Size(displaySize.width / 2, displaySize.height / 2) + ImageSize.FULL_SCREEN -> ds + ImageSize.HALF_SCREEN -> Size(ds.width / 2, ds.height / 2) ImageSize.VIDEO_RECORDING -> Size(640, 360) - ImageSize.EFFECT_GRID -> Size(displaySize.width / 4, displaySize.height / 4) + ImageSize.EFFECT_GRID -> Size(ds.width / 4, ds.height / 4) } } @@ -247,8 +301,11 @@ class MainActivity : Activity() { private fun handleAllocationFromCamera(imageFromCamera: CameraImage) { handler.post(fun() { - // Slightly ugly but some effects need the display size. - val cameraImage = imageFromCamera.withDisplaySize(displaySize) + // Add fields that the image generator doesn't have. Might be better to have a separate + // class that holds a CameraImage, display size, and portrait flag. + val ds = getLandscapeDisplaySize(this) + val orientation = imageFromCamera.orientation.withPortrait(isPortraitOrientation()) + val cameraImage = imageFromCamera.withDisplaySizeAndOrientation(ds, orientation) if (cameraImage.status == CameraStatus.CAPTURING_PHOTO) { Log.i(TAG, "Restarting preview capture") restartCameraImageGenerator() @@ -281,10 +338,13 @@ class MainActivity : Activity() { } imageProcessor.pause() cameraSelector.selectNextCamera() - cameraImageGenerator.stop() - cameraImageGenerator = cameraSelector.createImageGenerator(rs) - restartCameraImageGenerator() - updateControls() + cameraImageGenerator.stop({ + handler.post({ + cameraImageGenerator = cameraSelector.createImageGenerator(rs) + restartCameraImageGenerator() + updateControls() + }) + }) } private fun switchResolution(view: View) { diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/MediaMetadata.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/MediaMetadata.kt index ec09aa0..4bb3d52 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/MediaMetadata.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/MediaMetadata.kt @@ -18,8 +18,9 @@ data class MediaMetadata(val mediaType: MediaType, val effectMetadata: EffectMet "type" to mediaType.name.toLowerCase(), "width" to width, "height" to height, - "xFlipped" to orientation.isXFlipped(), - "yFlipped" to orientation.isYFlipped(), + "xFlipped" to orientation.xFlipped, + "yFlipped" to orientation.yFlipped, + "portrait" to orientation.portrait, "timestamp" to timestamp, "frameTimestamps" to frameTimestamps, "audioStartTimestamp" to audioStartTimestamp, @@ -54,8 +55,9 @@ data class MediaMetadata(val mediaType: MediaType, val effectMetadata: EffectMet EffectMetadata.fromJson(effectDict), (json["width"] as Number).toInt(), (json["height"] as Number).toInt(), - ImageOrientation.withXYFlipped( - json["xFlipped"] as Boolean, json["yFlipped"] as Boolean), + ImageOrientation( + json["xFlipped"] as Boolean, json["yFlipped"] as Boolean, + json.getOrElse("portrait", {false}) as Boolean), json["timestamp"] as Long, frameTimestamps as List, audioStartTimestamp.toLong(), diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ProcessedBitmap.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ProcessedBitmap.kt index e0bc433..27bce2b 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ProcessedBitmap.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ProcessedBitmap.kt @@ -1,7 +1,6 @@ package com.dozingcatsoftware.vectorcamera import android.graphics.* -import android.renderscript.RenderScript import android.util.Size import com.dozingcatsoftware.vectorcamera.effect.Effect @@ -23,19 +22,35 @@ data class ProcessedBitmap( tmpRect: RectF? = null, tmpMatrix: Matrix? = null) { val dstRect = tmpRect ?: RectF() val flipMatrix = tmpMatrix ?: Matrix() + flipMatrix.reset() - val scaleFactor = Math.min(width.toFloat() / bitmap.width, height.toFloat() / bitmap.height) - val scaledWidth = bitmap.width * scaleFactor - val scaledHeight = bitmap.height * scaleFactor + val o = sourceImage.orientation + val shouldRotate = o.portrait + val flipHorizontal = if (shouldRotate) o.yFlipped else o.xFlipped + val flipVertical = if (shouldRotate) o.xFlipped else o.yFlipped + + val bitmapWidth = if (shouldRotate) bitmap.height else bitmap.width + val bitmapHeight = if (shouldRotate) bitmap.width else bitmap.height + + val scaleFactor = Math.min(width.toFloat() / bitmapWidth, height.toFloat() / bitmapHeight) + val scaledWidth = bitmapWidth * scaleFactor + val scaledHeight = bitmapHeight * scaleFactor - val flipHorizontal = sourceImage.orientation.isXFlipped() - val flipVertical = sourceImage.orientation.isYFlipped() var xOffset = (width - scaledWidth) / 2 var yOffset = (height - scaledHeight) / 2 - flipMatrix.setScale(if (flipHorizontal) -scaleFactor else scaleFactor, + if (shouldRotate) { + // Rotating clockwise changes the rect from (0, 0, w, h) to (-h, 0, 0, w). + // Add h to the x coordinate to get to (0, 0, h, w). + flipMatrix.postRotate(90f) + flipMatrix.postTranslate(bitmap.height.toFloat(), 0f) + } + + flipMatrix.postScale( + if (flipHorizontal) -scaleFactor else scaleFactor, if (flipVertical) -scaleFactor else scaleFactor) - flipMatrix.postTranslate(if (flipHorizontal) xOffset + scaledWidth else xOffset, + flipMatrix.postTranslate( + if (flipHorizontal) xOffset + scaledWidth else xOffset, if (flipVertical) yOffset + scaledHeight else yOffset) if (xOffset > 0 && outsidePaint != null) { @@ -52,10 +67,14 @@ data class ProcessedBitmap( canvas.drawBitmap(bitmap, flipMatrix, null) } - fun renderBitmap(width: Int, height: Int): Bitmap { - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + fun renderBitmap(landscapeWidth: Int, landscapeHeight: Int): Bitmap { + val shouldRotate = sourceImage.orientation.portrait + val bitmap = Bitmap.createBitmap( + if (shouldRotate) landscapeHeight else landscapeWidth, + if (shouldRotate) landscapeWidth else landscapeHeight, + Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) - renderToCanvas(canvas, width, height) + renderToCanvas(canvas, bitmap.width, bitmap.height) return bitmap } diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/VideoReader.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/VideoReader.kt index 32ee85c..7a08c2e 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/VideoReader.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/VideoReader.kt @@ -13,15 +13,21 @@ class VideoReader(val rs: RenderScript, val photoLibrary: PhotoLibrary, val vide private val metadata = photoLibrary.metadataForItemId(videoId) private val frameBuffer: ByteArray // effect and displaySize can be changed after creation. + // forcePortrait is for when we're showing the effect selection grid and always want to fill + // the screen, so we enable portrait when the device is vertical regardless of the metadata. var effect: Effect + var forcePortrait: Boolean? = null init { effect = EffectRegistry.forMetadata(rs, metadata.effectMetadata) frameBuffer = ByteArray(bytesPerFrame()) } - fun videoWidth() = metadata.width - fun videoHeight() = metadata.height + fun isPortrait() = metadata.orientation.portrait + fun landscapeVideoWidth() = metadata.width + fun landscapeVideoHeight() = metadata.height + fun outputVideoWidth() = if (isPortrait()) metadata.height else metadata.width + fun outputVideoHeight() = if (isPortrait()) metadata.width else metadata.height fun numberOfFrames() = metadata.frameTimestamps.size private fun bytesPerFrame() = metadata.width * metadata.height * 3 / 2 @@ -35,9 +41,14 @@ class VideoReader(val rs: RenderScript, val photoLibrary: PhotoLibrary, val vide videoFile.readFully(frameBuffer) val allocation = PlanarYuvAllocations.fromInputStream( rs, ByteArrayInputStream(frameBuffer), metadata.width, metadata.height) - val cameraImage = CameraImage.withAllocationSet( + var cameraImage = CameraImage.withAllocationSet( rs, allocation, metadata.orientation, CameraStatus.CAPTURING_VIDEO, metadata.frameTimestamps[frameIndex], displaySize) + val fp = forcePortrait + if (fp != null) { + cameraImage = cameraImage.withDisplaySizeAndOrientation( + displaySize, cameraImage.orientation.withPortrait(fp)) + } return ProcessedBitmap(effect, cameraImage, effect.createBitmap(cameraImage)) } diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewImageActivity.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewImageActivity.kt index e788279..8f686bd 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewImageActivity.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewImageActivity.kt @@ -5,6 +5,7 @@ import android.app.AlertDialog import android.app.ProgressDialog import android.content.DialogInterface import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.renderscript.RenderScript @@ -17,7 +18,7 @@ import com.dozingcatsoftware.vectorcamera.effect.AsciiEffect import com.dozingcatsoftware.vectorcamera.effect.CombinationEffect import com.dozingcatsoftware.vectorcamera.effect.Effect import com.dozingcatsoftware.vectorcamera.effect.EffectRegistry -import com.dozingcatsoftware.util.getDisplaySize +import com.dozingcatsoftware.util.getLandscapeDisplaySize import kotlinx.android.synthetic.main.view_image.* import java.io.File @@ -27,6 +28,7 @@ class ViewImageActivity : Activity() { private lateinit var rs : RenderScript private lateinit var imageId: String private var inEffectSelectionMode = false + private var effectSelectionIsPortrait = false private val allEffectFactories = EffectRegistry.defaultEffectFactories() private val preferences = VCPreferences(this) private val handler = Handler() @@ -54,19 +56,39 @@ class ViewImageActivity : Activity() { } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (inEffectSelectionMode) { + val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + if (isPortrait != effectSelectionIsPortrait) { + Log.i(TAG, "Switching portrait: " + isPortraitOrientation()) + showModeSelectionGrid(isPortrait) + } + } + } + + private fun isPortraitOrientation(): Boolean { + return overlayView.height > overlayView.width + } + private fun loadImage() { val metadata = photoLibrary.metadataForItemId(imageId) val effect = EffectRegistry.forMetadata(rs, metadata.effectMetadata) showImage(effect, metadata) } + private fun showModeSelectionGrid(isPortrait: Boolean) { + val comboEffect = CombinationEffect(rs, preferences.lookupFunction, allEffectFactories) + // FIXME: This is slow because the saved image is high resolution. + showImage(comboEffect, photoLibrary.metadataForItemId(imageId), isPortrait) + controlBar.visibility = View.GONE + effectSelectionIsPortrait = isPortrait + } + private fun toggleEffectSelectionMode(view: View?) { inEffectSelectionMode = !inEffectSelectionMode if (inEffectSelectionMode) { - val comboEffect = CombinationEffect(rs, preferences.lookupFunction, allEffectFactories) - // FIXME: This is slow because the saved image is high resolution. - showImage(comboEffect, photoLibrary.metadataForItemId(imageId)) - controlBar.visibility = View.GONE + showModeSelectionGrid(isPortraitOrientation()) } else { loadImage() @@ -79,17 +101,22 @@ class ViewImageActivity : Activity() { PlanarYuvAllocations.fromInputStream(rs, it, metadata.width, metadata.height) } return CameraImage(rs, null, planarYuv, metadata.orientation, - CameraStatus.CAPTURING_PHOTO, metadata.timestamp, getDisplaySize(this)) + CameraStatus.CAPTURING_PHOTO, metadata.timestamp, getLandscapeDisplaySize(this)) } - private fun createProcessedBitmap(effect: Effect, metadata: MediaMetadata): ProcessedBitmap { - val inputImage = createCameraImage(metadata) + private fun createProcessedBitmap(effect: Effect, metadata: MediaMetadata, + forcePortrait: Boolean? = null): ProcessedBitmap { + var inputImage = createCameraImage(metadata) + if (forcePortrait != null) { + inputImage = inputImage.withDisplaySizeAndOrientation( + inputImage.displaySize, inputImage.orientation.withPortrait(forcePortrait)) + } val bitmap = effect.createBitmap(inputImage) return ProcessedBitmap(effect, inputImage, bitmap) } - private fun showImage(effect: Effect, metadata: MediaMetadata) { - overlayView.processedBitmap = createProcessedBitmap(effect, metadata) + private fun showImage(effect: Effect, metadata: MediaMetadata, forcePortrait: Boolean? = null) { + overlayView.processedBitmap = createProcessedBitmap(effect, metadata, forcePortrait) overlayView.invalidate() } diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewVideoActivity.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewVideoActivity.kt index 33a415a..1d88083 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewVideoActivity.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/ViewVideoActivity.kt @@ -5,6 +5,7 @@ import android.app.AlertDialog import android.app.ProgressDialog import android.content.DialogInterface import android.content.Intent +import android.content.res.Configuration import android.graphics.Bitmap import android.os.Bundle import android.os.Handler @@ -17,7 +18,7 @@ import android.widget.Toast import com.dozingcatsoftware.vectorcamera.effect.CombinationEffect import com.dozingcatsoftware.vectorcamera.effect.Effect import com.dozingcatsoftware.vectorcamera.effect.EffectRegistry -import com.dozingcatsoftware.util.getDisplaySize +import com.dozingcatsoftware.util.getLandscapeDisplaySize import com.dozingcatsoftware.util.scanSavedMediaFile import kotlinx.android.synthetic.main.view_video.* import java.io.File @@ -54,6 +55,7 @@ class ViewVideoActivity: Activity() { private lateinit var rs : RenderScript private lateinit var videoId: String private var inEffectSelectionMode = false + private var effectSelectionIsPortrait = false private var originalEffect: Effect? = null private val allEffectFactories = EffectRegistry.defaultEffectFactories() private lateinit var videoReader: VideoReader @@ -81,7 +83,7 @@ class ViewVideoActivity: Activity() { // Yes, this does I/O. videoId = intent.getStringExtra("videoId") - videoReader = VideoReader(rs, photoLibrary, videoId, getDisplaySize(this)) + videoReader = VideoReader(rs, photoLibrary, videoId, getLandscapeDisplaySize(this)) frameSeekBar.max = videoReader.numberOfFrames() - 1 frameSeekBar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { @@ -116,6 +118,16 @@ class ViewVideoActivity: Activity() { } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (inEffectSelectionMode) { + videoReader.forcePortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + if (!isPlaying) { + loadFrame(frameIndex) + } + } + } + private fun updateControls() { frameSeekBar.progress = frameIndex playPauseButton.setImageResource( @@ -123,6 +135,10 @@ class ViewVideoActivity: Activity() { else R.drawable.ic_play_arrow_white_36dp) } + private fun isPortraitOrientation(): Boolean { + return overlayView.height > overlayView.width + } + private fun loadFrame(index: Int) { frameIndex = index val bitmap = videoReader.bitmapForFrame(index) @@ -136,10 +152,12 @@ class ViewVideoActivity: Activity() { originalEffect = videoReader.effect videoReader.effect = CombinationEffect(rs, preferences.lookupFunction, allEffectFactories) + videoReader.forcePortrait = isPortraitOrientation() controlBar.visibility = View.GONE } else { videoReader.effect = originalEffect!! + videoReader.forcePortrait = null controlBar.visibility = View.VISIBLE } if (!isPlaying) { @@ -222,6 +240,7 @@ class ViewVideoActivity: Activity() { val effect = allEffectFactories[effectIndex](rs, preferences.lookupFunction) originalEffect = effect videoReader.effect = effect + videoReader.forcePortrait = null // Update thumbnail and metadata with effect. val newMetadata = photoLibrary.metadataForItemId(videoId) diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/WebMEncoder.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/WebMEncoder.kt index 04a1120..73fb62d 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/WebMEncoder.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/WebMEncoder.kt @@ -8,10 +8,10 @@ import java.io.File */ class WebMEncoder(val videoReader: VideoReader, val outputPath: String) { - private val frameArgb = IntArray(videoReader.videoWidth() * videoReader.videoHeight()) + private val frameArgb = IntArray(videoReader.outputVideoWidth() * videoReader.outputVideoHeight()) private val numFrames = videoReader.numberOfFrames() - private val videoWidth = videoReader.videoWidth() - private val videoHeight = videoReader.videoHeight() + private val outputWidth = videoReader.outputVideoWidth() + private val outputHeight = videoReader.outputVideoHeight() fun startEncoding() { val framesPerSecond = (numFrames / videoReader.totalDurationMillis()).toFloat() / 1000f @@ -23,7 +23,7 @@ class WebMEncoder(val videoReader: VideoReader, val outputPath: String) { } val deadlineMicros = 1_000_000 this.nativeStartEncoding( - outputPath, videoWidth, videoHeight, + outputPath, outputWidth, outputHeight, framesPerSecond, frameRelativeEndTimes, deadlineMicros) } @@ -31,8 +31,9 @@ class WebMEncoder(val videoReader: VideoReader, val outputPath: String) { if (frameIndex < 0 || frameIndex >= numFrames) { throw IllegalArgumentException("Invalid frame index: ${frameIndex}") } - val bitmap = videoReader.bitmapForFrame(frameIndex).renderBitmap(videoWidth, videoHeight) - bitmap.getPixels(frameArgb, 0, videoWidth, 0, 0, videoWidth, videoHeight) + val bitmap = videoReader.bitmapForFrame(frameIndex).renderBitmap( + videoReader.landscapeVideoWidth(), videoReader.landscapeVideoHeight()) + bitmap.getPixels(frameArgb, 0, outputWidth, 0, 0, outputWidth, outputHeight) nativeEncodeFrame(frameArgb) } diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/AsciiEffect.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/AsciiEffect.kt index b4e27f0..5d98977 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/AsciiEffect.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/AsciiEffect.kt @@ -118,8 +118,8 @@ class AsciiEffect(private val rs: RenderScript, script._numCharColumns = numCharacterColumns script._numCharRows = numCharacterRows script._numCharacters = pixelChars.length - script._flipHorizontal = cameraImage.orientation.isXFlipped() - script._flipVertical = cameraImage.orientation.isYFlipped() + script._flipHorizontal = cameraImage.orientation.xFlipped + script._flipVertical = cameraImage.orientation.yFlipped script._colorMode = colorMode.id // There's no input allocation passed directly to the kernel so we manually set x/y ranges. val options = Script.LaunchOptions().setX(0, numCharacterColumns).setY(0, numCharacterRows) @@ -154,8 +154,8 @@ class AsciiEffect(private val rs: RenderScript, script._numCharColumns = numCharacterColumns script._numCharRows = numCharacterRows script._numCharacters = pixelChars.length - script._flipHorizontal = camAllocation.orientation.isXFlipped() - script._flipVertical = camAllocation.orientation.isYFlipped() + script._flipHorizontal = camAllocation.orientation.xFlipped + script._flipVertical = camAllocation.orientation.yFlipped script._colorMode = colorMode.id if (camAllocation.planarYuvAllocations != null) { script._hasSingleYuvAllocation = false diff --git a/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/CombinationEffect.kt b/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/CombinationEffect.kt index 6dfbded..7fa7c58 100644 --- a/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/CombinationEffect.kt +++ b/app/src/main/java/com/dozingcatsoftware/vectorcamera/effect/CombinationEffect.kt @@ -38,6 +38,7 @@ class CombinationEffect( val resultBitmap = Bitmap.createBitmap(outputWidth, outputHeight, Bitmap.Config.ARGB_8888) val resultCanvas = Canvas(resultBitmap) + val shouldRotate = cameraImage.orientation.portrait val srcRect = RectF(0f, 0f, tileBuffer.width.toFloat(), tileBuffer.height.toFloat()) for (i in 0 until effectFactories.size) { val effect = effectFactories[i](rs, prefsFn) @@ -46,12 +47,12 @@ class CombinationEffect( effect.drawBackground(cameraImage, tileCanvas, srcRect) tileCanvas.drawBitmap(tileBitmap, tileBitmapRect, srcRect, null) - var gridX = i % gridSize - var gridY = i / gridSize - if (cameraImage.orientation.isXFlipped()) { + var gridX = if (shouldRotate) (i / gridSize) else (i % gridSize) + var gridY = if (shouldRotate) (gridSize - 1 - i % gridSize) else (i / gridSize) + if (cameraImage.orientation.xFlipped) { gridX = gridSize - 1 - gridX } - if (cameraImage.orientation.isYFlipped()) { + if (cameraImage.orientation.yFlipped) { gridY = gridSize - 1 - gridY } val dstRect = Rect(gridX * tileWidth, gridY * tileHeight, diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c52b53a..bdab84b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,6 +16,7 @@ android:layout_height="match_parent">