orientation

This commit is contained in:
agra
2026-04-22 16:22:45 +03:00
parent 3032442c31
commit 2113537078
14 changed files with 231 additions and 16 deletions

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.10)
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)
# 16KB page size support (required for Android 15+)

View 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;
}

View File

@@ -7,12 +7,11 @@ import android.view.ViewTreeObserver
import android.view.WindowInsets
import android.view.WindowInsetsAnimation
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.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
class KeyboardPlugin : NativePlugin, MethodChannel.MethodCallHandler {
private var methodChannel: MethodChannel? = null
private var activity: Activity? = null
private var windowFocusListener: ViewTreeObserver.OnWindowFocusChangeListener? = null

View 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()
}

View File

@@ -0,0 +1,9 @@
package io.swipelab.ux
object SensorBridge {
init {
System.loadLibrary("ux_keyboard")
}
@JvmStatic external fun nSetDeviceOrientation(v: Int)
}

View 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
}
}
}
}

View 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() }
}

View File

@@ -86,7 +86,7 @@ public func ux_keyboard_anim_gen() -> Int32 {
// MARK: - Plugin
public class KeyboardPlugin: NSObject, FlutterPlugin {
public class KeyboardPlugin: NSObject, NativePlugin {
fileprivate static var shared: KeyboardPlugin?
fileprivate var wakeCallback: WakeCallback?
@@ -112,15 +112,9 @@ public class KeyboardPlugin: NSObject, FlutterPlugin {
fileprivate var animDuration: Double = 0
fileprivate var animGeneration: Int32 = 0
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KeyboardPlugin()
KeyboardPlugin.shared = instance
instance.startObserving()
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result(FlutterMethodNotImplemented)
public func register(with registrar: FlutterPluginRegistrar) {
KeyboardPlugin.shared = self
startObserving()
}
/// Wake Dart so it reads the height on its next frame

View File

@@ -0,0 +1,5 @@
import Flutter
public protocol NativePlugin {
func register(with registrar: FlutterPluginRegistrar)
}

View 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
}
}
}
}

View 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
View 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];
}
}

View File

@@ -9,3 +9,4 @@ export 'src/bend_box.dart';
export 'src/json_extension.dart';
export 'src/bezier.dart';
export 'src/keyboard.dart';
export 'src/sensor.dart';

View File

@@ -3,7 +3,7 @@ description: >-
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
and layout primitives.
version: 0.4.0
version: 0.5.0
homepage: https://swipelab.co/ux.html
repository: https://github.com/swipelab/ux
issue_tracker: https://github.com/swipelab/ux/issues
@@ -31,7 +31,7 @@ flutter:
plugin:
platforms:
ios:
pluginClass: KeyboardPlugin
pluginClass: UxPlugin
android:
package: io.swipelab.ux
pluginClass: KeyboardPlugin
pluginClass: UxPlugin