پرش به مطلب اصلی

نمایشگر تراز دو محوره

این نمونه دو مقدار شیب را از متغیرهای اپ می‌گیرد و با یک دایرهٔ متحرک نمایش می‌دهد. در حالت دستی، دو لغزنده ورودی‌ها را تغییر می‌دهند. وقتی هر دو مقدار در آستانهٔ تعیین‌شده باشند، رنگ نشانگر سبز می‌شود.

سه حالت ورودی

این نمونه اکنون می‌تواند JSON شتاب‌سنج را از عملکرد «شروع سنسور» بخواند و به زاویه تبدیل کند. sensorVariable نام متغیر خروجی آن عملکرد است. بدون این پارامتر، حالت دستی و دو متغیر زاویه مانند قبل کار می‌کنند. manual: false به‌تنهایی دریافت سنسور را شروع نمی‌کند.

افزودن نمونه​

  1. ویجت Flutter سفارشی را اضافه کنید.
  2. فایل level.dart را دانلود کنید و در فرم بارگذاری کنید.
  3. ارتفاع را «به اندازه محتوا» و عرض را دست‌کم حدود ۲۶۰ واحد منطقی قرار دهید.
  4. پارامترهای زیر را وارد کنید، ذخیره کنید و خروجی جدید بسازید.
{
"title": "تراز دو محوره",
"xVariable": "level_x",
"yVariable": "level_y",
"initialX": 0,
"initialY": 0,
"maxTilt": 30,
"tolerance": 1,
"manual": true
}

پارامترها​

کلیدنوع و پیش‌فرضکاربرد
titleرشته؛ «تراز دو محوره»عنوان کارت
xVariableرشته؛ level_xمتغیر شیب افقی بر حسب درجه
yVariableرشته؛ level_yمتغیر شیب عمودی بر حسب درجه
initialX، initialYعدد؛ 0مقدار نمایشی پیش از دریافت متغیر
maxTiltعدد؛ 30حد مقیاس نمایش و لغزنده؛ قدر مطلق آن به ۱ تا ۹۰ محدود می‌شود
toleranceعدد؛ 1آستانهٔ مجاز هر محور؛ قدر مطلق آن بین صفر و maxTilt محدود می‌شود
sensorVariableرشته؛ بدون مقدارنام متغیر JSON عملکرد شتاب‌سنج؛ بر ورودی دستی و xVariable/yVariable اولویت دارد
manualبولی؛ trueنمایش کنترل‌های دستی

مقدار مثبت X نشانگر را به راست و مقدار مثبت Y آن را به بالا می‌برد. مقدارهای موجود در متغیر بر مقدارهای اولیه اولویت دارند. برای دو محور نام‌های متفاوت و ساده، بدون نقطه و اندیس، انتخاب کنید.

نمایش و ورودی‌ها​

  • در بازهٔ نمایش، جابه‌جایی نشانگر با مقدار شیب متناسب است.
  • مقدار بیشتر از maxTilt، نشانگر را در حد مسیر نگه می‌دارد؛ عدد اصلی همچنان نمایش داده می‌شود.
  • سبزشدن نیاز دارد قدر مطلق هر دو مقدار کمتر یا مساوی tolerance باشد.
  • در حالت متغیر، تا هر دو مقدار دریافت نشوند پیام «در انتظار داده» نمایش داده می‌شود؛ مقدار اولیه، اندازه‌گیری دستگاه محسوب نمی‌شود.

در حالت دستی، تغییر لغزنده‌ها دو متغیر موقت را می‌سازد یا بازنویسی می‌کند. دکمهٔ صفر کردن ورودی‌ها هر دو عدد را صفر می‌کند و عملکرد اختیاری onReset را اجرا می‌کند. این دکمه کالیبراسیون سنسور نیست.

کنترل از دیگر بخش‌های اپ​

برای پنهان‌کردن لغزنده‌ها، manual را false کنید. دو متغیر عددی level_x و level_y بسازید و از عملکردهای پنل مقدارشان را تغییر دهید. نمونه با دو watchVar تغییرات را دریافت می‌کند.

اگر از قبل منبع دادهٔ واقعی در اپ وجود دارد، باید محورهای آن را به قرارداد این نمونه تبدیل کند و مقدارها را در متغیرها بنویسد. برای استفاده از عملکرد شتاب‌سنج، روش زیر را اجرا کنید؛ نمونه تبدیل شتاب به زاویه را انجام می‌دهد ولی کالیبراسیون مبنا ندارد.

تراز با شتاب‌سنج واقعی​

  1. عملکرد شروع سنسور را به رویداد ورود صفحه وصل کنید.
  2. شناسه را levelSensor، نوع را شتاب‌سنج (با گرانش)، متغیر خروجی را levelSensorData و فاصله را ۲۰۰ میلی‌ثانیه قرار دهید.
  3. نام خروجی را به متغیر دستگاه یا متغیر مشترک دیگری اختصاص ندهید؛ دادهٔ سنسور موقت است.
  4. این پارامترها را در ویجت بنویسید:
{"title":"تراز دستگاه","sensorVariable":"levelSensorData","manual":false,"maxTilt":30,"tolerance":1}

کد با watchVar همان JSON را می‌خواند. فقط نمونه‌های status: active و sensor: accelerometer معتبرند. از atan2(x, sqrt(y² + z²)) و فرمول مشابه برای محور Y، زاویه بر حسب درجه به دست می‌آید. شتاب حرکت بدون گرانش برای این روش مناسب نیست. این حالت در level_x و level_y چیزی نمی‌نویسد.

گوشی باید در جهت عمودی معمول رابط (portrait)، صفحه رو به بالا و تقریباً ثابت باشد. تکان‌دادن و شتاب حرکت اندازه‌گیری را منحرف می‌کند. محورهای این نمونه مختصات دستگاه‌اند؛ برای چرخش افقی رابط، نگاشت محور را خودتان تغییر دهید. صفر کردن ورودی دستی، کالیبراسیون شتاب‌سنج نیست و در حالت سنسور نمایش داده نمی‌شود.

برای توقف، عملکرد توقف سنسور با شناسهٔ levelSensor را اجرا کنید. عملکرد سنسور هنگام پوشیده‌شدن صفحه یا پس‌زمینه مکث می‌کند و با برگشت از سر گرفته می‌شود؛ با بسته‌شدن صفحه آزاد می‌شود. آخرین مقدار پس از توقف صریح باقی می‌ماند؛ برای حذف نمایش آن، متغیر را با app.removeVar('levelSensorData') پاک کنید.

در پیش‌نمایش می‌توانید مقادیر شبیه‌سازی x=0، y=0، z=9.81 را برای عملکرد تنظیم کنید؛ خود ویجت سفارشی در شبیه‌ساز اجرا نمی‌شود. برای مشاهدهٔ تراز، خروجی جدید بسازید. دریافت و خطاهای سنسور را در نمونهٔ سنسورها ببینید.

امتحان در خروجی​

  1. در حالت دستی هر دو لغزنده را صفر کنید؛ نشانگر وسط و سبز می‌شود.
  2. X را ۱۵ و Y را منفی ۱۰ کنید؛ نشانگر به راست و پایین می‌رود و نارنجی می‌شود.
  3. در حالت متغیر X را ۰٫۵ و Y را منفی ۰٫۵ قرار دهید؛ با آستانهٔ ۱، وضعیت سبز است.
  4. مقدار ۶۰ را برای X بنویسید؛ با مقیاس ۳۰، عدد ۶۰ نمایش داده می‌شود اما نشانگر از مسیر خارج نمی‌شود.
  5. در حالت سنسور، گوشی را افقی و ثابت روی سطح قرار دهید؛ سپس آرام به دو سمت متمایل کنید. وقتی دریافت متوقف یا خطادار است، پیام انتظار نمایش داده می‌شود.

کد کامل​

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:my_app/appeditor/custom/AppEditorWidgetContext.dart';

// Reads tilt variables or the JSON output of the page accelerometer action.
Widget buildCustomWidget(BuildContext context, AppEditorWidgetContext app) {
return LevelWidget(app: app);
}

class LevelWidget extends StatefulWidget {
const LevelWidget({super.key, required this.app});
final AppEditorWidgetContext app;

State<LevelWidget> createState() => _LevelWidgetState();
}

class _LevelWidgetState extends State<LevelWidget> {
late String _xVariable;
late String _yVariable;
late Stream<dynamic> _xValues;
late Stream<dynamic> _yValues;
String? _sensorVariable;

double? _tilt(dynamic data, String axis) {
if (data is! Map || data['status'] != 'active' ||
data['sensor'] != 'accelerometer') return null;
final x = (data['x'] as num?)?.toDouble();
final y = (data['y'] as num?)?.toDouble();
final z = (data['z'] as num?)?.toDouble();
if (x == null || y == null || z == null ||
!x.isFinite || !y.isFinite || !z.isFinite ||
x * x + y * y + z * z < 0.01) return null;
return axis == 'x'
? math.atan2(x, math.sqrt(y * y + z * z)) * 180 / math.pi
: math.atan2(y, math.sqrt(x * x + z * z)) * 180 / math.pi;
}

void _bind() {
_xVariable = widget.app.params['xVariable'] as String? ?? 'level_x';
_yVariable = widget.app.params['yVariable'] as String? ?? 'level_y';
_sensorVariable = widget.app.params['sensorVariable'] as String?;
if (_sensorVariable != null && _sensorVariable!.isNotEmpty) {
_xValues = widget.app.watchVar(_sensorVariable!).map((data) => _tilt(data, 'x'));
_yValues = widget.app.watchVar(_sensorVariable!).map((data) => _tilt(data, 'y'));
} else {
_xValues = widget.app.watchVar(_xVariable);
_yValues = widget.app.watchVar(_yVariable);
}
}


void initState() {
super.initState();
_bind();
}


void didUpdateWidget(covariant LevelWidget oldWidget) {
super.didUpdateWidget(oldWidget);
final args = widget.app.params;
if (_sensorVariable != args['sensorVariable'] ||
_xVariable != (args['xVariable'] as String? ?? 'level_x') ||
_yVariable != (args['yVariable'] as String? ?? 'level_y') ||
!identical(widget.app, oldWidget.app)) {
_bind();
}
}

double _number(dynamic value, double fallback) {
final result = value is num ? value.toDouble() : double.tryParse('$value');
return result != null && result.isFinite ? result : fallback;
}

Future<void> _publish(String name, double value) async {
try {
await widget.app.addVar(name: name, value: value, overwrite: true);
} catch (error) {
widget.app.showMessage('ثبت شیب ممکن نشد: $error');
}
}

Future<void> _reset() async {
await _publish(_xVariable, 0);
if (!mounted) return;
await _publish(_yVariable, 0);
if (!mounted || !widget.app.hasAction('onReset')) return;
try {
await widget.app.runAction('onReset');
} catch (error) {
widget.app.showMessage('اجرای عملکرد ممکن نشد: $error');
}
}


Widget build(BuildContext context) {
final args = widget.app.params;
final manual = (_sensorVariable == null || _sensorVariable!.isEmpty) &&
args['manual'] != false;
final maxTilt = _number(args['maxTilt'], 30).abs().clamp(1.0, 90.0);
final tolerance = _number(args['tolerance'], 1).abs().clamp(0.0, maxTilt);
return StreamBuilder<dynamic>(
stream: _xValues,
builder: (context, xSnapshot) => StreamBuilder<dynamic>(
stream: _yValues,
builder: (context, ySnapshot) {
final x = _number(xSnapshot.data, _number(args['initialX'], 0));
final y = _number(ySnapshot.data, _number(args['initialY'], 0));
final hasData = xSnapshot.hasData && ySnapshot.hasData;
final centered = hasData && x.abs() <= tolerance && y.abs() <= tolerance;
final color = centered ? Colors.green : Colors.orange;
final dx = (x / maxTilt).clamp(-1.0, 1.0) * 80;
final dy = -(y / maxTilt).clamp(-1.0, 1.0) * 80;
final distance = math.sqrt(dx * dx + dy * dy);
final scale = distance > 80 ? 80 / distance : 1.0;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
args['title'] as String? ?? 'تراز دو محوره',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
manual
? 'حالت دستی — داده سنسور خوانده نمی‌شود'
: (_sensorVariable?.isNotEmpty == true
? 'شیب از شتاب‌سنج؛ گوشی را ثابت نگه دارید'
: 'شیب از متغیرهای اپ'),
),
const SizedBox(height: 16),
SizedBox.square(
dimension: 210,
child: Stack(
alignment: Alignment.center,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: color, width: 2),
),
),
const SizedBox(
height: 1,
width: 190,
child: ColoredBox(color: Colors.grey),
),
const SizedBox(
height: 190,
width: 1,
child: ColoredBox(color: Colors.grey),
),
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey),
),
),
Transform.translate(
offset: Offset(dx * scale, dy * scale),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
),
],
),
),
const SizedBox(height: 12),
Text(
'X: ${x.toStringAsFixed(1)}° Y: ${y.toStringAsFixed(1)}°',
textDirection: TextDirection.ltr,
),
Text(
!manual && !hasData
? 'در انتظار داده؛ وضعیت تراز دستگاه مشخص نیست.'
: centered
? 'مقادیر ورودی در محدوده ترازند'
: 'مقادیر ورودی خارج از محدوده ترازند',
),
if (manual) ...[
const Text('شیب افقی'),
Directionality(
textDirection: TextDirection.ltr,
child: Slider(
min: -maxTilt,
max: maxTilt,
value: x.clamp(-maxTilt, maxTilt),
onChanged: (value) => _publish(_xVariable, value),
),
),
const Text('شیب عمودی'),
Directionality(
textDirection: TextDirection.ltr,
child: Slider(
min: -maxTilt,
max: maxTilt,
value: y.clamp(-maxTilt, maxTilt),
onChanged: (value) => _publish(_yVariable, value),
),
),
OutlinedButton(
onPressed: _reset,
child: const Text('صفر کردن ورودی‌ها'),
),
],
],
),
),
);
},
),
);
}
}