generated from Grigo/AndroidTemplate
Initial commit: AIS Map Android application
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
package com.grigowashere.aismap.view;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public abstract class BaseDockWidget extends FrameLayout {
|
||||
private static final String TAG = "BaseDockWidget";
|
||||
|
||||
// Константы
|
||||
protected static final int CIRCLE_SIZE_DP = 120;
|
||||
protected static final int DEFAULT_DOCK_HEIGHT_DP = 80;
|
||||
protected static final float MIN_SCALE = 0.5f;
|
||||
protected static final float MAX_SCALE = 2.0f;
|
||||
protected static final float SCALE_STEP = 0.1f;
|
||||
|
||||
// Состояние виджета
|
||||
protected boolean isDocked = true; // По умолчанию в dock-режиме
|
||||
protected boolean dockTop = true;
|
||||
protected boolean isMorphing = false;
|
||||
protected float morphProgress = 0.0f; // 0 = dock, 1 = circle
|
||||
|
||||
// Перетаскивание
|
||||
protected boolean dragging = false;
|
||||
protected float dX, dY;
|
||||
protected Float targetDragX = null;
|
||||
protected Float targetDragY = null;
|
||||
|
||||
// Изменение размера в dock режиме
|
||||
protected boolean resizingDock = false;
|
||||
protected float lastTouchY;
|
||||
protected int dockHeightPx = 0;
|
||||
|
||||
// Масштабирование
|
||||
protected float scaleFactor = 1.0f;
|
||||
protected float initialDistance = 0;
|
||||
protected float initialScale = 1.0f;
|
||||
|
||||
// Анимация
|
||||
protected ValueAnimator morphAnimator;
|
||||
|
||||
// Интерфейс для уведомления об изменении размера
|
||||
public interface OnDockResizeListener {
|
||||
void onDockResize(int newHeight);
|
||||
}
|
||||
protected OnDockResizeListener dockResizeListener;
|
||||
|
||||
public BaseDockWidget(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public BaseDockWidget(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setClickable(true);
|
||||
setFocusable(true);
|
||||
|
||||
// Инициализируем в dock-режиме
|
||||
post(() -> {
|
||||
if (isDocked) {
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
setX(0);
|
||||
setY(0);
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
lp.height = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
dockHeightPx = 0; // Сбрасываем сохраненную высоту
|
||||
setLayoutParams(lp);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (isMorphing) return true;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
return handleTouchDown(event);
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
return handleTouchMove(event);
|
||||
case MotionEvent.ACTION_UP:
|
||||
return handleTouchUp(event);
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
return handlePointerDown(event);
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
return handlePointerUp(event);
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private boolean handleTouchDown(MotionEvent event) {
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
if (isDocked) {
|
||||
// Проверяем зоны изменения размера в зависимости от позиции закрепления
|
||||
if (dockTop) {
|
||||
// Если закреплен сверху, зона изменения размера только снизу
|
||||
if (y > getHeight() - dp(24)) {
|
||||
resizingDock = true;
|
||||
lastTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Если закреплен снизу, зона изменения размера только сверху
|
||||
if (y < dp(24)) {
|
||||
resizingDock = true;
|
||||
lastTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Если нажали в центральной области dock-виджета, переводим в movable режим
|
||||
// Вычисляем новую позицию, чтобы виджет был под пальцем
|
||||
float newX = event.getRawX() - dp(CIRCLE_SIZE_DP) / 2;
|
||||
float newY = event.getRawY() - dp(CIRCLE_SIZE_DP) / 2;
|
||||
|
||||
// Ограничиваем в пределах экрана
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
newX = Math.max(0, Math.min(newX, parent.getWidth() - dp(CIRCLE_SIZE_DP)));
|
||||
newY = Math.max(0, Math.min(newY, parent.getHeight() - dp(CIRCLE_SIZE_DP)));
|
||||
}
|
||||
|
||||
setDocked(false, dockTop, newX, newY);
|
||||
}
|
||||
|
||||
// Обычное перетаскивание
|
||||
dragging = true;
|
||||
dX = getX() - event.getRawX();
|
||||
dY = getY() - event.getRawY();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleTouchMove(MotionEvent event) {
|
||||
if (resizingDock) {
|
||||
handleDockResize(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обработка масштабирования двумя пальцами
|
||||
if (event.getPointerCount() == 2 && initialDistance > 0) {
|
||||
float distance = getDistance(event);
|
||||
float scale = distance / initialDistance;
|
||||
scaleFactor = Math.max(MIN_SCALE, Math.min(MAX_SCALE, initialScale * scale));
|
||||
requestLayout();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
float newX = event.getRawX() + dX;
|
||||
float newY = event.getRawY() + dY;
|
||||
|
||||
// Ограничиваем движение в пределах родителя
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
newX = Math.max(0, Math.min(newX, parent.getWidth() - getWidth()));
|
||||
newY = Math.max(0, Math.min(newY, parent.getHeight() - getHeight()));
|
||||
}
|
||||
|
||||
setX(newX);
|
||||
setY(newY);
|
||||
|
||||
// Проверяем возможность докинга
|
||||
checkDocking(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleTouchUp(MotionEvent event) {
|
||||
if (resizingDock) {
|
||||
resizingDock = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
|
||||
// Если виджет находится в зоне докинга, доким его
|
||||
if (shouldDock(event)) {
|
||||
performDocking(event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handlePointerDown(MotionEvent event) {
|
||||
if (event.getPointerCount() == 2) {
|
||||
initialDistance = getDistance(event);
|
||||
initialScale = scaleFactor;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handlePointerUp(MotionEvent event) {
|
||||
if (event.getPointerCount() < 2) {
|
||||
initialDistance = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleDockResize(MotionEvent event) {
|
||||
float deltaY = event.getRawY() - lastTouchY;
|
||||
lastTouchY = event.getRawY();
|
||||
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
int newHeight = lp.height;
|
||||
|
||||
// Направление изменения размера зависит от позиции закрепления
|
||||
if (dockTop) {
|
||||
// Если закреплен сверху, увеличиваем размер при движении вниз
|
||||
newHeight += (int) deltaY;
|
||||
} else {
|
||||
// Если закреплен снизу, увеличиваем размер при движении вверх
|
||||
newHeight -= (int) deltaY;
|
||||
}
|
||||
|
||||
// Ограничиваем минимальную и максимальную высоту
|
||||
int minHeight = (int) dp(40);
|
||||
int maxHeight = ((ViewGroup) getParent()).getHeight() / 2;
|
||||
|
||||
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
|
||||
|
||||
if (newHeight != lp.height) {
|
||||
lp.height = newHeight;
|
||||
dockHeightPx = newHeight;
|
||||
setLayoutParams(lp);
|
||||
|
||||
// Если закреплен снизу, нужно также изменить позицию Y
|
||||
if (!dockTop) {
|
||||
float newY = ((ViewGroup) getParent()).getHeight() - newHeight;
|
||||
setY(newY);
|
||||
}
|
||||
|
||||
if (dockResizeListener != null) {
|
||||
dockResizeListener.onDockResize(newHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDocking(MotionEvent event) {
|
||||
// Проверяем расстояние до краев экрана
|
||||
float screenHeight = ((ViewGroup) getParent()).getHeight();
|
||||
float y = event.getRawY();
|
||||
|
||||
float dockThreshold = dp(100);
|
||||
|
||||
if (y < dockThreshold) {
|
||||
// Близко к верхнему краю
|
||||
targetDragX = 0f;
|
||||
targetDragY = 0f;
|
||||
} else if (y > screenHeight - dockThreshold) {
|
||||
// Близко к нижнему краю
|
||||
targetDragX = 0f;
|
||||
targetDragY = screenHeight - getHeight();
|
||||
} else {
|
||||
targetDragX = null;
|
||||
targetDragY = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldDock(MotionEvent event) {
|
||||
return targetDragX != null && targetDragY != null;
|
||||
}
|
||||
|
||||
private void performDocking(MotionEvent event) {
|
||||
float screenHeight = ((ViewGroup) getParent()).getHeight();
|
||||
float y = event.getRawY();
|
||||
|
||||
boolean dockToTop = y < screenHeight / 2;
|
||||
|
||||
// При докинге всегда устанавливаем размер по умолчанию
|
||||
dockHeightPx = 0; // Сбрасываем сохраненную высоту
|
||||
|
||||
setDocked(true, dockToTop, 0f, dockToTop ? 0f : screenHeight - dp(DEFAULT_DOCK_HEIGHT_DP));
|
||||
}
|
||||
|
||||
private float getDistance(MotionEvent event) {
|
||||
if (event.getPointerCount() < 2) return 0;
|
||||
|
||||
float x = event.getX(0) - event.getX(1);
|
||||
float y = event.getY(0) - event.getY(1);
|
||||
return (float) Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (isDocked) {
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = dockHeightPx > 0 ? dockHeightPx : (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
setMeasuredDimension(width, height);
|
||||
} else {
|
||||
int size = (int)(dp(CIRCLE_SIZE_DP) * scaleFactor);
|
||||
setMeasuredDimension(size, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
// Вызываем соответствующий метод отрисовки
|
||||
if (isDocked) {
|
||||
onDrawDock(canvas);
|
||||
} else {
|
||||
onDrawCircle(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDocked(boolean docked, boolean top) {
|
||||
setDocked(docked, top, getX(), getY());
|
||||
}
|
||||
|
||||
public void setDocked(boolean docked, boolean top, float targetX, float targetY) {
|
||||
if (this.isDocked == docked && this.dockTop == top && getX() == targetX && getY() == targetY) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dockTop = top;
|
||||
|
||||
if (morphAnimator != null && morphAnimator.isRunning()) {
|
||||
morphAnimator.cancel();
|
||||
}
|
||||
|
||||
float startMorph = morphProgress;
|
||||
float endMorph = docked ? 0f : 1f;
|
||||
|
||||
int startW = getWidth();
|
||||
int startH = getHeight();
|
||||
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
int parentWidth = parent.getWidth();
|
||||
int parentHeight = parent.getHeight();
|
||||
int dockHeight = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
int circleSize = (int) dp(CIRCLE_SIZE_DP);
|
||||
|
||||
int endW = docked ? parentWidth : circleSize;
|
||||
int endH = docked ? dockHeight : circleSize;
|
||||
|
||||
float startX = getX();
|
||||
float startY = getY();
|
||||
float endX = targetX;
|
||||
float endY = targetY;
|
||||
|
||||
// Если доким в нижнюю часть, корректируем позицию Y
|
||||
if (docked && !top) {
|
||||
endY = parentHeight - dockHeight;
|
||||
}
|
||||
|
||||
// Сохраняем финальные значения для использования в lambda и inner class
|
||||
final float finalStartX = startX;
|
||||
final float finalStartY = startY;
|
||||
final float finalEndX = endX;
|
||||
final float finalEndY = endY;
|
||||
|
||||
morphAnimator = ValueAnimator.ofFloat(0f, 1f);
|
||||
morphAnimator.setDuration(350);
|
||||
morphAnimator.addUpdateListener(anim -> {
|
||||
float t = (float) anim.getAnimatedValue();
|
||||
morphProgress = startMorph + (endMorph - startMorph) * t;
|
||||
|
||||
int w = (int) (startW + (endW - startW) * t);
|
||||
int h = (int) (startH + (endH - startH) * t);
|
||||
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = w;
|
||||
lp.height = h;
|
||||
setLayoutParams(lp);
|
||||
|
||||
setX(finalStartX + (finalEndX - finalStartX) * t);
|
||||
setY(finalStartY + (finalEndY - finalStartY) * t);
|
||||
|
||||
postInvalidateOnAnimation();
|
||||
});
|
||||
|
||||
morphAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = endW;
|
||||
lp.height = endH;
|
||||
setLayoutParams(lp);
|
||||
|
||||
setX(finalEndX);
|
||||
setY(finalEndY);
|
||||
morphProgress = endMorph;
|
||||
|
||||
postInvalidateOnAnimation();
|
||||
|
||||
isMorphing = false;
|
||||
}
|
||||
});
|
||||
|
||||
morphAnimator.start();
|
||||
this.isDocked = docked;
|
||||
isMorphing = true;
|
||||
}
|
||||
|
||||
public boolean isDocked() {
|
||||
return isDocked;
|
||||
}
|
||||
|
||||
public boolean isDockTop() {
|
||||
return dockTop;
|
||||
}
|
||||
|
||||
protected boolean isMorphing() {
|
||||
return isMorphing;
|
||||
}
|
||||
|
||||
public void setOnDockResizeListener(OnDockResizeListener listener) {
|
||||
this.dockResizeListener = listener;
|
||||
}
|
||||
|
||||
protected float dp(float dp) {
|
||||
return dp * getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
// Абстрактные методы для переопределения в наследниках
|
||||
protected abstract void onDrawDock(Canvas canvas);
|
||||
protected abstract void onDrawCircle(Canvas canvas);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package com.grigowashere.aismap.view;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.utils.GeoUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompassView extends BaseDockWidget {
|
||||
private static final String TAG = "CompassView";
|
||||
|
||||
private float targetAzimuth = 0;
|
||||
private float currentAzimuth = 0;
|
||||
private float magneticCompass = 0; // магнитный компас
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint vesselPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path vesselPath = new Path();
|
||||
private final String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
|
||||
private float centerX;
|
||||
private float centerY;
|
||||
private static final float SMOOTHING_FACTOR = 0.15f;
|
||||
private List<AISVessel> nearbyVessels = new ArrayList<>();
|
||||
private Vessel ourVessel; // наше судно для расчета расстояний
|
||||
private static final float MAX_DISPLAY_DISTANCE = 10000; // 10 км
|
||||
private static final float MIN_VESSEL_SIZE = 10; // минимальный размер значка
|
||||
private static final float MAX_VESSEL_SIZE = 30; // максимальный размер значка
|
||||
|
||||
public CompassView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CompassView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTextSize(36f);
|
||||
|
||||
vesselPaint.setStyle(Paint.Style.FILL);
|
||||
vesselPaint.setAntiAlias(true);
|
||||
|
||||
// Устанавливаем фон для видимости
|
||||
setBackgroundColor(Color.argb(200, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
centerX = w / 2f;
|
||||
centerY = h / 2f;
|
||||
}
|
||||
|
||||
private float getShortestRotation(float start, float end) {
|
||||
float diff = end - start;
|
||||
while (diff > 180) diff -= 360;
|
||||
while (diff < -180) diff += 360;
|
||||
return diff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Прямая шкала (dock-режим)
|
||||
@Override
|
||||
protected void onDrawDock(Canvas canvas) {
|
||||
Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight());
|
||||
|
||||
float w = getWidth();
|
||||
float h = getHeight();
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
|
||||
return;
|
||||
}
|
||||
|
||||
// Простой фон для начала
|
||||
paint.setColor(Color.argb(200, 0, 0, 0));
|
||||
canvas.drawRect(0, 0, w, h, paint);
|
||||
|
||||
// Масштабируем размеры в зависимости от высоты виджета
|
||||
float baseHeight = dp(80); // базовая высота
|
||||
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
|
||||
|
||||
// Простой текст для проверки
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(24 * scaleFactor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText("КОМПАС", w/2, h/2, paint);
|
||||
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
|
||||
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
|
||||
|
||||
// Плавное обновление азимута
|
||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||
if (Math.abs(diff) > 0.1f) {
|
||||
currentAzimuth += diff * SMOOTHING_FACTOR;
|
||||
if (currentAzimuth > 360) currentAzimuth -= 360;
|
||||
if (currentAzimuth < 0) currentAzimuth += 360;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
// Рисуем простую шкалу
|
||||
float centerX = w / 2f;
|
||||
float centerY = h / 2f;
|
||||
float visibleDegrees = 120;
|
||||
|
||||
// Рисуем деления шкалы
|
||||
for (int degree = 0; degree < 360; degree += 15) {
|
||||
// Вычисляем относительное положение деления
|
||||
float relativeDegree = (degree - currentAzimuth + 360) % 360;
|
||||
if (relativeDegree > 180) relativeDegree -= 360;
|
||||
|
||||
// Рисуем только видимые деления
|
||||
if (Math.abs(relativeDegree) <= visibleDegrees / 2) {
|
||||
float x = centerX + (relativeDegree / (visibleDegrees / 2)) * (w / 2);
|
||||
float lineHeight = (degree % 30 == 0) ? 20 * scaleFactor : 10 * scaleFactor;
|
||||
canvas.drawLine(x, centerY - lineHeight, x, centerY + lineHeight, paint);
|
||||
|
||||
if (degree % 30 == 0) {
|
||||
String degreeText = String.valueOf(degree);
|
||||
paint.setTextSize(16 * scaleFactor);
|
||||
canvas.drawText(degreeText, x, centerY - 30 * scaleFactor, paint);
|
||||
}
|
||||
if (degree % 45 == 0) {
|
||||
int directionIndex = (degree / 45) % 8;
|
||||
if (directionIndex < directions.length) {
|
||||
paint.setTextSize(18 * scaleFactor);
|
||||
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рисуем суда
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float relativeBearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
if (relativeBearing > 180) relativeBearing -= 360;
|
||||
if (Math.abs(relativeBearing) <= visibleDegrees / 2) {
|
||||
float x = centerX + (relativeBearing / (visibleDegrees / 2)) * (w / 2);
|
||||
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
|
||||
float size = calculateVesselSize((float) distance) * scaleFactor;
|
||||
vesselPaint.setColor(getVesselColor(vessel));
|
||||
drawVesselTriangle(canvas, x, centerY, size, (float) (vessel.getCourse() - currentAzimuth));
|
||||
}
|
||||
}
|
||||
|
||||
// Центральная линия (направление вперёд)
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStrokeWidth(3 * scaleFactor);
|
||||
canvas.drawLine(centerX, centerY - h/2, centerX, centerY + h/2, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setStrokeWidth(1);
|
||||
|
||||
// Выделяем зону resize в зависимости от позиции закрепления
|
||||
if (isDocked) {
|
||||
Paint resizePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
resizePaint.setColor(Color.argb(120, 255, 255, 255));
|
||||
resizePaint.setStyle(Paint.Style.STROKE);
|
||||
resizePaint.setStrokeWidth(2);
|
||||
|
||||
paint.setTextSize(12);
|
||||
paint.setColor(Color.WHITE);
|
||||
|
||||
if (isDockTop()) {
|
||||
// Если закреплен сверху, показываем зону resize снизу
|
||||
canvas.drawRect(0, h - dp(24), w, h, resizePaint);
|
||||
canvas.drawText("↕", w/2, h - dp(12), paint);
|
||||
} else {
|
||||
// Если закреплен снизу, показываем зону resize сверху
|
||||
canvas.drawRect(0, 0, w, dp(24), resizePaint);
|
||||
canvas.drawText("↕", w/2, dp(12), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Круглый компас (draggable-режим)
|
||||
@Override
|
||||
protected void onDrawCircle(Canvas canvas) {
|
||||
Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight());
|
||||
|
||||
float w = getWidth();
|
||||
float h = getHeight();
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
|
||||
return;
|
||||
}
|
||||
|
||||
float cx = w / 2f;
|
||||
float cy = h / 2f;
|
||||
float radius = Math.min(w, h) / 2f * 0.9f;
|
||||
|
||||
// Масштабируем размеры в зависимости от размера виджета
|
||||
float baseSize = dp(120); // базовая высота
|
||||
float scaleFactor = Math.max(0.8f, Math.min(2.0f, Math.min(w, h) / baseSize));
|
||||
|
||||
// Фон
|
||||
paint.setColor(Color.argb(200, 0, 0, 0));
|
||||
canvas.drawCircle(cx, cy, radius, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
|
||||
// Плавное обновление азимута
|
||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||
if (Math.abs(diff) > 0.1f) {
|
||||
currentAzimuth += diff * SMOOTHING_FACTOR;
|
||||
if (currentAzimuth > 360) currentAzimuth -= 360;
|
||||
if (currentAzimuth < 0) currentAzimuth += 360;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
// Деления и метки по кругу
|
||||
for (int degree = 0; degree < 360; degree += 30) {
|
||||
float angle = (float) Math.toRadians(degree - currentAzimuth);
|
||||
float x1 = cx + (float) Math.sin(angle) * (radius * 0.85f);
|
||||
float y1 = cy - (float) Math.cos(angle) * (radius * 0.85f);
|
||||
float x2 = cx + (float) Math.sin(angle) * radius;
|
||||
float y2 = cy - (float) Math.cos(angle) * radius;
|
||||
paint.setStrokeWidth(2 * scaleFactor);
|
||||
canvas.drawLine(x1, y1, x2, y2, paint);
|
||||
|
||||
if (degree % 90 == 0) {
|
||||
int directionIndex = (degree / 90) % 4;
|
||||
String[] mainDirections = {"N", "E", "S", "W"};
|
||||
float dx = cx + (float) Math.sin(angle) * (radius - 25 * scaleFactor);
|
||||
float dy = cy - (float) Math.cos(angle) * (radius - 25 * scaleFactor);
|
||||
paint.setTextSize(16 * scaleFactor);
|
||||
canvas.drawText(mainDirections[directionIndex], dx, dy, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// Рисуем суда по кругу
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float bearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
float angle = (float) Math.toRadians(bearing);
|
||||
float vesselRadius = radius * 0.6f;
|
||||
float vx = cx + (float) Math.sin(angle) * vesselRadius;
|
||||
float vy = cy - (float) Math.cos(angle) * vesselRadius;
|
||||
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
|
||||
float size = calculateVesselSize((float) distance) * scaleFactor;
|
||||
vesselPaint.setColor(getVesselColor(vessel));
|
||||
drawVesselTriangle(canvas, vx, vy, size, (float) (vessel.getCourse() - currentAzimuth));
|
||||
}
|
||||
|
||||
// Центральная линия (направление вперёд)
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStrokeWidth(3 * scaleFactor);
|
||||
canvas.drawLine(cx, cy, cx, cy - radius, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setStrokeWidth(1);
|
||||
|
||||
// Текст азимута в центре
|
||||
paint.setTextSize(14 * scaleFactor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText((int)currentAzimuth + "°", cx, cy + 5 * scaleFactor, paint);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private float calculateVesselSize(float distance) {
|
||||
if (distance > MAX_DISPLAY_DISTANCE) return MIN_VESSEL_SIZE;
|
||||
// Линейная интерполяция размера от расстояния
|
||||
float ratio = 1 - Math.min(distance / MAX_DISPLAY_DISTANCE, 1);
|
||||
return MIN_VESSEL_SIZE + (MAX_VESSEL_SIZE - MIN_VESSEL_SIZE) * ratio;
|
||||
}
|
||||
|
||||
private int getVesselColor(AISVessel vessel) {
|
||||
// Можно настроить цвета в зависимости от параметров судна
|
||||
// Используем navigation status из AIS данных
|
||||
int navStatus = GeoUtils.getNavigationStatusCode(vessel.getNavigationalStatus());
|
||||
switch (navStatus) {
|
||||
case 0: // Under way using engine
|
||||
return Color.GREEN;
|
||||
case 1: // At anchor
|
||||
return Color.YELLOW;
|
||||
case 5: // Moored
|
||||
return Color.BLUE;
|
||||
default:
|
||||
return Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawVesselTriangle(Canvas canvas, float x, float y, float size, float rotation) {
|
||||
vesselPath.reset();
|
||||
|
||||
// Создаем треугольник
|
||||
float halfSize = size / 2;
|
||||
vesselPath.moveTo(x, y - halfSize); // вершина
|
||||
vesselPath.lineTo(x - halfSize, y + halfSize); // левый нижний угол
|
||||
vesselPath.lineTo(x + halfSize, y + halfSize); // правый нижний угол
|
||||
vesselPath.close();
|
||||
|
||||
// Поворачиваем треугольник
|
||||
canvas.save();
|
||||
canvas.rotate(rotation, x, y);
|
||||
canvas.drawPath(vesselPath, vesselPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public void setAzimuth(float azimuth) {
|
||||
this.targetAzimuth = azimuth;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setMagneticCompass(float magneticCompass) {
|
||||
this.magneticCompass = magneticCompass;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void updateNearbyVessels(List<AISVessel> vessels) {
|
||||
this.nearbyVessels = vessels;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setOurVessel(Vessel ourVessel) {
|
||||
this.ourVessel = ourVessel;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user