orientation
This commit is contained in:
@@ -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+)
|
||||
|
||||
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.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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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/bezier.dart';
|
||||
export 'src/keyboard.dart';
|
||||
export 'src/sensor.dart';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user