orientation
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(ux_keyboard)
|
project(ux_keyboard)
|
||||||
|
|
||||||
add_library(ux_keyboard SHARED keyboard_bridge.c)
|
add_library(ux_keyboard SHARED keyboard_bridge.c sensor_bridge.c)
|
||||||
target_link_libraries(ux_keyboard log)
|
target_link_libraries(ux_keyboard log)
|
||||||
|
|
||||||
# 16KB page size support (required for Android 15+)
|
# 16KB page size support (required for Android 15+)
|
||||||
|
|||||||
18
android/src/main/jni/sensor_bridge.c
Normal file
18
android/src/main/jni/sensor_bridge.c
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Shared state — Kotlin writes, Dart reads via FFI.
|
||||||
|
// Encoded as Surface rotation convention:
|
||||||
|
// 0 = portraitUp, 1 = landscapeLeft, 2 = portraitDown, 3 = landscapeRight.
|
||||||
|
static int32_t g_device_orientation = 0;
|
||||||
|
|
||||||
|
// --- Dart FFI reads ---
|
||||||
|
|
||||||
|
int32_t ux_device_orientation(void) { return g_device_orientation; }
|
||||||
|
|
||||||
|
// --- Kotlin JNI writes ---
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_io_swipelab_ux_SensorBridge_nSetDeviceOrientation(JNIEnv *env, jclass cls, jint v) {
|
||||||
|
g_device_orientation = v;
|
||||||
|
}
|
||||||
@@ -7,12 +7,11 @@ import android.view.ViewTreeObserver
|
|||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsAnimation
|
import android.view.WindowInsetsAnimation
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
class KeyboardPlugin : NativePlugin, MethodChannel.MethodCallHandler {
|
||||||
private var methodChannel: MethodChannel? = null
|
private var methodChannel: MethodChannel? = null
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var windowFocusListener: ViewTreeObserver.OnWindowFocusChangeListener? = null
|
private var windowFocusListener: ViewTreeObserver.OnWindowFocusChangeListener? = null
|
||||||
|
|||||||
13
android/src/main/kotlin/io/swipelab/ux/NativePlugin.kt
Normal file
13
android/src/main/kotlin/io/swipelab/ux/NativePlugin.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package io.swipelab.ux
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
|
interface NativePlugin {
|
||||||
|
fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
|
||||||
|
fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
|
||||||
|
fun onAttachedToActivity(binding: ActivityPluginBinding) {}
|
||||||
|
fun onDetachedFromActivity() {}
|
||||||
|
fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) = onAttachedToActivity(binding)
|
||||||
|
fun onDetachedFromActivityForConfigChanges() = onDetachedFromActivity()
|
||||||
|
}
|
||||||
9
android/src/main/kotlin/io/swipelab/ux/SensorBridge.kt
Normal file
9
android/src/main/kotlin/io/swipelab/ux/SensorBridge.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package io.swipelab.ux
|
||||||
|
|
||||||
|
object SensorBridge {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("ux_keyboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic external fun nSetDeviceOrientation(v: Int)
|
||||||
|
}
|
||||||
55
android/src/main/kotlin/io/swipelab/ux/SensorPlugin.kt
Normal file
55
android/src/main/kotlin/io/swipelab/ux/SensorPlugin.kt
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package io.swipelab.ux
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.OrientationEventListener
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
|
class SensorPlugin : NativePlugin {
|
||||||
|
private var listener: OrientationEventListener? = null
|
||||||
|
private var current: Int = 0
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
attach(binding.activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attach(activity: Activity) {
|
||||||
|
detach()
|
||||||
|
val l = object : OrientationEventListener(activity) {
|
||||||
|
override fun onOrientationChanged(degrees: Int) {
|
||||||
|
if (degrees == ORIENTATION_UNKNOWN) return
|
||||||
|
val bucket = bucketFor(degrees, current)
|
||||||
|
if (bucket == current) return
|
||||||
|
current = bucket
|
||||||
|
SensorBridge.nSetDeviceOrientation(bucket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l.canDetectOrientation()) l.enable()
|
||||||
|
listener = l
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detach() {
|
||||||
|
listener?.disable()
|
||||||
|
listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal fun bucketFor(degrees: Int, previous: Int): Int {
|
||||||
|
val d = ((degrees % 360) + 360) % 360
|
||||||
|
return when {
|
||||||
|
d in 0..30 || d in 330..359 -> 0
|
||||||
|
d in 60..120 -> 3
|
||||||
|
d in 150..210 -> 2
|
||||||
|
d in 240..300 -> 1
|
||||||
|
d in 31..59 -> if (previous == 0 || previous == 3) previous else 0
|
||||||
|
d in 121..149 -> if (previous == 3 || previous == 2) previous else 3
|
||||||
|
d in 211..239 -> if (previous == 2 || previous == 1) previous else 2
|
||||||
|
d in 301..329 -> if (previous == 1 || previous == 0) previous else 1
|
||||||
|
else -> previous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt
Normal file
30
android/src/main/kotlin/io/swipelab/ux/UxPlugin.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package io.swipelab.ux
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
|
class UxPlugin : FlutterPlugin, ActivityAware {
|
||||||
|
private val plugins: List<NativePlugin> = listOf(
|
||||||
|
KeyboardPlugin(),
|
||||||
|
SensorPlugin(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) =
|
||||||
|
plugins.forEach { it.onAttachedToEngine(binding) }
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) =
|
||||||
|
plugins.forEach { it.onDetachedFromEngine(binding) }
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) =
|
||||||
|
plugins.forEach { it.onAttachedToActivity(binding) }
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() =
|
||||||
|
plugins.forEach { it.onDetachedFromActivity() }
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) =
|
||||||
|
plugins.forEach { it.onReattachedToActivityForConfigChanges(binding) }
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() =
|
||||||
|
plugins.forEach { it.onDetachedFromActivityForConfigChanges() }
|
||||||
|
}
|
||||||
@@ -86,7 +86,7 @@ public func ux_keyboard_anim_gen() -> Int32 {
|
|||||||
|
|
||||||
// MARK: - Plugin
|
// MARK: - Plugin
|
||||||
|
|
||||||
public class KeyboardPlugin: NSObject, FlutterPlugin {
|
public class KeyboardPlugin: NSObject, NativePlugin {
|
||||||
fileprivate static var shared: KeyboardPlugin?
|
fileprivate static var shared: KeyboardPlugin?
|
||||||
|
|
||||||
fileprivate var wakeCallback: WakeCallback?
|
fileprivate var wakeCallback: WakeCallback?
|
||||||
@@ -112,15 +112,9 @@ public class KeyboardPlugin: NSObject, FlutterPlugin {
|
|||||||
fileprivate var animDuration: Double = 0
|
fileprivate var animDuration: Double = 0
|
||||||
fileprivate var animGeneration: Int32 = 0
|
fileprivate var animGeneration: Int32 = 0
|
||||||
|
|
||||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
public func register(with registrar: FlutterPluginRegistrar) {
|
||||||
let instance = KeyboardPlugin()
|
KeyboardPlugin.shared = self
|
||||||
KeyboardPlugin.shared = instance
|
startObserving()
|
||||||
|
|
||||||
instance.startObserving()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
result(FlutterMethodNotImplemented)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wake Dart so it reads the height on its next frame
|
/// Wake Dart so it reads the height on its next frame
|
||||||
|
|||||||
5
ios/Classes/NativePlugin.swift
Normal file
5
ios/Classes/NativePlugin.swift
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import Flutter
|
||||||
|
|
||||||
|
public protocol NativePlugin {
|
||||||
|
func register(with registrar: FlutterPluginRegistrar)
|
||||||
|
}
|
||||||
34
ios/Classes/SensorPlugin.swift
Normal file
34
ios/Classes/SensorPlugin.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import CoreMotion
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@_cdecl("ux_device_orientation")
|
||||||
|
public func ux_device_orientation() -> Int32 {
|
||||||
|
return SensorPlugin.shared?.currentIndex ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SensorPlugin: NSObject, NativePlugin {
|
||||||
|
fileprivate static var shared: SensorPlugin?
|
||||||
|
fileprivate var currentIndex: Int32 = 0
|
||||||
|
|
||||||
|
private let motion = CMMotionManager()
|
||||||
|
|
||||||
|
public func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
SensorPlugin.shared = self
|
||||||
|
guard motion.isDeviceMotionAvailable else { return }
|
||||||
|
motion.deviceMotionUpdateInterval = 1.0 / 10.0
|
||||||
|
motion.startDeviceMotionUpdates(to: .main) { [weak self] data, _ in
|
||||||
|
guard let self = self, let g = data?.gravity else { return }
|
||||||
|
if abs(g.z) > 0.8 { return }
|
||||||
|
let next: Int32
|
||||||
|
if abs(g.x) > abs(g.y) {
|
||||||
|
next = g.x < 0 ? 1 : 3
|
||||||
|
} else {
|
||||||
|
next = g.y < 0 ? 0 : 2
|
||||||
|
}
|
||||||
|
if next != self.currentIndex {
|
||||||
|
self.currentIndex = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ios/Classes/UxPlugin.swift
Normal file
20
ios/Classes/UxPlugin.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class UxPlugin: NSObject, FlutterPlugin {
|
||||||
|
private static var plugins: [NativePlugin] = []
|
||||||
|
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
plugins = [
|
||||||
|
KeyboardPlugin(),
|
||||||
|
SensorPlugin(),
|
||||||
|
]
|
||||||
|
for plugin in plugins {
|
||||||
|
plugin.register(with: registrar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/src/sensor.dart
Normal file
37
lib/src/sensor.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
DynamicLibrary? _initLib() {
|
||||||
|
if (Platform.isIOS) return DynamicLibrary.process();
|
||||||
|
if (Platform.isAndroid) return DynamicLibrary.open('libux_keyboard.so');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DynamicLibrary? _lib = _initLib();
|
||||||
|
|
||||||
|
int Function()? _lookupInt32(String name) {
|
||||||
|
if (_lib == null) return null;
|
||||||
|
try {
|
||||||
|
return _lib!.lookup<NativeFunction<Int32 Function()>>(name).asFunction<int Function()>();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _uxDeviceOrientation = _lookupInt32('ux_device_orientation');
|
||||||
|
|
||||||
|
class UxSensor {
|
||||||
|
UxSensor._();
|
||||||
|
|
||||||
|
/// Accelerometer-driven physical device rotation; updates regardless of
|
||||||
|
/// OS auto-rotate or app UI orientation lock.
|
||||||
|
static DeviceOrientation get orientation {
|
||||||
|
final idx = _uxDeviceOrientation?.call() ?? 0;
|
||||||
|
if (idx < 0 || idx >= DeviceOrientation.values.length) {
|
||||||
|
return DeviceOrientation.portraitUp;
|
||||||
|
}
|
||||||
|
return DeviceOrientation.values[idx];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,3 +9,4 @@ export 'src/bend_box.dart';
|
|||||||
export 'src/json_extension.dart';
|
export 'src/json_extension.dart';
|
||||||
export 'src/bezier.dart';
|
export 'src/bezier.dart';
|
||||||
export 'src/keyboard.dart';
|
export 'src/keyboard.dart';
|
||||||
|
export 'src/sensor.dart';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ description: >-
|
|||||||
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
|
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
|
||||||
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
|
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
|
||||||
and layout primitives.
|
and layout primitives.
|
||||||
version: 0.4.0
|
version: 0.5.0
|
||||||
homepage: https://swipelab.co/ux.html
|
homepage: https://swipelab.co/ux.html
|
||||||
repository: https://github.com/swipelab/ux
|
repository: https://github.com/swipelab/ux
|
||||||
issue_tracker: https://github.com/swipelab/ux/issues
|
issue_tracker: https://github.com/swipelab/ux/issues
|
||||||
@@ -31,7 +31,7 @@ flutter:
|
|||||||
plugin:
|
plugin:
|
||||||
platforms:
|
platforms:
|
||||||
ios:
|
ios:
|
||||||
pluginClass: KeyboardPlugin
|
pluginClass: UxPlugin
|
||||||
android:
|
android:
|
||||||
package: io.swipelab.ux
|
package: io.swipelab.ux
|
||||||
pluginClass: KeyboardPlugin
|
pluginClass: UxPlugin
|
||||||
|
|||||||
Reference in New Issue
Block a user