diff options
author | Hailey <me@haileyok.com> | 2024-08-07 14:45:06 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-07 14:45:06 -0700 |
commit | 1b02f81cb85333462e3a9a42accc05d09aca4f2c (patch) | |
tree | 766e80438c1f109a1a7d751e9f04b7f6242f9766 /modules | |
parent | fff2c079c2554861764974aaeeb56f79a25ba82a (diff) | |
download | voidsky-1b02f81cb85333462e3a9a42accc05d09aca4f2c.tar.zst |
[Video] Visibility detection view (#4741)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Diffstat (limited to 'modules')
11 files changed, 408 insertions, 2 deletions
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt new file mode 100644 index 000000000..ddbb05cde --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt @@ -0,0 +1,23 @@ +package expo.modules.blueskyswissarmy.visibilityview + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskyVisibilityViewModule : Module() { + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyVisibilityView") + + AsyncFunction("updateActiveViewAsync") { + VisibilityViewManager.updateActiveView() + } + + View(VisibilityView::class) { + Events(arrayOf("onChangeStatus")) + + Prop("enabled") { view: VisibilityView, prop: Boolean -> + view.isViewEnabled = prop + } + } + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt new file mode 100644 index 000000000..a55ab80d5 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt @@ -0,0 +1,63 @@ +package expo.modules.blueskyswissarmy.visibilityview + +import android.content.Context +import android.graphics.Rect +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class VisibilityView( + context: Context, + appContext: AppContext, +) : ExpoView(context, appContext) { + var isViewEnabled: Boolean = false + + private val onChangeStatus by EventDispatcher() + + private var isCurrentlyActive = false + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + VisibilityViewManager.addView(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + VisibilityViewManager.removeView(this) + } + + fun setIsCurrentlyActive(isActive: Boolean) { + if (isCurrentlyActive == isActive) { + return + } + + this.isCurrentlyActive = isActive + this.onChangeStatus( + mapOf( + "isActive" to isActive, + ), + ) + } + + fun getPositionOnScreen(): Rect? { + if (!this.isShown) { + return null + } + + val screenPosition = intArrayOf(0, 0) + this.getLocationInWindow(screenPosition) + return Rect( + screenPosition[0], + screenPosition[1], + screenPosition[0] + this.width, + screenPosition[1] + this.height, + ) + } + + fun isViewableEnough(): Boolean { + val positionOnScreen = this.getPositionOnScreen() ?: return false + val visibleArea = positionOnScreen.width() * positionOnScreen.height() + val totalArea = this.width * this.height + return visibleArea >= 0.5 * totalArea + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt new file mode 100644 index 000000000..ec1e49816 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt @@ -0,0 +1,82 @@ +package expo.modules.blueskyswissarmy.visibilityview + +import android.graphics.Rect + +class VisibilityViewManager { + companion object { + private val views = HashMap<Int, VisibilityView>() + private var currentlyActiveView: VisibilityView? = null + private var prevCount = 0 + + fun addView(view: VisibilityView) { + this.views[view.id] = view + + if (this.prevCount == 0) { + this.updateActiveView() + } + this.prevCount = this.views.count() + } + + fun removeView(view: VisibilityView) { + this.views.remove(view.id) + this.prevCount = this.views.count() + } + + fun updateActiveView() { + var activeView: VisibilityView? = null + val count = this.views.count() + + if (count == 1) { + val view = this.views.values.first() + if (view.isViewableEnough()) { + activeView = view + } + } else if (count > 1) { + val views = this.views.values + var mostVisibleView: VisibilityView? = null + var mostVisiblePosition: Rect? = null + + views.forEach { view -> + if (!view.isViewableEnough()) { + return + } + + val position = view.getPositionOnScreen() ?: return@forEach + val topY = position.centerY() - (position.height() / 2) + + if (topY >= 150) { + if (mostVisiblePosition == null) { + mostVisiblePosition = position + } + + if (position.centerY() <= mostVisiblePosition!!.centerY()) { + mostVisibleView = view + mostVisiblePosition = position + } + } + } + + activeView = mostVisibleView + } + + if (activeView == this.currentlyActiveView) { + return + } + + this.clearActiveView() + if (activeView != null) { + this.setActiveView(activeView) + } + } + + private fun clearActiveView() { + this.currentlyActiveView?.setIsCurrentlyActive(false) + this.currentlyActiveView = null + } + + private fun setActiveView(view: VisibilityView) { + view.setIsCurrentlyActive(true) + this.currentlyActiveView = view + } + } +} diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index adb535e7f..4cdc11e99 100644 --- a/modules/expo-bluesky-swiss-army/expo-module.config.json +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -1,12 +1,18 @@ { "platforms": ["ios", "tvos", "android", "web"], "ios": { - "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule", "ExpoPlatformInfoModule"] + "modules": [ + "ExpoBlueskySharedPrefsModule", + "ExpoBlueskyReferrerModule", + "ExpoBlueskyVisibilityViewModule", + "ExpoPlatformInfoModule" + ] }, "android": { "modules": [ "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule", "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule", + "expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule", "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule" ] } diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts index f62596cb7..ebd67913e 100644 --- a/modules/expo-bluesky-swiss-army/index.ts +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -1,5 +1,6 @@ import * as PlatformInfo from './src/PlatformInfo' import * as Referrer from './src/Referrer' import * as SharedPrefs from './src/SharedPrefs' +import VisibilityView from './src/VisibilityView' -export {PlatformInfo, Referrer, SharedPrefs} +export {PlatformInfo, Referrer, SharedPrefs, VisibilityView} diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift new file mode 100644 index 000000000..ec12a84af --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift @@ -0,0 +1,21 @@ +import ExpoModulesCore + +public class ExpoBlueskyVisibilityViewModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyVisibilityView") + + AsyncFunction("updateActiveViewAsync") { + VisibilityViewManager.shared.updateActiveView() + } + + View(VisibilityView.self) { + Events([ + "onChangeStatus" + ]) + + Prop("enabled") { (view: VisibilityView, prop: Bool) in + view.enabled = prop + } + } + } +} diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift new file mode 100644 index 000000000..ae8e16868 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift @@ -0,0 +1,86 @@ +import Foundation + +class VisibilityViewManager { + static let shared = VisibilityViewManager() + + private let views = NSHashTable<VisibilityView>(options: .weakMemory) + private var currentlyActiveView: VisibilityView? + private var screenHeight: CGFloat = UIScreen.main.bounds.height + private var prevCount = 0 + + func addView(_ view: VisibilityView) { + self.views.add(view) + + if self.prevCount == 0 { + self.updateActiveView() + } + self.prevCount = self.views.count + } + + func removeView(_ view: VisibilityView) { + self.views.remove(view) + self.prevCount = self.views.count + } + + func updateActiveView() { + DispatchQueue.main.async { + var activeView: VisibilityView? + + if self.views.count == 1 { + let view = self.views.allObjects[0] + if view.isViewableEnough() { + activeView = view + } + } else if self.views.count > 1 { + let views = self.views.allObjects + var mostVisibleView: VisibilityView? + var mostVisiblePosition: CGRect? + + views.forEach { view in + if !view.isViewableEnough() { + return + } + + guard let position = view.getPositionOnScreen() else { + return + } + + if position.minY >= 150 { + if mostVisiblePosition == nil { + mostVisiblePosition = position + } + + if let unwrapped = mostVisiblePosition, + position.minY <= unwrapped.minY { + mostVisibleView = view + mostVisiblePosition = position + } + } + } + + activeView = mostVisibleView + } + + if activeView == self.currentlyActiveView { + return + } + + self.clearActiveView() + if let view = activeView { + self.setActiveView(view) + } + } + } + + private func clearActiveView() { + if let currentlyActiveView = self.currentlyActiveView { + currentlyActiveView.setIsCurrentlyActive(isActive: false) + self.currentlyActiveView = nil + } + } + + private func setActiveView(_ view: VisibilityView) { + view.setIsCurrentlyActive(isActive: true) + self.currentlyActiveView = view + } +} diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift new file mode 100644 index 000000000..fd99ee493 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift @@ -0,0 +1,69 @@ +import ExpoModulesCore + +class VisibilityView: ExpoView { + var enabled = false { + didSet { + if enabled { + VisibilityViewManager.shared.removeView(self) + } + } + } + + private let onChangeStatus = EventDispatcher() + private var isCurrentlyActiveView = false + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + } + + public override func willMove(toWindow newWindow: UIWindow?) { + super.willMove(toWindow: newWindow) + + if !self.enabled { + return + } + + if newWindow == nil { + VisibilityViewManager.shared.removeView(self) + } else { + VisibilityViewManager.shared.addView(self) + } + } + + func setIsCurrentlyActive(isActive: Bool) { + if isCurrentlyActiveView == isActive { + return + } + self.isCurrentlyActiveView = isActive + self.onChangeStatus([ + "isActive": isActive + ]) + } +} + +// 🚨 DANGER 🚨 +// These functions need to be called from the main thread. Xcode will warn you if you call one of them +// off the main thread, so pay attention! +extension UIView { + func getPositionOnScreen() -> CGRect? { + if let window = self.window { + return self.convert(self.bounds, to: window) + } + return nil + } + + func isViewableEnough() -> Bool { + guard let window = self.window else { + return false + } + + let viewFrameOnScreen = self.convert(self.bounds, to: window) + let screenBounds = window.bounds + let intersection = viewFrameOnScreen.intersection(screenBounds) + + let viewHeight = viewFrameOnScreen.height + let intersectionHeight = intersection.height + + return intersectionHeight >= 0.5 * viewHeight + } +} diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx new file mode 100644 index 000000000..9d0e8cf22 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' +import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' + +import {VisibilityViewProps} from './types' +const NativeView: React.ComponentType<{ + onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void + children: React.ReactNode + enabled: Boolean + style: StyleProp<ViewStyle> +}> = requireNativeViewManager('ExpoBlueskyVisibilityView') + +const NativeModule = requireNativeModule('ExpoBlueskyVisibilityView') + +export async function updateActiveViewAsync() { + await NativeModule.updateActiveViewAsync() +} + +export default function VisibilityView({ + children, + onChangeStatus: onChangeStatusOuter, + enabled, +}: VisibilityViewProps) { + const onChangeStatus = React.useCallback( + (e: {nativeEvent: {isActive: boolean}}) => { + onChangeStatusOuter(e.nativeEvent.isActive) + }, + [onChangeStatusOuter], + ) + + return ( + <NativeView + onChangeStatus={onChangeStatus} + enabled={enabled} + style={{flex: 1}}> + {children} + </NativeView> + ) +} diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx new file mode 100644 index 000000000..8b4f1928c --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx @@ -0,0 +1,10 @@ +import {NotImplementedError} from '../NotImplemented' +import {VisibilityViewProps} from './types' + +export async function updateActiveViewAsync() { + throw new NotImplementedError() +} + +export default function VisibilityView({children}: VisibilityViewProps) { + return children +} diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts b/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts new file mode 100644 index 000000000..312acf2d2 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts @@ -0,0 +1,6 @@ +import React from 'react' +export interface VisibilityViewProps { + children: React.ReactNode + onChangeStatus: (isActive: boolean) => void + enabled: boolean +} |