cls = EditText.class;
+ Method method;
+ try {
+ method = cls.getMethod("setShowSoftInputOnFocus", boolean.class);
+ method.setAccessible(true);
+ method.invoke(editText, false);
+ } catch (Exception e) {
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ }
+ }
+ }
+
+ /**
+ * 禁止输入框复制粘贴菜单
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ public static void disableCopyAndPaste(final EditText editText) {
+ try {
+ if (editText == null) {
+ return;
+ }
+ editText.setOnLongClickListener(v -> true);
+ editText.setLongClickable(false);
+ editText.setOnTouchListener((v, event) -> {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ setInsertionDisabled(editText);
+ }
+ return false;
+ });
+ editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+ });
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void setInsertionDisabled(EditText editText) {
+ try {
+ Field editorField = TextView.class.getDeclaredField("mEditor");
+ editorField.setAccessible(true);
+ Object editorObject = editorField.get(editText);
+ Class editorClass = Class.forName("android.widget.Editor");
+ Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
+ mInsertionControllerEnabledField.setAccessible(true);
+ mInsertionControllerEnabledField.set(editorObject, false);
+ Field mSelectionControllerEnabledField = editorClass.getDeclaredField("mSelectionControllerEnabled");
+ mSelectionControllerEnabledField.setAccessible(true);
+ mSelectionControllerEnabledField.set(editorObject, false);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/servicing/src/main/java/com/za/signature/view/CircleImageView.java b/servicing/src/main/java/com/za/signature/view/CircleImageView.java
new file mode 100644
index 0000000..82bff6e
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/CircleImageView.java
@@ -0,0 +1,481 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.widget.AppCompatImageView;
+
+import com.za.servicing.R;
+import com.za.signature.util.BitmapUtil;
+
+public class CircleImageView extends AppCompatImageView {
+
+ private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
+
+ private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_4444;
+ private static final int COLORDRAWABLE_DIMENSION = 2;
+
+ private static final int DEFAULT_BORDER_WIDTH = 0;
+ private static final int DEFAULT_BORDER_COLOR = Color.parseColor("#0c53ab");
+ private static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.WHITE;
+ private static final boolean DEFAULT_BORDER_OVERLAY = false;
+
+ private final RectF mDrawableRect = new RectF();
+ private final RectF mBorderRect = new RectF();
+
+ private final Matrix mShaderMatrix = new Matrix();
+ private final Paint mBitmapPaint = new Paint();
+ private final Paint mBorderPaint = new Paint();
+ private final Paint mCircleBackgroundPaint = new Paint();
+
+ private int mBorderColor = DEFAULT_BORDER_COLOR;
+ private int mBorderWidth = DEFAULT_BORDER_WIDTH;
+ private int mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR;
+
+ private Bitmap mBitmap;
+ private BitmapShader mBitmapShader;
+ private int mBitmapWidth;
+ private int mBitmapHeight;
+
+ private float mDrawableRadius;
+ private float mBorderRadius;
+
+ private ColorFilter mColorFilter;
+
+ private boolean mReady;
+ private boolean mSetupPending;
+ private boolean mBorderOverlay;
+ private boolean mDisableCircularTransformation;
+
+ public CircleImageView(Context context) {
+ super(context);
+
+ init();
+ }
+
+ public CircleImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
+
+ mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
+ mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
+ mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
+
+ // Look for deprecated civ_fill_color if civ_circle_background_color is not set
+ if (a.hasValue(R.styleable.CircleImageView_civ_circle_background_color)) {
+ mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color,
+ DEFAULT_CIRCLE_BACKGROUND_COLOR);
+ } else if (a.hasValue(R.styleable.CircleImageView_civ_fill_color)) {
+ mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_fill_color,
+ DEFAULT_CIRCLE_BACKGROUND_COLOR);
+ }
+
+ a.recycle();
+
+ init();
+ }
+
+ private void init() {
+ super.setScaleType(SCALE_TYPE);
+ mReady = true;
+
+ setOutlineProvider(new OutlineProvider());
+
+ if (mSetupPending) {
+ setup();
+ mSetupPending = false;
+ }
+ }
+
+ @Override
+ public ScaleType getScaleType() {
+ return SCALE_TYPE;
+ }
+
+ @Override
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType != SCALE_TYPE) {
+ throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
+ }
+ }
+
+ @Override
+ public void setAdjustViewBounds(boolean adjustViewBounds) {
+ if (adjustViewBounds) {
+ throw new IllegalArgumentException("adjustViewBounds not supported.");
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDisableCircularTransformation) {
+ super.onDraw(canvas);
+ return;
+ }
+
+ if (mBitmap == null) {
+ return;
+ }
+
+ if (mCircleBackgroundColor != Color.TRANSPARENT) {
+ canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);
+ }
+ canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
+ if (mBorderWidth > 0) {
+ canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ setup();
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ super.setPadding(left, top, right, bottom);
+ setup();
+ }
+
+ @Override
+ public void setPaddingRelative(int start, int top, int end, int bottom) {
+ super.setPaddingRelative(start, top, end, bottom);
+ setup();
+ }
+
+ public int getBorderColor() {
+ return mBorderColor;
+ }
+
+ public void setBorderColor(@ColorInt int borderColor) {
+ if (borderColor == mBorderColor) {
+ return;
+ }
+
+ mBorderColor = borderColor;
+ mBorderPaint.setColor(mBorderColor);
+ invalidate();
+ }
+
+ /**
+ * @deprecated Use {@link #setBorderColor(int)} instead
+ */
+ @Deprecated
+ public void setBorderColorResource(@ColorRes int borderColorRes) {
+ setBorderColor(getContext().getResources().getColor(borderColorRes));
+ }
+
+ public int getCircleBackgroundColor() {
+ return mCircleBackgroundColor;
+ }
+
+ public void setCircleBackgroundColor(@ColorInt int circleBackgroundColor) {
+ if (circleBackgroundColor == mCircleBackgroundColor) {
+ return;
+ }
+
+ mCircleBackgroundColor = circleBackgroundColor;
+ mCircleBackgroundPaint.setColor(circleBackgroundColor);
+ invalidate();
+ }
+
+ public void setCircleBackgroundColorResource(@ColorRes int circleBackgroundRes) {
+ setCircleBackgroundColor(getContext().getResources().getColor(circleBackgroundRes));
+ }
+
+ /**
+ * Return the color drawn behind the circle-shaped drawable.
+ *
+ * @return The color drawn behind the drawable
+ * @deprecated Use {@link #getCircleBackgroundColor()} instead.
+ */
+ @Deprecated
+ public int getFillColor() {
+ return getCircleBackgroundColor();
+ }
+
+ /**
+ * Set mergeBitmapRecycle color to be drawn behind the circle-shaped drawable. Note that
+ * this has no effect if the drawable is opaque or no drawable is set.
+ *
+ * @param fillColor The color to be drawn behind the drawable
+ * @deprecated Use {@link #setCircleBackgroundColor(int)} instead.
+ */
+ @Deprecated
+ public void setFillColor(@ColorInt int fillColor) {
+ setCircleBackgroundColor(fillColor);
+ }
+
+ /**
+ * Set mergeBitmapRecycle color to be drawn behind the circle-shaped drawable. Note that
+ * this has no effect if the drawable is opaque or no drawable is set.
+ *
+ * @param fillColorRes The color resource to be resolved to mergeBitmapRecycle color and
+ * drawn behind the drawable
+ * @deprecated Use {@link #setCircleBackgroundColorResource(int)} instead.
+ */
+ @Deprecated
+ public void setFillColorResource(@ColorRes int fillColorRes) {
+ setCircleBackgroundColorResource(fillColorRes);
+ }
+
+ public int getBorderWidth() {
+ return mBorderWidth;
+ }
+
+ public void setBorderWidth(int borderWidth) {
+ if (borderWidth == mBorderWidth) {
+ return;
+ }
+
+ mBorderWidth = borderWidth;
+ setup();
+ }
+
+ public boolean isBorderOverlay() {
+ return mBorderOverlay;
+ }
+
+ public void setBorderOverlay(boolean borderOverlay) {
+ if (borderOverlay == mBorderOverlay) {
+ return;
+ }
+
+ mBorderOverlay = borderOverlay;
+ setup();
+ }
+
+ public boolean isDisableCircularTransformation() {
+ return mDisableCircularTransformation;
+ }
+
+ public void setDisableCircularTransformation(boolean disableCircularTransformation) {
+ if (mDisableCircularTransformation == disableCircularTransformation) {
+ return;
+ }
+
+ mDisableCircularTransformation = disableCircularTransformation;
+ initializeBitmap();
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bm) {
+ super.setImageBitmap(bm);
+ initializeBitmap();
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ super.setImageDrawable(drawable);
+ initializeBitmap();
+ }
+
+ @Override
+ public void setImageResource(@DrawableRes int resId) {
+ super.setImageResource(resId);
+ initializeBitmap();
+ }
+
+ @Override
+ public void setImageURI(Uri uri) {
+ super.setImageURI(uri);
+ initializeBitmap();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ if (cf == mColorFilter) {
+ return;
+ }
+
+ mColorFilter = cf;
+ applyColorFilter();
+ invalidate();
+ }
+
+ @Override
+ public ColorFilter getColorFilter() {
+ return mColorFilter;
+ }
+
+ private void applyColorFilter() {
+ if (mBitmapPaint != null) {
+ mBitmapPaint.setColorFilter(mColorFilter);
+ }
+ }
+
+ private Bitmap getBitmapFromDrawable(Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ try {
+ Bitmap bitmap;
+
+ if (drawable instanceof ColorDrawable) {
+ bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
+ } else {
+ bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private void initializeBitmap() {
+ if (mDisableCircularTransformation) {
+ mBitmap = null;
+ } else {
+ mBitmap = getBitmapFromDrawable(getDrawable());
+ }
+ setup();
+ }
+
+ private void setup() {
+ if (!mReady) {
+ mSetupPending = true;
+ return;
+ }
+
+ if (getWidth() == 0 && getHeight() == 0) {
+ return;
+ }
+
+ if (mBitmap == null) {
+ invalidate();
+ return;
+ }
+
+ mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+
+ mBitmapPaint.setAntiAlias(true);
+ mBitmapPaint.setShader(mBitmapShader);
+
+ mBorderPaint.setStyle(Paint.Style.STROKE);
+ mBorderPaint.setAntiAlias(true);
+ mBorderPaint.setColor(mBorderColor);
+ mBorderPaint.setStrokeWidth(mBorderWidth);
+
+ mCircleBackgroundPaint.setStyle(Paint.Style.FILL);
+ mCircleBackgroundPaint.setAntiAlias(true);
+ mCircleBackgroundPaint.setColor(mCircleBackgroundColor);
+
+ mBitmapHeight = mBitmap.getHeight();
+ mBitmapWidth = mBitmap.getWidth();
+
+ mBorderRect.set(calculateBounds());
+ mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
+
+ mDrawableRect.set(mBorderRect);
+ if (!mBorderOverlay && mBorderWidth > 0) {
+ mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);
+ }
+ mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
+
+ applyColorFilter();
+ updateShaderMatrix();
+ invalidate();
+ }
+
+ private RectF calculateBounds() {
+ int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
+ int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ int sideLength = Math.min(availableWidth, availableHeight);
+
+ float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
+ float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
+
+ return new RectF(left, top, left + sideLength, top + sideLength);
+ }
+
+ private void updateShaderMatrix() {
+ float scale;
+ float dx = 0;
+ float dy = 0;
+
+ mShaderMatrix.set(null);
+
+ if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
+ scale = mDrawableRect.height() / (float) mBitmapHeight;
+ dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
+ } else {
+ scale = mDrawableRect.width() / (float) mBitmapWidth;
+ dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
+ }
+
+ mShaderMatrix.setScale(scale, scale);
+ mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
+
+ mBitmapShader.setLocalMatrix(mShaderMatrix);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return inTouchableArea(event.getX(), event.getY()) && super.onTouchEvent(event);
+ }
+
+ private boolean inTouchableArea(float x, float y) {
+ return Math.pow(x - mBorderRect.centerX(), 2) + Math.pow(y - mBorderRect.centerY(), 2) <= Math.pow(mBorderRadius, 2);
+ }
+
+
+ private class OutlineProvider extends ViewOutlineProvider {
+
+ @Override
+ public void getOutline(View view, Outline outline) {
+ Rect bounds = new Rect();
+ mBorderRect.roundOut(bounds);
+ outline.setRoundRect(bounds, bounds.width() / 2.0f);
+ }
+
+ }
+
+
+ public void setImage(int id, int color) {
+ this.mBorderColor = color;
+ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), id);
+ setImageBitmap(BitmapUtil.changeBitmapColor(bitmap, color));
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/CircleView.java b/servicing/src/main/java/com/za/signature/view/CircleView.java
new file mode 100644
index 0000000..6ffabd6
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/CircleView.java
@@ -0,0 +1,186 @@
+package com.za.signature.view;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.za.servicing.R;
+import com.za.signature.config.PenConfig;
+import com.za.signature.util.DisplayUtil;
+
+
+/**
+ * 自定义圆形View
+ *
+ * @author king
+ * @since 2018-06-01
+ */
+public class CircleView extends View {
+
+ private Paint mPaint;
+ private Paint backPaint;
+ private Paint borderPaint;
+ private Paint outBorderPaint;
+ private int paintColor;
+ private int outBorderColor = Color.parseColor("#0c53ab");
+ private int circleRadius;
+ private int radiusLevel;
+ private boolean showBorder = false;
+ private boolean showOutBorder = false;
+
+ public CircleView(Context context) {
+ this(context, null);
+ }
+
+ public CircleView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
+ paintColor = ta.getColor(R.styleable.CircleView_penColor, PenConfig.PAINT_COLOR);
+ outBorderColor = ta.getColor(R.styleable.CircleView_penColor, Color.parseColor("#0c53ab"));
+ radiusLevel = ta.getInteger(R.styleable.CircleView_sizeLevel, 2);
+ circleRadius = DisplayUtil.dip2px(context, PaintSettingWindow.PEN_SIZES[radiusLevel]);
+ showBorder = ta.getBoolean(R.styleable.CircleView_showBorder, false);
+ showOutBorder = ta.getBoolean(R.styleable.CircleView_showOutBorder, false);
+ ta.recycle();
+ init();
+ }
+
+
+ private void init() {
+
+ borderPaint = new Paint();
+ borderPaint.setColor(paintColor);
+ borderPaint.setStrokeWidth(5);
+ borderPaint.setAntiAlias(true);
+ borderPaint.setStrokeJoin(Paint.Join.ROUND);
+ borderPaint.setStyle(Paint.Style.STROKE);
+
+ outBorderPaint = new Paint();
+ outBorderPaint.setColor(outBorderColor);
+ outBorderPaint.setStrokeWidth(3.5f);
+ outBorderPaint.setAntiAlias(true);
+ outBorderPaint.setStrokeJoin(Paint.Join.ROUND);
+ outBorderPaint.setStyle(Paint.Style.STROKE);
+
+ backPaint = new Paint();
+ backPaint.setColor(Color.WHITE);
+ backPaint.setAntiAlias(true);
+ backPaint.setStrokeJoin(Paint.Join.ROUND);
+ backPaint.setStyle(Paint.Style.FILL);
+
+ mPaint = new Paint();
+ mPaint.setColor(paintColor);
+ mPaint.setStrokeWidth(20);
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, (float) getWidth() / 2, backPaint);
+ //绘制内边框
+ if (showBorder) {
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, circleRadius / 2.5f + 10, borderPaint);
+ }
+ //绘制外边框
+ if (showOutBorder) {
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, (float) getWidth() / 2 - 2f, outBorderPaint);
+ }
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, circleRadius / 2.5f, mPaint);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = onMeasureR(0, widthMeasureSpec);
+ int height = onMeasureR(1, heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * 计算控件宽高
+ */
+ public int onMeasureR(int attr, int oldMeasure) {
+
+ int newSize = 0;
+ int mode = MeasureSpec.getMode(oldMeasure);
+ int oldSize = MeasureSpec.getSize(oldMeasure);
+
+ switch (mode) {
+ case MeasureSpec.EXACTLY:
+ newSize = oldSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.UNSPECIFIED:
+ float value;
+ if (attr == 0) {
+ if (showOutBorder) {
+ value = (circleRadius / 2.5f + 40) * 2;
+ } else {
+ value = (circleRadius / 2.5f + 25) * 2;
+ }
+ newSize = (int) (getPaddingLeft() + value + getPaddingRight());
+ } else if (attr == 1) {
+ if (showOutBorder) {
+ value = (circleRadius / 2.5f + 40) * 2;
+ } else {
+ value = (circleRadius / 2.5f + 25) * 2;
+ }
+// value = (circleRadius / 2.5f + 20) * 2;
+ // 控件的高度 + getPaddingTop() + getPaddingBottom()
+ newSize = (int) (getPaddingTop() + value + getPaddingBottom());
+
+ }
+ break;
+ default:
+ break;
+ }
+
+ return newSize;
+ }
+
+ public void setPaintColor(int paintColor) {
+ this.paintColor = paintColor;
+ mPaint.setColor(paintColor);
+ invalidate();
+ }
+
+ public void setRadiusLevel(int level) {
+ this.radiusLevel = level;
+ this.circleRadius = DisplayUtil.dip2px(getContext(), PaintSettingWindow.PEN_SIZES[level]);
+ invalidate();
+ }
+
+ public void showBorder(boolean showBorder) {
+ this.showBorder = showBorder;
+ invalidate();
+ }
+
+ public void setOutBorderColor(int outBorderColor) {
+ this.outBorderColor = outBorderColor;
+ outBorderPaint.setColor(outBorderColor);
+ invalidate();
+ }
+
+ public int getPaintColor() {
+ return paintColor;
+ }
+
+ public int getRadiusLevel() {
+ return radiusLevel;
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/EraserView.java b/servicing/src/main/java/com/za/signature/view/EraserView.java
new file mode 100644
index 0000000..efb673b
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/EraserView.java
@@ -0,0 +1,69 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.za.signature.util.StatusBarCompat;
+
+/***
+ * 名称:EraserView
+ * 描述:橡皮擦指示器
+ * 最近修改时间:2018年09月13日 16:57分
+ * @since 2018-09-13
+ * @author king
+ */
+public class EraserView extends View {
+
+ private Paint paint;
+ private int statusBarHeight;
+
+ public EraserView(Context context) {
+ this(context, null);
+ }
+
+ public EraserView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EraserView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ setBackgroundDrawable(null);
+ setBackground(null);
+ setDrawingCacheEnabled(false);
+ paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.FILL);
+ if (Build.MODEL.contains("EBEN")) {//E人E本顶部没有状态栏
+ statusBarHeight = 0;
+ } else {
+ statusBarHeight = StatusBarCompat.getStatusBarHeight(context);
+ }
+ }
+
+ @Override
+ public void setX(float x) {
+ super.setX(x - (float) getWidth() / 2);
+ }
+
+ @Override
+ public void setY(float y) {
+ super.setY(y - (float) getHeight() / 2 - statusBarHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawColor(Color.TRANSPARENT);
+ paint.setColor(Color.LTGRAY);
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, (float) getWidth() / 2, paint);
+ paint.setColor(Color.WHITE);
+ canvas.drawCircle((float) getWidth() / 2, (float) getHeight() / 2, (float) getWidth() / 2 - 8, paint);
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/GridDrawable.java b/servicing/src/main/java/com/za/signature/view/GridDrawable.java
new file mode 100644
index 0000000..05ce790
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/GridDrawable.java
@@ -0,0 +1,121 @@
+package com.za.signature.view;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/***
+ * 名称:GridDrawable
+ * 描述:自定义米格背景视图
+ * 最近修改时间:
+ * @since 2017/11/16
+ * @author king
+ */
+
+public class GridDrawable extends Drawable {
+
+ private Paint mPaint;
+ private Paint mDashPaint;
+ private Paint mLinePaint;
+
+ private Bitmap mBitmap;
+ private Canvas mCanvas;
+
+ private Path linePath;
+ private Path dashPath;
+
+ private final int mWidth;
+ private final int mHeight;
+ private final int backgroundColor;
+
+ public GridDrawable(int width, int height, int backgroundColor) {
+ this.mWidth = width;
+ this.mHeight = height;
+ this.backgroundColor = backgroundColor;
+ init();
+ }
+
+ public void init() {
+ //边框画笔
+ mPaint = new Paint();
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeWidth(10.0f);
+ mPaint.setColor(Color.parseColor("#c4c4c4"));
+
+ //虚线画笔
+ mDashPaint = new Paint();
+ mDashPaint.setStyle(Paint.Style.STROKE);
+ mDashPaint.setAntiAlias(true);
+ mDashPaint.setStrokeWidth(5.0f);
+ mDashPaint.setColor(Color.parseColor("#c4c4c4"));
+ mDashPaint.setPathEffect(new DashPathEffect(new float[]{50, 40}, 0));
+
+ mLinePaint = new Paint();
+ mLinePaint.setStyle(Paint.Style.STROKE);
+ mLinePaint.setAntiAlias(true);
+ mLinePaint.setStrokeWidth(5.0f);
+ mLinePaint.setColor(Color.parseColor("#c4c4c4"));
+
+ linePath = new Path();
+ dashPath = new Path();
+
+ mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_4444);
+ mCanvas = new Canvas(mBitmap);
+ mCanvas.drawColor(backgroundColor);
+ doDraw();
+ }
+
+ private void doDraw() {
+ Rect rect = new Rect(0, 0, mWidth, mHeight);
+ mCanvas.drawRect(rect, mPaint);
+
+ linePath.moveTo(0, (float) mHeight / 2);
+ linePath.lineTo(mWidth, (float) mHeight / 2);
+ mCanvas.drawPath(linePath, mLinePaint);
+
+ linePath.moveTo((float) mWidth / 2, 0);
+ linePath.lineTo((float) mWidth / 2, mHeight);
+ mCanvas.drawPath(linePath, mLinePaint);
+
+ dashPath.moveTo(0, 0);
+ dashPath.lineTo(mWidth, mHeight);
+ mCanvas.drawPath(dashPath, mDashPaint);
+
+ dashPath.moveTo(mWidth, 0);
+ dashPath.lineTo(0, mHeight);
+ mCanvas.drawPath(dashPath, mDashPaint);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ }
+
+ @Override
+ public void setAlpha(int i) {
+ mPaint.setAlpha(i);
+ mDashPaint.setAlpha(i);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ mDashPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/GridPaintView.java b/servicing/src/main/java/com/za/signature/view/GridPaintView.java
new file mode 100644
index 0000000..bb07621
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/GridPaintView.java
@@ -0,0 +1,221 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.za.servicing.R;
+import com.za.signature.config.PenConfig;
+import com.za.signature.pen.BasePen;
+import com.za.signature.pen.SteelPen;
+import com.za.signature.util.BitmapUtil;
+import com.za.signature.util.DisplayUtil;
+
+/**
+ * 田字格手写画板
+ *
+ * @author king
+ * @since 2018/5/4
+ */
+public class GridPaintView extends View {
+ private Paint mPaint;
+ private Canvas mCanvas;
+ private Bitmap mBitmap;
+ private BasePen mStokeBrushPen;
+
+ /**
+ * 是否有绘制
+ */
+ private boolean hasDraw;
+
+ /**
+ * 画布宽度
+ */
+ private final int mWidth;
+ /**
+ * 画布高度
+ */
+ private final int mHeight;
+
+ public GridPaintView(Context context) {
+ this(context, null);
+ }
+
+ public GridPaintView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GridPaintView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mWidth = (int) getResources().getDimension(R.dimen.sign_grid_size);
+ mHeight = (int) getResources().getDimension(R.dimen.sign_grid_size);
+ initParameter();
+ }
+
+ private void initParameter() {
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
+ mStokeBrushPen = new SteelPen();
+
+ initPaint();
+ initCanvas();
+ }
+
+
+ private void initPaint() {
+ mPaint = new Paint();
+ mPaint.setColor(PenConfig.PAINT_COLOR);
+ mPaint.setStrokeWidth(DisplayUtil.dip2px(getContext(), PaintSettingWindow.PEN_SIZES[PenConfig.PAINT_SIZE_LEVEL]));
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setAlpha(0xFF);
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeMiter(1.0f);
+ mStokeBrushPen.setPaint(mPaint);
+ }
+
+ private void initCanvas() {
+ mCanvas = new Canvas(mBitmap);
+ //设置画布的颜色的问题
+ mCanvas.drawColor(Color.TRANSPARENT);
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ mStokeBrushPen.draw(canvas);
+ super.onDraw(canvas);
+ }
+
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ mStokeBrushPen.onTouchEvent(event, mCanvas);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mGetWriteListener != null) {
+ mGetWriteListener.onWriteStart();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ hasDraw = true;
+ if (mGetWriteListener != null) {
+ mGetWriteListener.onWriteStart();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mGetWriteListener != null) {
+ mGetWriteListener.onWriteCompleted(System.currentTimeMillis());
+ }
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ /**
+ * @return 判断是否有绘制内容在画布上
+ */
+ public boolean isEmpty() {
+ return !hasDraw;
+ }
+
+
+ /**
+ * 清除画布
+ */
+ public void reset() {
+ mBitmap.eraseColor(Color.TRANSPARENT);
+ hasDraw = false;
+ mStokeBrushPen.clear();
+ invalidate();
+ }
+
+
+ public void release() {
+ destroyDrawingCache();
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ }
+
+
+ public WriteListener mGetWriteListener;
+
+ public void setGetTimeListener(WriteListener l) {
+ mGetWriteListener = l;
+ }
+
+
+ public interface WriteListener {
+ /**
+ * 开始书写
+ */
+ void onWriteStart();
+
+ /**
+ * 书写完毕
+ *
+ * @param time 当前时间
+ */
+ void onWriteCompleted(long time);
+
+ }
+
+
+ /**
+ * 设置画笔大小
+ *
+ * @param width 大小
+ */
+ public void setPaintWidth(int width) {
+ if (mPaint != null) {
+ mPaint.setStrokeWidth(DisplayUtil.dip2px(getContext(), width));
+ mStokeBrushPen.setPaint(mPaint);
+ invalidate();
+ }
+ }
+
+
+ /**
+ * 设置画笔颜色
+ *
+ * @param color 颜色
+ */
+ public void setPaintColor(int color) {
+ if (mPaint != null) {
+ mPaint.setColor(color);
+ mStokeBrushPen.setPaint(mPaint);
+ invalidate();
+ }
+ }
+
+
+ /**
+ * 构建Bitmap
+ *
+ * @return 所绘制的bitmap
+ */
+ public Bitmap buildBitmap(boolean clearBlank, int zoomSize) {
+ if (!hasDraw) {
+ return null;
+ }
+ Bitmap result = BitmapUtil.zoomImg(mBitmap, zoomSize);
+ if (clearBlank) {
+ result = BitmapUtil.clearLRBlank(result, 10, Color.TRANSPARENT);
+ }
+ return result;
+ }
+
+}
diff --git a/servicing/src/main/java/com/za/signature/view/HVScrollView.java b/servicing/src/main/java/com/za/signature/view/HVScrollView.java
new file mode 100644
index 0000000..f31b5ad
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/HVScrollView.java
@@ -0,0 +1,2739 @@
+
+package com.za.signature.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AnimationUtils;
+import android.widget.ScrollView;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.InputDeviceCompat;
+import androidx.core.view.MotionEventCompat;
+import androidx.core.view.NestedScrollingChild;
+import androidx.core.view.NestedScrollingChildHelper;
+import androidx.core.view.NestedScrollingParent;
+import androidx.core.view.NestedScrollingParentHelper;
+import androidx.core.view.ScrollingView;
+import androidx.core.view.VelocityTrackerCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityEventCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.view.accessibility.AccessibilityRecordCompat;
+import androidx.core.widget.EdgeEffectCompat;
+import androidx.core.widget.ScrollerCompat;
+
+import com.za.servicing.R;
+
+import java.util.List;
+
+
+/**
+ * 作者:LuckyJayce
+ * HVScrollView is just like {@link ScrollView}, but it supports acting
+ * as both a nested scrolling parent and child on both new and old versions of Android.
+ * can scroll horizontal and vertical
+ * 能够水平和垂直滚动的 ScrollView,
+ * 默认同时可以水平和垂直滚动
+ * 当canScrollH 设置为false的时候就是一个垂直的的ScrollView
+ * 当canScrollV 设置为false的时候就是一个水平的的ScrollView
+ * 代码修改于v4的25.0.0版本的NestedScrollView,参考了RecyclerView的双向滚动的事件,参考了FrameLayoutd的onMeasure和ScrollView的HorizontalScrollView的onMeasure
+ * 支持滑动末尾,ViewPager的页面切换
+ */
+public class HVScrollView extends ViewGroup implements NestedScrollingParent,
+ NestedScrollingChild, ScrollingView {
+ private boolean DEBUG = false;
+ static final int ANIMATED_SCROLL_GAP = 250;
+
+ static final float MAX_SCROLL_FACTOR = 0.5f;
+
+ private static final String TAG = "NestedScrollView";
+ private boolean mChildLayoutCenter;
+ private int mInitialTouchY;
+ private int mInitialTouchX;
+
+ public void setChildLayoutCenter(boolean childLayoutCenter) {
+ if (mChildLayoutCenter != childLayoutCenter) {
+ this.mChildLayoutCenter = childLayoutCenter;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the scroll
+ * X or Y positions of a view change.
+ *
+ * This version of the interface works on all versions of Android, back to API v4.
+ *
+ * @see #setOnScrollChangeListener(OnScrollChangeListener)
+ */
+ public interface OnScrollChangeListener {
+ /**
+ * Called when the scroll position of a view changes.
+ *
+ * @param v The view whose scroll position has changed.
+ * @param scrollX Current horizontal scroll origin.
+ * @param scrollY Current vertical scroll origin.
+ * @param oldScrollX Previous horizontal scroll origin.
+ * @param oldScrollY Previous vertical scroll origin.
+ */
+ void onScrollChange(HVScrollView v, int scrollX, int scrollY,
+ int oldScrollX, int oldScrollY);
+ }
+
+ private long mLastScroll;
+
+ private final Rect mTempRect = new Rect();
+ private ScrollerCompat mScroller;
+ private EdgeEffectCompat mEdgeGlowTop;
+ private EdgeEffectCompat mEdgeGlowBottom;
+ private EdgeEffectCompat mEdgeGlowLeft;
+ private EdgeEffectCompat mEdgeGlowRight;
+
+ /**
+ * Position of the last motion event.
+ */
+ private int mLastMotionY;
+
+ private int mLastMotionX;
+
+ /**
+ * True when the layout has changed but the traversal has not come through yet.
+ * Ideally the view hierarchy would keep track of this for us.
+ */
+ private boolean mIsLayoutDirty = true;
+ private boolean mIsLaidOut = false;
+
+ /**
+ * The child to give focus to in the event that a child has requested focus while the
+ * layout is dirty. This prevents the scroll from being wrong if the child has not been
+ * laid out before requesting focus.
+ */
+ private View mChildToScrollTo = null;
+
+// /**
+// * True if the user is currently dragging this ScrollView around. This is
+// * not the same as 'is being flinged', which can be checked by
+// * mScroller.isFinished() (flinging begins when the user lifts his finger).
+// */
+// private boolean mIsBeingDragged = false;
+
+ /**
+ * The RecyclerView is not currently scrolling.
+ */
+ public static final int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * The RecyclerView is currently being dragged by outside input such as user touch input.
+ */
+ public static final int SCROLL_STATE_DRAGGING = 1;
+
+ /**
+ * The RecyclerView is currently animating to a final position while not under
+ * outside control.
+ */
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ // Touch/scrolling handling
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ private boolean mSmoothScrollingEnabled = true;
+
+ private int mTouchSlop;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+
+ /**
+ * Used during scrolling to retrieve the new offset within the window.
+ */
+ private final int[] mScrollOffset = new int[2];
+ private final int[] mScrollConsumed = new int[2];
+ private int mNestedYOffset;
+ private int mNestedXOffset;
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ private SavedState mSavedState;
+
+ private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate();
+
+ private boolean mFillViewportH;
+ private boolean mFillViewportV;
+ private final NestedScrollingParentHelper mParentHelper;
+ private final NestedScrollingChildHelper mChildHelper;
+
+ private float mVerticalScrollFactor;
+
+ private OnScrollChangeListener mOnScrollChangeListener;
+
+ public HVScrollView(Context context) {
+ this(context, null);
+ }
+
+ public HVScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public static final int SCROLL_ORIENTATION_NONE = 0;
+ public static final int SCROLL_ORIENTATION_HORIZONTAL = 1;
+ public static final int SCROLL_ORIENTATION_VERTICAL = 2;
+ public static final int SCROLL_ORIENTATION_BOTH = 3;
+ private int mScrollOrientation;
+
+ public void setScrollOrientation(int scrollOrientation) {
+ this.mScrollOrientation = scrollOrientation;
+ requestLayout();
+ }
+
+ public int getScrollOrientation() {
+ return mScrollOrientation;
+ }
+
+ public HVScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initScrollView();
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.HVScrollView, defStyleAttr, 0);
+
+ mChildLayoutCenter = a.getBoolean(R.styleable.HVScrollView_childLayoutCenter, false);
+ mFillViewportH = a.getBoolean(R.styleable.HVScrollView_fillViewportH, false);
+ mFillViewportV = a.getBoolean(R.styleable.HVScrollView_fillViewportV, false);
+ mScrollOrientation = a.getInt(R.styleable.HVScrollView_scrollOrientation, SCROLL_ORIENTATION_BOTH);
+
+ a.recycle();
+
+ mParentHelper = new NestedScrollingParentHelper(this);
+ mChildHelper = new NestedScrollingChildHelper(this);
+
+ // ...because why else would you be using this widget?
+ setNestedScrollingEnabled(true);
+
+ ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE);
+ }
+
+ // NestedScrollingChild
+
+ @Override
+ public void setNestedScrollingEnabled(boolean enabled) {
+ mChildHelper.setNestedScrollingEnabled(enabled);
+ }
+
+ @Override
+ public boolean isNestedScrollingEnabled() {
+ return mChildHelper.isNestedScrollingEnabled();
+ }
+
+ @Override
+ public boolean startNestedScroll(int axes) {
+ return mChildHelper.startNestedScroll(axes);
+ }
+
+ @Override
+ public void stopNestedScroll() {
+ mChildHelper.stopNestedScroll();
+ }
+
+ @Override
+ public boolean hasNestedScrollingParent() {
+ return mChildHelper.hasNestedScrollingParent();
+ }
+
+ @Override
+ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed, int[] offsetInWindow) {
+ return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ offsetInWindow);
+ }
+
+ @Override
+ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
+ return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
+ }
+
+ @Override
+ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+ return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
+ }
+
+ @Override
+ public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+ return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
+ }
+
+ // NestedScrollingParent
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
+ return ((nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScrollVertically()) || ((nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0 && canScrollHorizontally());
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
+ mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
+ int axes = ViewCompat.SCROLL_AXIS_NONE;
+ axes = canScrollHorizontally() ? axes | ViewCompat.SCROLL_AXIS_HORIZONTAL : axes;
+ axes = canScrollVertically() ? axes | ViewCompat.SCROLL_AXIS_VERTICAL : axes;
+ startNestedScroll(axes);
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull View target) {
+ mParentHelper.onStopNestedScroll(target);
+ stopNestedScroll();
+ }
+
+ public boolean canScrollHorizontally() {
+ return (mScrollOrientation & SCROLL_ORIENTATION_HORIZONTAL) == SCROLL_ORIENTATION_HORIZONTAL;
+ }
+
+ public boolean canScrollVertically() {
+ return (mScrollOrientation & SCROLL_ORIENTATION_VERTICAL) == SCROLL_ORIENTATION_VERTICAL;
+ }
+
+
+ @Override
+ public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed) {
+ final int oldScrollY = getScrollY();
+ final int oldScrollX = getScrollX();
+ scrollBy(canScrollHorizontally() ? dxUnconsumed : 0, canScrollVertically() ? dyUnconsumed : 0);
+ final int myConsumedY = getScrollY() - oldScrollY;
+ final int myConsumedX = getScrollX() - oldScrollX;
+ final int myUnconsumedY = dyUnconsumed - myConsumedY;
+ final int myUnconsumedX = dxUnconsumed - myConsumedX;
+ dispatchNestedScroll(myConsumedX, myConsumedY, myUnconsumedX, myUnconsumedY, null);
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
+ dispatchNestedPreScroll(dx, dy, consumed, null);
+ }
+
+ @Override
+ public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
+ if (!consumed) {
+ fling((int) velocityX, (int) velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
+ return dispatchNestedPreFling(velocityX, velocityY);
+ }
+
+ @Override
+ public int getNestedScrollAxes() {
+ return mParentHelper.getNestedScrollAxes();
+ }
+
+ // ScrollView import
+
+ @Override
+ protected float getLeftFadingEdgeStrength() {
+ if (getChildCount() == 0 || !canScrollHorizontally()) {
+ return 0.0f;
+ }
+
+ final int length = getHorizontalFadingEdgeLength();
+ final int scrollX = getScrollX();
+ if (scrollX < length) {
+ return scrollX / (float) length;
+ }
+ return 1.0f;
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ if (getChildCount() == 0 || !canScrollVertically()) {
+ return 0.0f;
+ }
+
+ final int length = getVerticalFadingEdgeLength();
+ final int scrollY = getScrollY();
+ if (scrollY < length) {
+ return scrollY / (float) length;
+ }
+ return 1.0f;
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ if (getChildCount() == 0 || !canScrollVertically()) {
+ return 0.0f;
+ }
+
+ final int length = getVerticalFadingEdgeLength();
+ final int bottomEdge = getHeight() - getPaddingBottom();
+ final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
+ if (span < length) {
+ return span / (float) length;
+ }
+
+ return 1.0f;
+ }
+
+ @Override
+ protected float getRightFadingEdgeStrength() {
+ if (getChildCount() == 0 || !canScrollHorizontally()) {
+ return 0.0f;
+ }
+
+ final int length = getHorizontalFadingEdgeLength();
+ final int rightEdge = getWidth() - getPaddingRight();
+ final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
+ if (span < length) {
+ return span / (float) length;
+ }
+
+ return 1.0f;
+ }
+
+ /**
+ * @return The maximum amount this scroll view will scroll in response to
+ * an arrow event.
+ */
+ public int getMaxScrollAmountY() {
+ return (int) (MAX_SCROLL_FACTOR * getHeight());
+ }
+
+ public int getMaxScrollAmountX() {
+ return (int) (MAX_SCROLL_FACTOR * getWidth());
+ }
+
+ private void initScrollView() {
+ mScroller = ScrollerCompat.create(getContext(), null);
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setWillNotDraw(false);
+ setOverScrollMode(ScrollView.OVER_SCROLL_NEVER);
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ }
+
+ @Override
+ public void addView(View child) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child);
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index);
+ }
+
+ @Override
+ public void addView(View child, LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, params);
+ }
+
+ @Override
+ public void addView(View child, int index, LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Register a callback to be invoked when the scroll X or Y positions of
+ * this view change.
+ * This version of the method works on all versions of Android, back to API v4.
+ *
+ * @param l The listener to notify when the scroll X or Y position changes.
+ * @see View#getScrollX()
+ * @see View#getScrollY()
+ */
+ public void setOnScrollChangeListener(OnScrollChangeListener l) {
+ mOnScrollChangeListener = l;
+ }
+
+ /**
+ * @return Returns true this ScrollView can be scrolled
+ */
+ private boolean canScroll() {
+ View child = getChildAt(0);
+ if (child != null) {
+ int childHeight = child.getHeight();
+ int childWidth = child.getWidth();
+ boolean canY = getHeight() < childHeight + getPaddingTop() + getPaddingBottom();
+ canY |= canScrollVertically();
+ boolean canX = getWidth() < childWidth + getPaddingLeft() + getPaddingRight();
+ canX |= canScrollHorizontally();
+ return canX || canY;
+ }
+ return false;
+ }
+
+ /**
+ * @return Whether arrow scrolling will animate its transition.
+ */
+ public boolean isSmoothScrollingEnabled() {
+ return mSmoothScrollingEnabled;
+ }
+
+ /**
+ * Set whether arrow scrolling will animate its transition.
+ *
+ * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+ */
+ public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+ mSmoothScrollingEnabled = smoothScrollingEnabled;
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+
+ if (mOnScrollChangeListener != null) {
+ mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+ }
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ public boolean executeKeyEvent(KeyEvent event) {
+ mTempRect.setEmpty();
+
+ if (!canScroll()) {
+ if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+ currentFocused, View.FOCUS_DOWN);
+ return nextFocused != null
+ && nextFocused != this
+ && nextFocused.requestFocus(View.FOCUS_DOWN);
+ }
+ return false;
+ }
+
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (canScrollVertically()) {
+ if (!event.isAltPressed()) {
+ handled = arrowScrollVertically(View.FOCUS_UP);
+ } else {
+ handled = fullScrollVertically(View.FOCUS_UP);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (canScrollVertically()) {
+ if (!event.isAltPressed()) {
+ handled = arrowScrollVertically(View.FOCUS_DOWN);
+ } else {
+ handled = fullScrollVertically(View.FOCUS_DOWN);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (canScrollHorizontally()) {
+ if (!event.isAltPressed()) {
+ handled = arrowScrollHorizontally(View.FOCUS_LEFT);
+ } else {
+ handled = fullScrollHorizontally(View.FOCUS_LEFT);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (canScrollHorizontally()) {
+ if (!event.isAltPressed()) {
+ handled = arrowScrollHorizontally(View.FOCUS_DOWN);
+ } else {
+ handled = fullScrollHorizontally(View.FOCUS_DOWN);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ if (canScrollHorizontally()) {
+ pageScrollHorizontally(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+ } else if (canScrollVertically()) {
+ pageScrollVertically(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+ }
+ break;
+ }
+ }
+
+ return handled;
+ }
+
+ private boolean inChild(int x, int y) {
+ if (getChildCount() > 0) {
+ final int scrollY = getScrollY();
+ final View child = getChildAt(0);
+ return !(y < child.getTop() - scrollY
+ || y >= child.getBottom() - scrollY
+ || x < child.getLeft()
+ || x >= child.getRight());
+ }
+ return false;
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ private void setScrollState(int state) {
+ if (state == mScrollState) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
+ new Exception());
+ }
+ mScrollState = state;
+ }
+
+ private void cancelTouch() {
+ resetTouch();
+ setScrollState(SCROLL_STATE_IDLE);
+ }
+
+ private void resetTouch() {
+ recycleVelocityTracker();
+ stopNestedScroll();
+ releaseGlows();
+ }
+
+ private void releaseGlows() {
+ boolean needsInvalidate = false;
+ if (mEdgeGlowLeft != null) needsInvalidate = mEdgeGlowLeft.onRelease();
+ if (mEdgeGlowTop != null) needsInvalidate |= mEdgeGlowTop.onRelease();
+ if (mEdgeGlowRight != null) needsInvalidate |= mEdgeGlowRight.onRelease();
+ if (mEdgeGlowBottom != null) needsInvalidate |= mEdgeGlowBottom.onRelease();
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ /*
+ * Shortcut the most recurring case: the user is in the dragging
+ * state and he is moving his finger. We want to intercept this
+ * motion.
+ */
+
+
+ final int action = ev.getAction();
+// if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+// return true;
+// }
+
+ if (action == MotionEvent.ACTION_MOVE && mScrollState == SCROLL_STATE_DRAGGING) {
+ return true;
+ }
+
+ setScrollState(SCROLL_STATE_IDLE);
+
+ final boolean canScrollHorizontally = canScrollHorizontally();
+ final boolean canScrollVertically = canScrollVertically();
+
+ switch (action & MotionEventCompat.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: {
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+ * whether the user has moved far enough from his original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value
+ * of the down event.
+ */
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
+ if (pointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + activePointerId
+ + " in onInterceptTouchEvent");
+ break;
+ }
+
+ final int y = (int) MotionEventCompat.getY(ev, pointerIndex);
+ final int x = (int) MotionEventCompat.getX(ev, pointerIndex);
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ final int dx = x - mInitialTouchX;
+ final int dy = y - mInitialTouchY;
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ mLastMotionX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ mLastMotionY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ mNestedYOffset = 0;
+ if (startScroll) {
+// mIsBeingDragged = true;
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ final int y = (int) ev.getY();
+ final int x = (int) ev.getX();
+ if (!inChild(x, y)) {
+ setScrollState(SCROLL_STATE_IDLE);
+ recycleVelocityTracker();
+ break;
+ }
+
+
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionY = y;
+ mLastMotionX = x;
+ mInitialTouchY = mLastMotionY;
+ mInitialTouchX = mLastMotionX;
+
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ /*
+ * If being flinged and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when
+ * being flinged. We need to call computeScrollOffset() first so that
+ * isFinished() is correct.
+ */
+ mScroller.computeScrollOffset();
+// mIsBeingDragged = !mScroller.isFinished();
+
+ if (mScrollState == SCROLL_STATE_SETTLING) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
+ if (canScrollHorizontally) {
+ nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
+ }
+ if (canScrollVertically) {
+ nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
+ }
+ startNestedScroll(nestedScrollAxis);
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+// mIsBeingDragged = false;
+ cancelTouch();
+ break;
+ case MotionEvent.ACTION_UP:
+ /* Release the drag */
+// mIsBeingDragged = false;
+// mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+// if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) {
+// ViewCompat.postInvalidateOnAnimation(this);
+// }
+ stopNestedScroll();
+ break;
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+
+ initVelocityTrackerIfNotExists();
+
+ MotionEvent vtev = MotionEvent.obtain(ev);
+
+ final boolean canScrollHorizontally = canScrollHorizontally();
+ final boolean canScrollVertically = canScrollVertically();
+
+ final int actionMasked = MotionEventCompat.getActionMasked(ev);
+
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ mNestedYOffset = 0;
+ mNestedXOffset = 0;
+ }
+ vtev.offsetLocation(mNestedXOffset, mNestedYOffset);
+
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN: {
+
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ // Remember where the motion event started
+ mInitialTouchX = mLastMotionY = (int) ev.getY();
+ mInitialTouchY = mLastMotionX = (int) ev.getX();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+
+ int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
+ if (canScrollHorizontally) {
+ nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
+ }
+ if (canScrollVertically) {
+ nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
+ }
+ startNestedScroll(nestedScrollAxis);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
+ mActivePointerId);
+ if (activePointerIndex == -1) {
+ Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+ return false;
+ }
+
+ final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
+ final int x = (int) MotionEventCompat.getX(ev, activePointerIndex);
+ int dy = mLastMotionY - y;
+ int dx = mLastMotionX - x;
+ if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
+ dy -= mScrollConsumed[1];
+ dx -= mScrollConsumed[0];
+ vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ mNestedXOffset += mScrollOffset[0];
+ }
+
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ if (dx > 0) {
+ dx -= mTouchSlop;
+ } else {
+ dx += mTouchSlop;
+ }
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ if (dy > 0) {
+ dy -= mTouchSlop;
+ } else {
+ dy += mTouchSlop;
+ }
+ startScroll = true;
+ }
+ if (startScroll) {
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+
+ if (mScrollState == SCROLL_STATE_DRAGGING) {
+ // Scroll to follow the motion event
+ mLastMotionY = y - mScrollOffset[1];
+ mLastMotionX = x - mScrollOffset[0];
+
+ final int oldY = getScrollY();
+ final int oldX = getScrollX();
+ final int rangeY = getScrollRangeY();
+ final int rangeX = getScrollRangeX();
+ final int overscrollMode = ViewCompat.getOverScrollMode(this);
+ boolean canOverscrollY = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ rangeY > 0);
+ boolean canOverscrollX = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ rangeX > 0);
+ boolean canOverscroll = (canOverscrollY && canScrollVertically()) || (canOverscrollX && canScrollHorizontally());
+
+// final ViewParent parent = getParent();
+// if (parent != null) {
+// parent.requestDisallowInterceptTouchEvent(true);
+// }
+
+ // Calling overScrollByCompat will call onOverScrolled, which
+ // calls onScrollChanged if applicable.
+ if (overScrollByCompat(dx, dy, getScrollX(), getScrollY(), rangeX, rangeY, 0, 0, true)) {
+ // Break our velocity if we hit a scroll barrier.
+// mVelocityTracker.clear();
+ if (mChildHelper.hasNestedScrollingParent()) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ final int scrolledDeltaY = getScrollY() - oldY;
+ final int scrolledDeltaX = getScrollX() - oldX;
+ final int unconsumedY = dy - scrolledDeltaY;
+ final int unconsumedX = dx - scrolledDeltaX;
+ if (dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset)) {
+ mLastMotionY -= mScrollOffset[1];
+ mLastMotionX -= mScrollOffset[0];
+ vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+ mNestedYOffset += mScrollOffset[1];
+ mNestedXOffset += mScrollOffset[0];
+ } else if (canOverscroll) {
+ ensureGlows();
+ final int pulledToY = oldY + dy;
+ final int pulledToX = oldX + dx;
+ if (canScrollVertically()) {
+ if (pulledToY < 0) {
+ mEdgeGlowTop.onPull((float) dy / getHeight(), MotionEventCompat.getX(ev, activePointerIndex) / getWidth());
+ if (!mEdgeGlowBottom.isFinished()) {
+ mEdgeGlowBottom.onRelease();
+ }
+ } else if (pulledToY > rangeY) {
+ mEdgeGlowBottom.onPull((float) dy / getHeight(), 1.f - MotionEventCompat.getX(ev, activePointerIndex) / getWidth());
+ if (!mEdgeGlowTop.isFinished()) {
+ mEdgeGlowTop.onRelease();
+ }
+ }
+ }
+ if (canScrollHorizontally()) {
+ if (pulledToX < 0) {
+ mEdgeGlowLeft.onPull((float) dx / getWidth(), MotionEventCompat.getX(ev, activePointerIndex) / getHeight());
+ if (!mEdgeGlowLeft.isFinished()) {
+ mEdgeGlowLeft.onRelease();
+ }
+ } else if (pulledToX > rangeX) {
+ mEdgeGlowRight.onPull((float) dx / getWidth(), 1.f - MotionEventCompat.getX(ev, activePointerIndex) / getHeight());
+ if (!mEdgeGlowRight.isFinished()) {
+ mEdgeGlowRight.onRelease();
+ }
+ }
+ }
+ if (mEdgeGlowTop != null && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() || !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+
+ final float xvel = canScrollHorizontally ?
+ -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId) : 0;
+ final float yvel = canScrollVertically ?
+ -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId) : 0;
+
+ if (fling((int) xvel, (int) yvel)) {
+ setScrollState(SCROLL_STATE_IDLE);
+ } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ mActivePointerId = INVALID_POINTER;
+ resetTouch();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mScrollState == SCROLL_STATE_DRAGGING) {
+ if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ mActivePointerId = INVALID_POINTER;
+ cancelTouch();
+ break;
+ case MotionEventCompat.ACTION_POINTER_DOWN: {
+ final int index = MotionEventCompat.getActionIndex(ev);
+ mInitialTouchX = mLastMotionY = (int) MotionEventCompat.getY(ev, index);
+ mInitialTouchX = mLastMotionX = (int) MotionEventCompat.getX(ev, index);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+ break;
+ }
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(vtev);
+ }
+ vtev.recycle();
+ return true;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >>
+ MotionEventCompat.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mInitialTouchX = mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex);
+ mInitialTouchY = mLastMotionX = (int) MotionEventCompat.getX(ev, newPointerIndex);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEventCompat.ACTION_SCROLL: {
+ final float vscroll = MotionEventCompat.getAxisValue(event, MotionEventCompat.AXIS_VSCROLL);
+ final float hscroll = MotionEventCompat.getAxisValue(event, MotionEventCompat.AXIS_HSCROLL);
+ if (vscroll != 0) {
+ int newScrollY = getScrollY(), oldScrollY = newScrollY;
+ int newScrollX = getScrollX(), oldScrollX = newScrollX;
+ if (canScrollHorizontally()) {
+ final int rangeX = getScrollRangeX();
+ final int deltaX = (int) (hscroll * getHorizontalScrollFactorCompat());
+ newScrollX = oldScrollX - deltaX;
+ if (newScrollX < 0) {
+ newScrollX = 0;
+ } else if (newScrollX > rangeX) {
+ newScrollX = rangeX;
+ }
+ }
+ if (canScrollVertically()) {
+ final int deltaY = (int) (vscroll * getVerticalScrollFactorCompat());
+ final int rangeY = getScrollRangeY();
+ newScrollY = oldScrollY - deltaY;
+ if (newScrollY < 0) {
+ newScrollY = 0;
+ } else if (newScrollY > rangeY) {
+ newScrollY = rangeY;
+ }
+ }
+ if (newScrollY != oldScrollY || newScrollX != oldScrollX) {
+ super.scrollTo(newScrollX, newScrollY);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private float getVerticalScrollFactorCompat() {
+ if (mVerticalScrollFactor == 0) {
+ TypedValue outValue = new TypedValue();
+ final Context context = getContext();
+ if (!context.getTheme().resolveAttribute(
+ android.R.attr.listPreferredItemHeight, outValue, true)) {
+ throw new IllegalStateException(
+ "Expected theme to define listPreferredItemHeight.");
+ }
+ mVerticalScrollFactor = outValue.getDimension(
+ context.getResources().getDisplayMetrics());
+ }
+ return mVerticalScrollFactor;
+ }
+
+ private float getHorizontalScrollFactorCompat() {
+ if (mVerticalScrollFactor == 0) {
+ TypedValue outValue = new TypedValue();
+ final Context context = getContext();
+ if (!context.getTheme().resolveAttribute(
+ android.R.attr.listPreferredItemHeight, outValue, true)) {
+ throw new IllegalStateException(
+ "Expected theme to define listPreferredItemHeight.");
+ }
+ mVerticalScrollFactor = outValue.getDimension(
+ context.getResources().getDisplayMetrics());
+ }
+ return mVerticalScrollFactor;
+ }
+
+ protected void onOverScrolled(int scrollX, int scrollY,
+ boolean clampedX, boolean clampedY) {
+ super.scrollTo(scrollX, scrollY);
+ }
+
+ boolean overScrollByCompat(int deltaX, int deltaY,
+ int scrollX, int scrollY,
+ int scrollRangeX, int scrollRangeY,
+ int maxOverScrollX, int maxOverScrollY,
+ boolean isTouchEvent) {
+ int oldScrollX = getScrollX();
+ int oldScrollY = getScrollY();
+
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+ final boolean canScrollHorizontal =
+ computeHorizontalScrollRange() > computeHorizontalScrollExtent() && canScrollHorizontally();
+ final boolean canScrollVertical =
+ computeVerticalScrollRange() > computeVerticalScrollExtent() && canScrollVertically();
+ final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
+ final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
+
+ int newScrollX = scrollX;
+ if (canScrollHorizontally()) {
+ newScrollX += deltaX;
+ }
+
+ int newScrollY = scrollY;
+ if (canScrollVertically()) {
+ newScrollY += deltaY;
+ }
+
+ // Clamp values if at the limits and record
+ final int left = -maxOverScrollX;
+ final int right = maxOverScrollX + scrollRangeX;
+ final int top = -maxOverScrollY;
+ final int bottom = maxOverScrollY + scrollRangeY;
+
+ boolean clampedX = false;
+ if (newScrollX > right) {
+ newScrollX = right;
+ clampedX = true;
+ } else if (newScrollX < left) {
+ newScrollX = left;
+ clampedX = true;
+ }
+
+ boolean clampedY = false;
+ if (newScrollY > bottom) {
+ newScrollY = bottom;
+ clampedY = true;
+ } else if (newScrollY < top) {
+ newScrollY = top;
+ clampedY = true;
+ }
+
+ if (clampedY && clampedX) {
+ mScroller.springBack(newScrollX, newScrollY, 0, getScrollRangeX(), 0, getScrollRangeY());
+ }
+
+ onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
+
+ return getScrollX() - oldScrollX == deltaX || getScrollY() - oldScrollY == deltaY;
+ }
+
+ private int getScrollRangeY() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0, child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
+ }
+ return scrollRange;
+ }
+
+ private int getScrollRangeX() {
+ int scrollRange = 0;
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ scrollRange = Math.max(0, child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight()));
+ }
+ return scrollRange;
+ }
+
+ /**
+ * Finds the next focusable component that fits in the specified bounds.
+ *
+ * @param topFocus look for a candidate is the one at the top of the bounds
+ * if topFocus is true, or at the bottom of the bounds if topFocus is
+ * false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found
+ * @param bottom the bottom offset of the bounds in which a focusable must
+ * be found
+ * @return the next focusable component in the bounds or null if none can
+ * be found
+ */
+ private View findFocusableViewInBoundsVertically(boolean topFocus, int top, int bottom) {
+
+ List focusables = getFocusables(View.FOCUS_FORWARD);
+ View focusCandidate = null;
+
+ /*
+ * A fully contained focusable is one where its top is below the bound's
+ * top, and its bottom is above the bound's bottom. A partially
+ * contained focusable is one where some part of it is within the
+ * bounds, but it also has some part that is not within bounds. A fully contained
+ * focusable is preferred to a partially contained focusable.
+ */
+ boolean foundFullyContainedFocusable = false;
+
+ int count = focusables.size();
+ for (int i = 0; i < count; i++) {
+ View view = focusables.get(i);
+ int viewTop = view.getTop();
+ int viewBottom = view.getBottom();
+
+ if (top < viewBottom && viewTop < bottom) {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+
+ final boolean viewIsFullyContained = (top < viewTop) &&
+ (viewBottom < bottom);
+
+ if (focusCandidate == null) {
+ /* No candidate, take this one */
+ focusCandidate = view;
+ foundFullyContainedFocusable = viewIsFullyContained;
+ } else {
+ final boolean viewIsCloserToBoundary =
+ (topFocus && viewTop < focusCandidate.getTop()) ||
+ (!topFocus && viewBottom > focusCandidate
+ .getBottom());
+
+ if (foundFullyContainedFocusable) {
+ if (viewIsFullyContained && viewIsCloserToBoundary) {
+ /*
+ * We're dealing with only fully contained views, so
+ * it has to be closer to the boundary to beat our
+ * candidate
+ */
+ focusCandidate = view;
+ }
+ } else {
+ if (viewIsFullyContained) {
+ /* Any fully contained view beats a partially contained view */
+ focusCandidate = view;
+ foundFullyContainedFocusable = true;
+ } else if (viewIsCloserToBoundary) {
+ /*
+ * Partially contained view beats another partially
+ * contained view if it's closer
+ */
+ focusCandidate = view;
+ }
+ }
+ }
+ }
+ }
+
+ return focusCandidate;
+ }
+
+ private View findFocusableViewInBoundsHorizontally(boolean topFocus, int left, int right) {
+ List focusables = getFocusables(View.FOCUS_FORWARD);
+ View focusCandidate = null;
+
+ /*
+ * A fully contained focusable is one where its top is below the bound's
+ * top, and its bottom is above the bound's bottom. A partially
+ * contained focusable is one where some part of it is within the
+ * bounds, but it also has some part that is not within bounds. A fully contained
+ * focusable is preferred to a partially contained focusable.
+ */
+ boolean foundFullyContainedFocusable = false;
+
+ int count = focusables.size();
+ for (int i = 0; i < count; i++) {
+ View view = focusables.get(i);
+ int viewLeft = view.getLeft();
+ int viewRight = view.getRight();
+
+ if (left < viewRight && viewLeft < viewRight) {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+
+ final boolean viewIsFullyContained = (left < viewLeft) &&
+ (viewRight < right);
+
+ if (focusCandidate == null) {
+ /* No candidate, take this one */
+ focusCandidate = view;
+ foundFullyContainedFocusable = viewIsFullyContained;
+ } else {
+ final boolean viewIsCloserToBoundary =
+ (topFocus && viewLeft < focusCandidate.getTop()) ||
+ (!topFocus && viewRight > focusCandidate
+ .getBottom());
+
+ if (foundFullyContainedFocusable) {
+ if (viewIsFullyContained && viewIsCloserToBoundary) {
+ /*
+ * We're dealing with only fully contained views, so
+ * it has to be closer to the boundary to beat our
+ * candidate
+ */
+ focusCandidate = view;
+ }
+ } else {
+ if (viewIsFullyContained) {
+ /* Any fully contained view beats a partially contained view */
+ focusCandidate = view;
+ foundFullyContainedFocusable = true;
+ } else if (viewIsCloserToBoundary) {
+ /*
+ * Partially contained view beats another partially
+ * contained view if it's closer
+ */
+ focusCandidate = view;
+ }
+ }
+ }
+ }
+ }
+
+ return focusCandidate;
+ }
+
+ /**
+ * Handles scrolling in response to a "page up/down" shortcut press. This
+ * method will scroll the view by one page up or down and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.
+ *
+ * @param direction the scroll direction: {@link View#FOCUS_UP}
+ * to go one page up or
+ * {@link View#FOCUS_DOWN} to go one page down
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean pageScrollHorizontally(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int width = getWidth();
+
+ if (down) {
+ mTempRect.left = getScrollX() + width;
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ if (mTempRect.left + width > view.getRight()) {
+ mTempRect.left = view.getRight() - width;
+ }
+ }
+ } else {
+ mTempRect.left = getScrollY() - width;
+ if (mTempRect.left < 0) {
+ mTempRect.left = 0;
+ }
+ }
+ mTempRect.right = mTempRect.left + width;
+ return scrollAndFocusHorizontally(direction, mTempRect.left, mTempRect.right);
+ }
+
+ public boolean pageScrollVertically(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ if (down) {
+ mTempRect.top = getScrollY() + height;
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ if (mTempRect.top + height > view.getBottom()) {
+ mTempRect.top = view.getBottom() - height;
+ }
+ }
+ } else {
+ mTempRect.top = getScrollY() - height;
+ if (mTempRect.top < 0) {
+ mTempRect.top = 0;
+ }
+ }
+ mTempRect.bottom = mTempRect.top + height;
+ return scrollAndFocusVertically(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the top or bottom and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.
+ *
+ * @param direction the scroll direction: {@link View#FOCUS_UP}
+ * to go the top of the view or
+ * {@link View#FOCUS_DOWN} to go the bottom
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean fullScrollVertically(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ mTempRect.top = 0;
+ mTempRect.bottom = height;
+
+ if (down) {
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ mTempRect.bottom = view.getBottom() + getPaddingBottom();
+ mTempRect.top = mTempRect.bottom - height;
+ }
+ }
+
+ return scrollAndFocusVertically(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the top or bottom and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.
+ *
+ * @param direction the scroll direction: {@link View#FOCUS_UP}
+ * to go the top of the view or
+ * {@link View#FOCUS_DOWN} to go the bottom
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean fullScrollHorizontally(int direction) {
+ boolean down = direction == View.FOCUS_RIGHT;
+ int width = getWidth();
+
+ mTempRect.left = 0;
+ mTempRect.right = width;
+
+ if (down) {
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ mTempRect.right = view.getRight() + getPaddingLeft();
+ mTempRect.left = mTempRect.right - width;
+ }
+ }
+
+ return scrollAndFocusHorizontally(direction, mTempRect.left, mTempRect.right);
+ }
+
+
+ private boolean scrollAndFocusHorizontally(int direction, int left, int right) {
+ boolean handled = true;
+
+ int width = getWidth();
+ int containerLeft = getScrollX();
+ int containerRight = containerLeft + width;
+ boolean up = direction == View.FOCUS_LEFT;
+
+ View newFocused = findFocusableViewInBoundsHorizontally(up, left, right);
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (left >= containerLeft && right <= containerRight) {
+ handled = false;
+ } else {
+ int delta = up ? (left - containerLeft) : (right - containerRight);
+ doScrollX(delta);
+ }
+
+ if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+ return handled;
+ }
+
+ /**
+ * Scrolls the view to make the area defined by top
and
+ * bottom
visible. This method attempts to give the focus
+ * to a component visible in this area. If no component can be focused in
+ * the new visible area, the focus is reclaimed by this ScrollView.
+ *
+ * @param direction the scroll direction: {@link View#FOCUS_UP}
+ * to go upward, {@link View#FOCUS_DOWN} to downward
+ * @param top the top offset of the new area to be made visible
+ * @param bottom the bottom offset of the new area to be made visible
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ private boolean scrollAndFocusVertically(int direction, int top, int bottom) {
+ boolean handled = true;
+
+ int height = getHeight();
+ int containerTop = getScrollY();
+ int containerBottom = containerTop + height;
+ boolean up = direction == View.FOCUS_UP;
+
+ View newFocused = findFocusableViewInBoundsVertically(up, top, bottom);
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (top >= containerTop && bottom <= containerBottom) {
+ handled = false;
+ } else {
+ int delta = up ? (top - containerTop) : (bottom - containerBottom);
+ doScrollY(delta);
+ }
+
+ if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+ return handled;
+ }
+
+ /**
+ * Handle scrolling in response to an up or down arrow click.
+ *
+ * @param direction The direction corresponding to the arrow key that was
+ * pressed
+ * @return True if we consumed the event, false otherwise
+ */
+ public boolean arrowScrollHorizontally(int direction) {
+
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+ final int maxJump = getMaxScrollAmountX();
+
+ if (nextFocused != null && isWithinDeltaOfScreenX(nextFocused, maxJump, getWidth())) {
+ nextFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+ int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollX(scrollDelta[0]);
+ nextFocused.requestFocus(direction);
+ } else {
+ // no new focus
+ int scrollDelta = maxJump;
+
+ if (direction == View.FOCUS_UP && getScrollX() < scrollDelta) {
+ scrollDelta = getScrollX();
+ } else if (direction == View.FOCUS_DOWN) {
+ if (getChildCount() > 0) {
+ int daRight = getChildAt(0).getRight();
+ int screenRight = getScrollX() + getWidth() - getPaddingRight();
+ if (daRight - screenRight < maxJump) {
+ scrollDelta = daRight - screenRight;
+ }
+ }
+ }
+ if (scrollDelta == 0) {
+ return false;
+ }
+ doScrollX(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+ }
+
+ if (currentFocused != null && currentFocused.isFocused() && isOffScreenX(currentFocused)) {
+ // previously focused item still has focus and is off screen, give
+ // it up (take it back to ourselves)
+ // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+ // sure to
+ // get it)
+ final int descendantFocusability = getDescendantFocusability(); // save
+ setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ requestFocus();
+ setDescendantFocusability(descendantFocusability); // restore
+ }
+ return true;
+ }
+
+ /**
+ * Handle scrolling in response to an up or down arrow click.
+ *
+ * @param direction The direction corresponding to the arrow key that was
+ * pressed
+ * @return True if we consumed the event, false otherwise
+ */
+ public boolean arrowScrollVertically(int direction) {
+
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+ final int maxJump = getMaxScrollAmountY();
+
+ if (nextFocused != null && isWithinDeltaOfScreenY(nextFocused, maxJump, getHeight())) {
+ nextFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+ int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollY(scrollDelta[1]);
+ nextFocused.requestFocus(direction);
+ } else {
+ // no new focus
+ int scrollDelta = maxJump;
+
+ if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
+ scrollDelta = getScrollY();
+ } else if (direction == View.FOCUS_DOWN) {
+ if (getChildCount() > 0) {
+ int daBottom = getChildAt(0).getBottom();
+ int screenBottom = getScrollY() + getHeight() - getPaddingBottom();
+ if (daBottom - screenBottom < maxJump) {
+ scrollDelta = daBottom - screenBottom;
+ }
+ }
+ }
+ if (scrollDelta == 0) {
+ return false;
+ }
+ doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+ }
+
+ if (currentFocused != null && currentFocused.isFocused()
+ && isOffScreenY(currentFocused)) {
+ // previously focused item still has focus and is off screen, give
+ // it up (take it back to ourselves)
+ // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+ // sure to
+ // get it)
+ final int descendantFocusability = getDescendantFocusability(); // save
+ setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ requestFocus();
+ setDescendantFocusability(descendantFocusability); // restore
+ }
+ return true;
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is scrolled off
+ * screen.
+ */
+ private boolean isOffScreenX(View descendant) {
+ return !isWithinDeltaOfScreenX(descendant, 0, getWidth());
+ }
+
+
+ /**
+ * @return whether the descendant of this scroll view is scrolled off
+ * screen.
+ */
+ private boolean isOffScreenY(View descendant) {
+ return !isWithinDeltaOfScreenY(descendant, 0, getHeight());
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private boolean isWithinDeltaOfScreenX(View descendant, int delta, int width) {
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+ return (mTempRect.right + delta) >= getScrollX()
+ && (mTempRect.left - delta) <= (getScrollX() + width);
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private boolean isWithinDeltaOfScreenY(View descendant, int delta, int height) {
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+ return (mTempRect.bottom + delta) >= getScrollY()
+ && (mTempRect.top - delta) <= (getScrollY() + height);
+ }
+
+ private void doScrollXY(int[] delta) {
+ doScrollXY(delta[0], delta[1]);
+ }
+
+ private void doScrollXY(int deltaX, int deltaY) {
+ if (deltaX != 0 || deltaY != 0) {
+ if (mSmoothScrollingEnabled) {
+ smoothScrollBy(deltaX, deltaY);
+ } else {
+ scrollBy(deltaX, deltaY);
+ }
+ }
+ }
+
+ /**
+ * Smooth scroll by a Y delta
+ *
+ * @param delta the number of pixels to scroll by on the Y axis
+ */
+ private void doScrollY(int delta) {
+ if (delta != 0) {
+ if (mSmoothScrollingEnabled) {
+ smoothScrollBy(0, delta);
+ } else {
+ scrollBy(0, delta);
+ }
+ }
+ }
+
+ private void doScrollX(int delta) {
+ if (delta != 0) {
+ if (mSmoothScrollingEnabled) {
+ smoothScrollBy(delta, 0);
+ } else {
+ scrollBy(delta, 0);
+ }
+ }
+ }
+
+ /**
+ * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param dx the number of pixels to scroll by on the X axis
+ * @param dy the number of pixels to scroll by on the Y axis
+ */
+ public final void smoothScrollBy(int dx, int dy) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ return;
+ }
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ if (duration > ANIMATED_SCROLL_GAP) {
+ setScrollState(SCROLL_STATE_SETTLING);
+
+ final int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ final int bottom = getChildAt(0).getHeight();
+ final int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ final int right = getChildAt(0).getWidth();
+ final int maxY = Math.max(0, bottom - height);
+ final int maxX = Math.max(0, right - width);
+ final int scrollY = getScrollY();
+ final int scrollX = getScrollX();
+ dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
+ dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
+
+ mScroller.startScroll(scrollX, scrollY, dx, dy);
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ setScrollState(SCROLL_STATE_IDLE);
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+ scrollBy(dx, dy);
+ }
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+ }
+
+ /**
+ * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+ *
+ * @param x the position where to scroll on the X axis
+ * @param y the position where to scroll on the Y axis
+ */
+ public final void smoothScrollTo(int x, int y) {
+ smoothScrollBy(x - getScrollX(), y - getScrollY());
+ }
+
+
+ /**
+ * The scroll range of a scroll view is the overall height of all of its
+ * children.
+ */
+ @Override
+ public int computeVerticalScrollRange() {
+ if (!canScrollVertically()) {
+ return super.computeVerticalScrollRange();
+ }
+ final int count = getChildCount();
+ final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+ if (count == 0) {
+ return contentHeight;
+ }
+
+ int scrollRange = getChildAt(0).getBottom();
+ final int scrollY = getScrollY();
+ final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
+ if (scrollY < 0) {
+ scrollRange -= scrollY;
+ } else if (scrollY > overscrollBottom) {
+ scrollRange += scrollY - overscrollBottom;
+ }
+
+ return scrollRange;
+ }
+
+ /**
+ *
+ */
+ @Override
+ public int computeVerticalScrollOffset() {
+ return Math.max(0, super.computeVerticalScrollOffset());
+ }
+
+ /**
+ *
+ */
+ @Override
+ public int computeVerticalScrollExtent() {
+ return super.computeVerticalScrollExtent();
+ }
+
+ /**
+ *
+ */
+ @Override
+ public int computeHorizontalScrollRange() {
+ if (!canScrollHorizontally()) {
+ return super.computeHorizontalScrollRange();
+ }
+ final int count = getChildCount();
+ final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft();
+ if (count == 0) {
+ return contentWidth;
+ }
+
+ int scrollRange = getChildAt(0).getRight();
+ final int scrollX = getScrollX();
+ final int overscrollRight = Math.max(0, scrollRange - contentWidth);
+ if (scrollX < 0) {
+ scrollRange -= scrollX;
+ } else if (scrollX > overscrollRight) {
+ scrollRange += scrollX - overscrollRight;
+ }
+ return scrollRange;
+ }
+
+ /**
+ *
+ */
+ @Override
+ public int computeHorizontalScrollOffset() {
+ return Math.max(0, super.computeHorizontalScrollOffset());
+ }
+
+ /**
+ *
+ */
+ @Override
+ public int computeHorizontalScrollExtent() {
+ return super.computeHorizontalScrollExtent();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (getChildCount() == 0) {
+ return;
+ }
+ View child = getChildAt(0);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int childLeft = getPaddingLeft() + lp.leftMargin;
+ int childTop = getPaddingTop() + lp.topMargin;
+ if (mChildLayoutCenter) {
+ if (getMeasuredWidth() > child.getMeasuredWidth()) {
+ childLeft = (getMeasuredWidth() - child.getMeasuredWidth()) / 2;
+ }
+ if (getMeasuredHeight() > child.getMeasuredHeight()) {
+ childTop = (getMeasuredHeight() - child.getMeasuredHeight()) / 2;
+ }
+ }
+ int measureHeight = child.getMeasuredHeight();
+ int measuredWidth = child.getMeasuredWidth();
+ child.layout(childLeft, childTop, measuredWidth + childLeft, childTop + measureHeight);
+
+
+ mIsLayoutDirty = false;
+ // Give a child focus if it needs it
+ if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+ scrollToChild(mChildToScrollTo);
+ }
+ mChildToScrollTo = null;
+
+ if (!mIsLaidOut) {
+ if (mSavedState != null) {
+ scrollTo(mSavedState.scrollXPosition, mSavedState.scrollYPosition);
+ mSavedState = null;
+ } // mScrollY default value is "0"
+
+ final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
+ final int scrollRangeY = Math.max(0,
+ childHeight - (b - t - getPaddingBottom() - getPaddingTop()));
+
+ final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0;
+ final int scrollRangeX = Math.max(0,
+ childWidth - (b - t - getPaddingRight() - getPaddingLeft()));
+
+ int sY = getScrollY();
+ int sX = getScrollX();
+
+ // Don't forget to clamp
+ if (getScrollY() > scrollRangeY) {
+ sY = scrollRangeY;
+ } else if (getScrollY() < 0) {
+ sY = 0;
+ }
+ if (getScrollX() > scrollRangeX) {
+ sX = scrollRangeX;
+ } else if (getScrollX() < 0) {
+ sX = 0;
+ }
+ if (sX != getScrollX() || sY != getScrollY()) {
+ scrollTo(sX, sY);
+ }
+ }
+
+ // Calling this with the present values causes it to re-claim them
+ scrollTo(getScrollX(), getScrollY());
+ mIsLaidOut = true;
+ }
+
+ @Override
+ public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean checkLayoutParams(LayoutParams p) {
+ return p instanceof MarginLayoutParams;
+ }
+
+ @Override
+ protected MarginLayoutParams generateLayoutParams(LayoutParams p) {
+ return new MarginLayoutParams(p);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (getChildCount() == 0) {
+ return;
+ }
+ View child = getChildAt(0);
+ MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int widthPadding = lp.leftMargin + lp.rightMargin + getPaddingLeft() + getPaddingRight();
+ int heightPadding = lp.topMargin + lp.bottomMargin + getPaddingTop() + getPaddingBottom();
+ int maxWidth = widthPadding;
+ int maxHeight = heightPadding;
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ int childState = 0;
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+
+ if (canScrollVertically()) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingBottom() + getPaddingTop() + lp.topMargin + lp.bottomMargin, lp.height);
+ }
+ if (canScrollHorizontally()) {
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width);
+ }
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ childState = combineMeasuredStates(childState, child.getMeasuredState());
+ }
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ int measuredWidth = ViewCompat.resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
+ int measuredHeight = ViewCompat.resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT);
+ setMeasuredDimension(measuredWidth, measuredHeight);
+
+
+ boolean needMeasure = false;
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode != MeasureSpec.UNSPECIFIED && mFillViewportV) {
+ if (child.getMeasuredHeight() < measuredHeight - heightPadding) {
+ int newChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight - heightPadding, MeasureSpec.EXACTLY);
+ if (newChildHeightMeasureSpec != childHeightMeasureSpec) {
+ childHeightMeasureSpec = newChildHeightMeasureSpec;
+ needMeasure = true;
+ }
+ }
+ }
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ if (widthMode != MeasureSpec.UNSPECIFIED && mFillViewportH) {
+ if (child.getMeasuredWidth() < measuredWidth - widthPadding) {
+ int newChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth - widthPadding, MeasureSpec.EXACTLY);
+ if (newChildWidthMeasureSpec != childWidthMeasureSpec) {
+ childWidthMeasureSpec = newChildWidthMeasureSpec;
+ needMeasure = true;
+ }
+ }
+ }
+ if (needMeasure) {
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+
+ if (oldX != x || oldY != y) {
+ final int rangeY = getScrollRangeY();
+ final int rangeX = getScrollRangeX();
+ final int overscrollMode = ViewCompat.getOverScrollMode(this);
+ final boolean canOverscrollY = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && rangeY > 0);
+ final boolean canOverscrollX = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && rangeX > 0);
+ boolean canOverscroll = canOverscrollY || canOverscrollX;
+
+ overScrollByCompat(x - oldX, y - oldY, oldX, oldY, rangeX, rangeY,
+ 0, 0, false);
+
+ if (canOverscroll) {
+ ensureGlows();
+ if (y <= 0 && oldY > 0) {
+ mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
+ } else if (y >= rangeY && oldY < rangeY) {
+ mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
+ }
+
+ if (x <= 0 && oldX > 0) {
+ mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
+ } else if (x >= rangeX && oldX < rangeX) {
+ mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
+ }
+ }
+ }
+
+// final boolean fullyConsumedVertical = canScrollVertically()
+// && x == getScrollX();
+// final boolean fullyConsumedHorizontal = canScrollHorizontally()
+// && x == getScrollY();
+// final boolean fullyConsumedAny = fullyConsumedHorizontal
+// || fullyConsumedVertical;
+
+ if (mScroller.isFinished()) {
+ setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
+ } else {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ /**
+ * Scrolls the view to the given child.
+ *
+ * @param child the View to scroll to
+ */
+ private void scrollToChild(View child) {
+ child.getDrawingRect(mTempRect);
+
+ /* Offset from child's local coordinates to ScrollView coordinates */
+ offsetDescendantRectToMyCoords(child, mTempRect);
+
+ int scrollDelta[] = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+
+ if (scrollDelta[0] != 0 || scrollDelta[1] != 0) {
+ scrollBy(scrollDelta[0], scrollDelta[1]);
+ }
+ }
+
+ /**
+ * If rect is off screen, scroll just enough to get it (or at least the
+ * first screen size chunk of it) on screen.
+ *
+ * @param rect The rectangle.
+ * @param immediate True to scroll immediately without animation
+ * @return true if scrolling was performed
+ */
+ private boolean scrollToChildRect(Rect rect, boolean immediate) {
+ final int delta[] = computeScrollDeltaToGetChildRectOnScreen(rect);
+ final boolean scroll = delta[0] != 0 || delta[1] != 0;
+ if (scroll) {
+ if (delta[0] < 0) {
+ delta[0] = 5 * delta[0];
+ }
+ if (immediate) {
+ scrollBy(delta[0], delta[1]);
+ } else {
+
+ smoothScrollBy(delta[0], delta[1]);
+ }
+ }
+ return scroll;
+ }
+
+ /**
+ * Compute the amount to scroll in the Y direction in order to get
+ * a rectangle completely on the screen (or, if taller than the screen,
+ * at least the first screen size chunk of it).
+ *
+ * @param rect The rect.
+ * @return The scroll delta.
+ */
+ protected int[] computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+ if (getChildCount() == 0) return new int[]{0, 0};
+
+ int height = getHeight();
+ int width = getWidth();
+ int screenTop = getScrollY();
+ int screenBottom = screenTop + height;
+ int screenLeft = getScrollX();
+ int screenRight = screenLeft + width;
+
+ int fadingEdgeY = getVerticalFadingEdgeLength();
+ int fadingEdgeX = getHorizontalFadingEdgeLength();
+
+ // leave room for top fading edge as long as rect isn't at very top
+ if (rect.top > 0) {
+ screenTop += fadingEdgeY;
+ }
+ if (rect.left > 0) {
+ screenLeft += fadingEdgeX;
+ }
+
+ // leave room for bottom fading edge as long as rect isn't at very bottom
+ if (rect.bottom < getChildAt(0).getHeight()) {
+ screenBottom -= fadingEdgeY;
+ }
+ if (rect.right < getChildAt(0).getWidth()) {
+ screenRight -= fadingEdgeX;
+ }
+
+ int scrollYDelta = 0;
+ int scrollXDelta = 0;
+
+ if (rect.bottom > screenBottom && rect.top > screenTop) {
+ // need to move down to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.height() > height) {
+ // just enough to get screen size chunk on
+ scrollYDelta += (rect.top - screenTop);
+ } else {
+ // get entire rect at bottom of screen
+ scrollYDelta += (rect.bottom - screenBottom);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int bottom = getChildAt(0).getBottom();
+ int distanceToBottom = bottom - screenBottom;
+ scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+
+ } else if (rect.top < screenTop && rect.bottom < screenBottom) {
+ // need to move up to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.height() > height) {
+ // screen size chunk
+ scrollYDelta -= (screenBottom - rect.bottom);
+ } else {
+ // entire rect at top
+ scrollYDelta -= (screenTop - rect.top);
+ }
+
+ // make sure we aren't scrolling any further than the top our content
+ scrollYDelta = Math.max(scrollYDelta, -getScrollY());
+ }
+ if (rect.right > screenRight && rect.left > screenLeft) {
+ // need to move down to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.width() > width) {
+ // just enough to get screen size chunk on
+ scrollXDelta += (rect.left - screenLeft);
+ } else {
+ // get entire rect at bottom of screen
+ scrollXDelta += (rect.right - screenRight);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int right = getChildAt(0).getRight();
+ int distanceToRight = right - screenRight;
+ scrollXDelta = Math.min(scrollXDelta, distanceToRight);
+
+ } else if (rect.left < screenLeft && rect.right < screenRight) {
+ // need to move up to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.width() > width) {
+ // screen size chunk
+ scrollXDelta -= (screenRight - rect.right);
+ } else {
+ // entire rect at top
+ scrollXDelta -= (screenLeft - rect.left);
+ }
+
+ // make sure we aren't scrolling any further than the top our content
+ scrollXDelta = Math.max(scrollXDelta, -getScrollX());
+ }
+ return new int[]{scrollXDelta, scrollYDelta};
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (!mIsLayoutDirty) {
+ scrollToChild(focused);
+ } else {
+ // The child may not be laid out yet, we can't compute the scroll yet
+ mChildToScrollTo = focused;
+ }
+ super.requestChildFocus(child, focused);
+ }
+
+
+ /**
+ * When looking for focus in children of a scroll view, need to be a little
+ * more careful not to give focus to something that is scrolled off screen.
+ *
+ * This is more expensive than the default {@link ViewGroup}
+ * implementation, otherwise this behavior might have been made the default.
+ */
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+
+ // convert from forward / backward notation to up / down / left / right
+ // (ugh).
+ if (direction == View.FOCUS_FORWARD) {
+ direction = View.FOCUS_DOWN;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ direction = View.FOCUS_UP;
+ }
+
+ final View nextFocus = previouslyFocusedRect == null ?
+ FocusFinder.getInstance().findNextFocus(this, null, direction) :
+ FocusFinder.getInstance().findNextFocusFromRect(this,
+ previouslyFocusedRect, direction);
+
+ if (nextFocus == null) {
+ return false;
+ }
+
+ if (canScrollHorizontally()) {
+ if (isOffScreenX(nextFocus)) {
+ return false;
+ }
+ } else if (canScrollVertically()) {
+ if (isOffScreenY(nextFocus)) {
+ return false;
+ }
+ }
+
+ return nextFocus.requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+ boolean immediate) {
+ // offset into coordinate space of this scroll view
+ rectangle.offset(child.getLeft() - child.getScrollX(),
+ child.getTop() - child.getScrollY());
+ return scrollToChildRect(rectangle, immediate);
+ }
+
+ @Override
+ public void requestLayout() {
+ mIsLayoutDirty = true;
+ super.requestLayout();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mIsLaidOut = false;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ View currentFocused = findFocus();
+ if (null == currentFocused || this == currentFocused)
+ return;
+
+ // If the currently-focused view was visible on the screen when the
+ // screen was at the old height, then scroll the screen to make that
+ // view visible with the new screen height.
+ if (isWithinDeltaOfScreenX(currentFocused, 0, oldw) || isWithinDeltaOfScreenY(currentFocused, 0, oldh)) {
+ currentFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+ int scrollDelta[] = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollXY(scrollDelta);
+ }
+ }
+
+ /**
+ * Return true if child is a descendant of parent, (or equal to the parent).
+ */
+ private static boolean isViewDescendantOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+ }
+
+ private boolean fling(int velocityX, int velocityY) {
+ final boolean canScrollHorizontal = canScrollHorizontally();
+ final boolean canScrollVertical = canScrollVertically();
+
+ if (!canScrollHorizontal || Math.abs(velocityX) < mMinimumVelocity) {
+ velocityX = 0;
+ }
+ if (!canScrollVertical || Math.abs(velocityY) < mMinimumVelocity) {
+ velocityY = 0;
+ }
+ if (velocityX == 0 && velocityY == 0) {
+ // If we don't have any velocity, return false
+ return false;
+ }
+
+ if (!dispatchNestedPreFling(velocityX, velocityY)) {
+ final int scrollY = getScrollY();
+ final int scrollX = getScrollX();
+ final boolean canFlingY = (scrollY > 0 || velocityY > 0) &&
+ (scrollY < getScrollRangeY() || velocityY < 0);
+ final boolean canFlingX = (scrollX > 0 || velocityX > 0) &&
+ (scrollX < getScrollRangeX() || velocityX < 0);
+ boolean canFling = canFlingY || canFlingX;
+ dispatchNestedFling(velocityX, velocityY, canFling);
+ if (canFling) {
+ setScrollState(SCROLL_STATE_SETTLING);
+
+ velocityX = Math.max(-mMaximumVelocity, Math.min(velocityX, mMaximumVelocity));
+ velocityY = Math.max(-mMaximumVelocity, Math.min(velocityY, mMaximumVelocity));
+
+ int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ int bottom = getChildAt(0).getHeight();
+ int right = getChildAt(0).getWidth();
+ mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, Math.max(0, right - width), 0,
+ Math.max(0, bottom - height), width / 2, height / 2);
+
+// ViewCompat.postInvalidateOnAnimation(this);
+ return true;
+ }
+ }
+ return false;
+ }
+
+// private void endDrag() {
+// mIsBeingDragged = false;
+//
+// recycleVelocityTracker();
+// stopNestedScroll();
+//
+// if (mEdgeGlowTop != null) {
+// mEdgeGlowTop.onRelease();
+// mEdgeGlowBottom.onRelease();
+// mEdgeGlowLeft.onRelease();
+// mEdgeGlowRight.onRelease();
+// }
+// }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This version also clamps the scrolling to the bounds of our child.
+ */
+ @Override
+ public void scrollTo(int x, int y) {
+ // we rely on the fact the View.scrollBy calls scrollTo.
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
+ y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
+ if (x != getScrollX() || y != getScrollY()) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ private void ensureGlows() {
+ if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
+ if (mEdgeGlowTop == null) {
+ Context context = getContext();
+ mEdgeGlowTop = new EdgeEffectCompat(context);
+ mEdgeGlowBottom = new EdgeEffectCompat(context);
+ mEdgeGlowLeft = new EdgeEffectCompat(context);
+ mEdgeGlowRight = new EdgeEffectCompat(context);
+ }
+ } else {
+ mEdgeGlowTop = null;
+ mEdgeGlowBottom = null;
+ mEdgeGlowLeft = null;
+ mEdgeGlowRight = null;
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ if (mEdgeGlowTop != null) {
+ final int scrollX = getScrollX();
+ final int scrollY = getScrollY();
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+ canvas.translate(getPaddingLeft() + scrollX, Math.min(0, scrollY));
+ mEdgeGlowTop.setSize(width, getHeight());
+ if (mEdgeGlowTop.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowBottom.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight();
+
+ canvas.translate(-width + getPaddingLeft() + scrollX,
+ Math.max(getScrollRangeY(), scrollY) + height);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowLeft.isFinished()) {
+
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(270);
+
+ canvas.translate(-height + getPaddingTop() - scrollY, Math.min(0, scrollX));
+// canvas.rotate(90, 0, 0);
+ mEdgeGlowLeft.setSize(height, width);
+ if (mEdgeGlowLeft.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(90);
+ canvas.translate(-getPaddingTop() + scrollY,
+ -(Math.max(getScrollRangeX(), scrollX) + width));
+ mEdgeGlowRight.setSize(height, width);
+ if (mEdgeGlowRight.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+ }
+
+ private static int clamp(int n, int my, int child) {
+ if (my >= child || n < 0) {
+ /* my >= child is this case:
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ *
+ * n < 0 is this case:
+ * |------ me ------|
+ * |-------- child --------|
+ * |-- mScrollX --|
+ */
+ return 0;
+ }
+ if ((my + n) > child) {
+ /* this case:
+ * |------ me ------|
+ * |------ child ------|
+ * |-- mScrollX --|
+ */
+ return child - my;
+ }
+ return n;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mSavedState = ss;
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.scrollYPosition = getScrollY();
+ ss.scrollXPosition = getScrollX();
+ return ss;
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int scrollYPosition;
+ public int scrollXPosition;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ scrollYPosition = source.readInt();
+ scrollXPosition = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(scrollYPosition);
+ dest.writeInt(scrollXPosition);
+ }
+
+ @Override
+ public String toString() {
+ return "HorizontalScrollView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " scrollXPosition=" + scrollXPosition + " scrollYPosition=" + scrollYPosition + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ static class AccessibilityDelegate extends AccessibilityDelegateCompat {
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+ if (super.performAccessibilityAction(host, action, arguments)) {
+ return true;
+ }
+ final HVScrollView nsvHost = (HVScrollView) host;
+ if (!nsvHost.isEnabled()) {
+ return false;
+ }
+ switch (action) {
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
+ final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom()
+ - nsvHost.getPaddingTop();
+ final int viewportWidth = nsvHost.getWidth() - nsvHost.getPaddingRight()
+ - nsvHost.getPaddingLeft();
+ final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight,
+ nsvHost.getScrollRangeY());
+ final int targetScrollX = Math.min(nsvHost.getScrollX() + viewportWidth,
+ nsvHost.getScrollRangeX());
+ if (targetScrollY != nsvHost.getScrollY() || targetScrollX != nsvHost.getScrollX()) {
+ nsvHost.smoothScrollTo(targetScrollX, targetScrollY);
+ return true;
+ }
+ }
+ return false;
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
+ final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom()
+ - nsvHost.getPaddingTop();
+ final int viewportWidth = nsvHost.getWidth() - nsvHost.getPaddingRight()
+ - nsvHost.getPaddingLeft();
+ final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0);
+ final int targetScrollX = Math.min(nsvHost.getScrollX() - viewportWidth, 0);
+ if (targetScrollY != nsvHost.getScrollY() || targetScrollX != nsvHost.getScrollX()) {
+ nsvHost.smoothScrollTo(targetScrollX, targetScrollY);
+ return true;
+ }
+ }
+ return false;
+ }
+ return false;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ final HVScrollView nsvHost = (HVScrollView) host;
+ info.setClassName(ScrollView.class.getName());
+ if (nsvHost.isEnabled()) {
+ final int scrollRangeY = nsvHost.getScrollRangeY();
+ final int scrollRangeX = nsvHost.getScrollRangeX();
+ if (scrollRangeY > 0 || scrollRangeX > 0) {
+ info.setScrollable(true);
+ if (nsvHost.getScrollY() > 0 || nsvHost.getScrollX() > 0) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+ }
+ if (nsvHost.getScrollY() < scrollRangeY || nsvHost.getScrollX() < scrollRangeX) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(@NonNull View host, @NonNull AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ final HVScrollView nsvHost = (HVScrollView) host;
+ event.setClassName(ScrollView.class.getName());
+ final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
+ final boolean scrollableX = nsvHost.getScrollRangeY() > 0;
+ final boolean scrollableY = nsvHost.getScrollRangeX() > 0;
+ boolean scrollable = scrollableX || scrollableY;
+ record.setScrollable(scrollable);
+ record.setScrollX(nsvHost.getScrollX());
+ record.setScrollY(nsvHost.getScrollY());
+ record.setMaxScrollX(nsvHost.getScrollRangeX());
+ record.setMaxScrollY(nsvHost.getScrollRangeY());
+ }
+ }
+
+
+ public void setFillViewportH(boolean fillViewportH) {
+ if (mFillViewportH != fillViewportH) {
+ this.mFillViewportH = fillViewportH;
+ requestLayout();
+ }
+ }
+
+ public void setFillViewportV(boolean fillViewportV) {
+ if (mFillViewportV != fillViewportV) {
+ this.mFillViewportV = fillViewportV;
+ requestLayout();
+ }
+ }
+
+ public void setFillViewportHV(boolean fillViewportH, boolean fillViewportV) {
+ if (mFillViewportH != fillViewportH || mFillViewportV != fillViewportV) {
+ this.mFillViewportH = fillViewportH;
+ this.mFillViewportV = fillViewportV;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public boolean canScrollHorizontally(int direction) {
+ if (direction > 0) {
+ return getScrollX() < getScrollRangeX();
+ } else {
+ return getScrollX() > 0 && getScrollRangeX() > 0;
+ }
+ }
+
+ @Override
+ public boolean canScrollVertically(int direction) {
+ if (direction > 0) {
+ return getScrollY() < getScrollRangeY();
+ } else {
+ return getScrollY() > 0 && getScrollRangeY() > 0;
+ }
+ }
+
+
+}
diff --git a/servicing/src/main/java/com/za/signature/view/HandWriteEditView.java b/servicing/src/main/java/com/za/signature/view/HandWriteEditView.java
new file mode 100644
index 0000000..d4ed89b
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/HandWriteEditView.java
@@ -0,0 +1,201 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.inputmethod.EditorInfo;
+
+import androidx.appcompat.widget.AppCompatEditText;
+
+import com.za.signature.util.DisplayUtil;
+import com.za.signature.util.SystemUtil;
+
+
+/**
+ * 显示手写字的View
+ *
+ * @author king
+ * @since 2018-06-28
+ */
+public class HandWriteEditView extends AppCompatEditText {
+
+ private float lineHeight = 150f;
+ boolean reLayout = false;
+ private TextWatch textWatcher;
+
+ public HandWriteEditView(Context context) {
+ super(context);
+ init();
+ }
+
+ public HandWriteEditView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public HandWriteEditView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ this.setTextIsSelectable(false);
+ this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+ this.setGravity(Gravity.START);
+
+ //禁止选择复制粘贴
+ SystemUtil.disableCopyAndPaste(this);
+ lineHeight = (float) DisplayUtil.dip2px(getContext(), 50f);
+ addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ float add = lineHeight;
+ setLineSpacing(0f, 1f);
+ setLineSpacing(add, 0);
+ setIncludeFontPadding(false);
+ setGravity(Gravity.CENTER_VERTICAL);
+ int top = (int) ((add - getTextSize()) * 0.5f);
+ setPadding(getPaddingLeft(), top, getPaddingRight(), -top);
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ if (textWatcher != null) {
+ textWatcher.afterTextChanged(editable);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * 设置行高
+ *
+ */
+ public void setLineHeight(float lineHeight) {
+ this.lineHeight = lineHeight;
+ invalidate();
+ }
+
+
+ /**
+ * 添加手写文字
+ *
+ * @param srcBitmap 手写文字图片
+ */
+ public Editable addBitmapToText(Bitmap srcBitmap) {
+ if (srcBitmap == null) {
+ return null;
+ }
+ SpannableString mSpan = new SpannableString("1");
+ mSpan.setSpan(new ImageSpan(getContext(), srcBitmap), mSpan.length() - 1, mSpan.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ Editable editable = getText();
+ //获取光标所在位置
+ int index = getSelectionStart();
+ editable.insert(index, mSpan);
+ setText(editable);
+ setSelection(index + mSpan.length());
+ return editable;
+ }
+
+ /**
+ * 添加空格
+ */
+ public void addSpace(int fontSize) {
+ int size = DisplayUtil.dip2px(getContext(), fontSize);
+ ColorDrawable drawable = new ColorDrawable(Color.TRANSPARENT);
+ Bitmap bitmap = Bitmap.createBitmap(fontSize, size, Bitmap.Config.ARGB_4444);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.draw(canvas);
+ addBitmapToText(bitmap);
+ }
+
+ /**
+ * 删除文字
+ */
+ public Editable deleteBitmapFromText() {
+
+ Editable editable = getEditableText();
+ int start = getSelectionStart();
+ if (start == 0) {
+ return null;
+ }
+ editable.delete(start - 1, start);
+ return editable;
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (!reLayout) {
+ reLayout = true;
+ setIncludeFontPadding(false);
+ setGravity(Gravity.CENTER_VERTICAL);
+ setLineSpacing(lineHeight, 0);
+ int top = (int) ((lineHeight - getTextSize()) * 0.5f);
+ setPadding(getPaddingLeft(), top, getPaddingRight(), -top);
+ requestLayout();
+ invalidate();
+ }
+ }
+
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ super.onMeasure(expandSpec, expandSpec);
+ }
+
+ boolean canPaste() {
+ return false;
+ }
+
+ boolean canCut() {
+ return false;
+ }
+
+ boolean canCopy() {
+ return false;
+ }
+
+ boolean canSelectAllText() {
+ return false;
+ }
+
+ boolean canSelectText() {
+ return false;
+ }
+
+ boolean textCanBeSelected() {
+ return false;
+ }
+
+ @Override
+ public boolean isSuggestionsEnabled() {
+ return false;
+ }
+
+ public void addTextWatcher(TextWatch textWatcher) {
+ this.textWatcher = textWatcher;
+ }
+
+ public interface TextWatch {
+ void afterTextChanged(Editable var1);
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/PaintSettingWindow.java b/servicing/src/main/java/com/za/signature/view/PaintSettingWindow.java
new file mode 100644
index 0000000..8ab38e6
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/PaintSettingWindow.java
@@ -0,0 +1,172 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+
+import com.za.servicing.R;
+import com.za.signature.config.PenConfig;
+
+/**
+ * 画笔设置窗口
+ *
+ * @author king
+ * @since 2018-06-04
+ */
+public class PaintSettingWindow extends PopupWindow {
+ public static final String[] PEN_COLORS = new String[]{"#101010", "#027de9", "#0cba02", "#f9d403", "#ec041f"};
+ public static final int[] PEN_SIZES = new int[]{5, 15, 20, 25, 30};
+
+ private final Context context;
+ private CircleView lastSelectColorView;
+ private CircleView lastSelectSizeView;
+ private int selectColor;
+ private View rootView;
+ private OnSettingListener settingListener;
+
+ public PaintSettingWindow(Context context) {
+ super(context);
+ this.context = context;
+ init();
+ }
+
+ private void init() {
+ rootView = LayoutInflater.from(context).inflate(R.layout.sign_paint_setting, null);
+ LinearLayout container = rootView.findViewById(R.id.color_container);
+ for (int i = 0; i < container.getChildCount(); i++) {
+ final int index = i;
+ final CircleView circleView = (CircleView) container.getChildAt(i);
+
+ if (circleView.getPaintColor() == PenConfig.PAINT_COLOR) {
+ circleView.showBorder(true);
+ lastSelectColorView = circleView;
+ }
+
+ circleView.setOnClickListener(v -> {
+ if (lastSelectColorView != null) {
+ lastSelectColorView.showBorder(false);
+ }
+ circleView.showBorder(true);
+ selectColor = Color.parseColor(PEN_COLORS[index]);
+ lastSelectColorView = circleView;
+ PenConfig.PAINT_COLOR = selectColor;
+ PenConfig.setPaintColor(context, selectColor);
+ if (settingListener != null) {
+ settingListener.onColorSetting(selectColor);
+ }
+ });
+ }
+ LinearLayout sizeContainer = rootView.findViewById(R.id.size_container);
+ for (int i = 0; i < sizeContainer.getChildCount(); i++) {
+ final int index = i;
+ final CircleView circleView = (CircleView) sizeContainer.getChildAt(i);
+ if (circleView.getRadiusLevel() == PenConfig.PAINT_SIZE_LEVEL) {
+ circleView.showBorder(true);
+ lastSelectSizeView = circleView;
+ }
+ circleView.setOnClickListener(v -> {
+ if (lastSelectSizeView != null) {
+ lastSelectSizeView.showBorder(false);
+ }
+ circleView.showBorder(true);
+ lastSelectSizeView = circleView;
+ PenConfig.PAINT_SIZE_LEVEL = index;
+ PenConfig.savePaintTextLevel(context, index);
+ if (settingListener != null) {
+ settingListener.onSizeSetting(index);
+ }
+ });
+ }
+ this.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ this.setContentView(rootView);
+ this.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ this.setFocusable(true);
+ this.setOutsideTouchable(true);
+ this.update();
+ }
+
+ /**
+ * 显示在左上角
+ */
+ public void popAtTopLeft() {
+ View sv = rootView.findViewById(R.id.size_container);
+ View cv = rootView.findViewById(R.id.color_container);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(20, 10, 20, 0);
+ lp.gravity = Gravity.CENTER;
+ sv.setLayoutParams(lp);
+ LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp1.setMargins(20, 0, 20, 40);
+ lp1.gravity = Gravity.CENTER;
+ cv.setLayoutParams(lp1);
+ rootView.setBackgroundResource(R.mipmap.sign_top_left_pop_bg);
+ }
+
+ /**
+ * 显示在右上角
+ */
+ public void popAtTopRight() {
+ View sv = rootView.findViewById(R.id.size_container);
+ View cv = rootView.findViewById(R.id.color_container);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(20, 10, 20, 0);
+ lp.gravity = Gravity.CENTER;
+ sv.setLayoutParams(lp);
+ LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp1.setMargins(20, 0, 20, 40);
+ lp1.gravity = Gravity.CENTER;
+ cv.setLayoutParams(lp1);
+ rootView.setBackgroundResource(R.mipmap.sign_top_right_pop_bg);
+ }
+
+ /**
+ * 显示在右下角
+ */
+ public void popAtBottomRight() {
+ View sv = rootView.findViewById(R.id.size_container);
+ View cv = rootView.findViewById(R.id.color_container);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(20, 40, 20, 0);
+ lp.gravity = Gravity.CENTER;
+ sv.setLayoutParams(lp);
+ LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp1.setMargins(20, 0, 20, 10);
+ lp1.gravity = Gravity.CENTER;
+ cv.setLayoutParams(lp1);
+ rootView.setBackgroundResource(R.mipmap.sign_bottom_right_pop_bg);
+ }
+
+ /**
+ * 显示在左边
+ */
+ public void popAtLeft() {
+ View sv = rootView.findViewById(R.id.size_container);
+ View cv = rootView.findViewById(R.id.color_container);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(40, 20, 55, 0);
+ lp.gravity = Gravity.CENTER;
+ sv.setLayoutParams(lp);
+ LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp1.setMargins(40, 0, 55, 20);
+ lp1.gravity = Gravity.CENTER;
+ cv.setLayoutParams(lp1);
+ rootView.setBackgroundResource(R.mipmap.sign_left_pop_bg);
+ }
+
+ public interface OnSettingListener {
+ void onColorSetting(int color);
+
+ void onSizeSetting(int index);
+ }
+
+ public void setSettingListener(OnSettingListener settingListener) {
+ this.settingListener = settingListener;
+ }
+}
diff --git a/servicing/src/main/java/com/za/signature/view/PaintView.java b/servicing/src/main/java/com/za/signature/view/PaintView.java
new file mode 100644
index 0000000..44e08bd
--- /dev/null
+++ b/servicing/src/main/java/com/za/signature/view/PaintView.java
@@ -0,0 +1,504 @@
+package com.za.signature.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.za.servicing.R;
+import com.za.signature.config.PenConfig;
+import com.za.signature.pen.BasePen;
+import com.za.signature.pen.Eraser;
+import com.za.signature.pen.SteelPen;
+import com.za.signature.util.BitmapUtil;
+import com.za.signature.util.DisplayUtil;
+import com.za.signature.util.StepOperator;
+
+
+/**
+ * 手写画板
+ *
+ * @author king
+ * @since 2018/5/4
+ */
+public class PaintView extends View {
+
+ public static final int TYPE_PEN = 0;
+ public static final int TYPE_ERASER = 1;
+
+ private Paint mPaint;
+ private Canvas mCanvas;
+ private Bitmap mBitmap;
+ private BasePen mStokeBrushPen;
+
+ /**
+ * 是否允许写字
+ */
+ private boolean isFingerEnable = true;
+ /**
+ * 是否橡皮擦模式
+ */
+ private boolean isEraser = false;
+
+ /**
+ * 是否有绘制
+ */
+ private boolean hasDraw = false;
+
+
+ /**
+ * 画笔轨迹记录
+ */
+ private StepOperator mStepOperation;
+
+ private StepCallback mCallback;
+
+ /**
+ * 是否可以撤销
+ */
+ private boolean mCanUndo;
+ /**
+ * 是否可以恢复
+ */
+ private boolean mCanRedo;
+
+ private int mWidth;
+ private int mHeight;
+
+ private boolean isDrawing = false;//是否正在绘制
+ private int toolType = 0; //记录手写笔类型:触控笔/手指
+
+ private Eraser eraser;
+
+ public PaintView(Context context) {
+ this(context, null);
+ }
+
+ public PaintView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PaintView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ }
+
+ /**
+ * 初始化画板
+ *
+ * @param width 画板宽度
+ * @param height 画板高度
+ * @param path 初始图片路径
+ */
+ public void init(int width, int height, String path) {
+ this.mWidth = width;
+ this.mHeight = height;
+
+ mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_4444);
+ mStokeBrushPen = new SteelPen();
+
+ initPaint();
+ initCanvas();
+
+ mStepOperation = new StepOperator();
+ if (!TextUtils.isEmpty(path)) {
+ Bitmap bitmap = BitmapFactory.decodeFile(path);
+ resize(bitmap, mWidth, mHeight);
+ } else {
+ mStepOperation.addBitmap(mBitmap);
+ }
+ //橡皮擦
+ eraser = new Eraser(getResources().getDimensionPixelSize(R.dimen.sign_eraser_size));
+ }
+
+ /**
+ * 初始画笔设置
+ */
+ private void initPaint() {
+ int strokeWidth = DisplayUtil.dip2px(getContext(), PaintSettingWindow.PEN_SIZES[PenConfig.PAINT_SIZE_LEVEL]);
+ mPaint = new Paint();
+ mPaint.setColor(PenConfig.PAINT_COLOR);
+ mPaint.setStrokeWidth(strokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setAlpha(0xFF);
+ mPaint.setAntiAlias(true);
+ mPaint.setStrokeMiter(1.0f);
+ mStokeBrushPen.setPaint(mPaint);
+ }
+
+ private void initCanvas() {
+ mCanvas = new Canvas(mBitmap);
+ //设置画布的背景色为透明
+ mCanvas.drawColor(Color.TRANSPARENT);
+ }
+
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ if (!isEraser) {
+ mStokeBrushPen.draw(canvas);
+ }
+ super.onDraw(canvas);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ return super.dispatchTouchEvent(ev);
+ }
+
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ toolType = event.getToolType(event.getActionIndex());
+ if (!isFingerEnable && toolType != MotionEvent.TOOL_TYPE_STYLUS) {
+ return false;
+ }
+ if (isEraser) {
+ eraser.handleEraserEvent(event, mCanvas);
+ } else {
+ mStokeBrushPen.onTouchEvent(event, mCanvas);
+ }
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ isDrawing = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ hasDraw = true;
+ mCanUndo = true;
+ isDrawing = true;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ isDrawing = false;
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mStepOperation != null && isDrawing) {
+ mStepOperation.addBitmap(mBitmap);
+ }
+ mCanUndo = !mStepOperation.currentIsFirst();
+ mCanRedo = !mStepOperation.currentIsLast();
+ if (mCallback != null) {
+ mCallback.onOperateStatusChanged();
+ }
+ isDrawing = false;
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ /**
+ * @return 判断是否有绘制内容在画布上
+ */
+ public boolean isEmpty() {
+ return !hasDraw;
+ }
+
+ /**
+ * 撤销
+ */
+ public void undo() {
+
+ if (mStepOperation == null || !mCanUndo) {
+ return;
+ }
+ if (!mStepOperation.currentIsFirst()) {
+ mCanUndo = true;
+ mStepOperation.undo(mBitmap);
+ hasDraw = true;
+ invalidate();
+
+ if (mStepOperation.currentIsFirst()) {
+ mCanUndo = false;
+ hasDraw = false;
+ }
+ } else {
+ mCanUndo = false;
+ hasDraw = false;
+ }
+ if (!mStepOperation.currentIsLast()) {
+ mCanRedo = true;
+ }
+ if (mCallback != null) {
+ mCallback.onOperateStatusChanged();
+ }
+ }
+
+ /**
+ * 恢复
+ */
+ public void redo() {
+ if (mStepOperation == null || !mCanRedo) {
+ return;
+ }
+ if (!mStepOperation.currentIsLast()) {
+ mCanRedo = true;
+ mStepOperation.redo(mBitmap);
+ hasDraw = true;
+ invalidate();
+ if (mStepOperation.currentIsLast()) {
+ mCanRedo = false;
+ }
+ } else {
+ mCanRedo = false;
+ }
+ if (!mStepOperation.currentIsFirst()) {
+ mCanUndo = true;
+ }
+ if (mCallback != null) {
+ mCallback.onOperateStatusChanged();
+ }
+ }
+
+ /**
+ * 清除画布,记得清除点的集合
+ */
+ public void reset() {
+ mBitmap.eraseColor(Color.TRANSPARENT);
+ hasDraw = false;
+ mStokeBrushPen.clear();
+ if (mStepOperation != null) {
+ mStepOperation.reset();
+ mStepOperation.addBitmap(mBitmap);
+ }
+ mCanRedo = false;
+ mCanUndo = false;
+ if (mCallback != null) {
+ mCallback.onOperateStatusChanged();
+ }
+ invalidate();
+ }
+
+
+ public void release() {
+ destroyDrawingCache();
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ if (mStepOperation != null) {
+ mStepOperation.freeBitmaps();
+ mStepOperation = null;
+ }
+ }
+
+ public interface StepCallback {
+ /**
+ * 操作变更
+ */
+ void onOperateStatusChanged();
+ }
+
+ public void setStepCallback(StepCallback callback) {
+ this.mCallback = callback;
+ }
+
+ /**
+ * 设置画笔样式
+ *
+ */
+ public void setPenType(int penType) {
+ isEraser = false;
+ switch (penType) {
+ case TYPE_PEN:
+ mStokeBrushPen = new SteelPen();
+ break;
+ case TYPE_ERASER:
+ isEraser = true;
+ break;
+ }
+ //设置
+ if (mStokeBrushPen.isNullPaint()) {
+ mStokeBrushPen.setPaint(mPaint);
+ }
+ invalidate();
+ }
+
+ /**
+ * 设置画笔大小
+ *
+ * @param width 大小
+ */
+ public void setPaintWidth(int width) {
+ if (mPaint != null) {
+ mPaint.setStrokeWidth(DisplayUtil.dip2px(getContext(), width));
+// eraser.setPaintWidth(DisplayUtil.dip2px(getContext(), width));
+ mStokeBrushPen.setPaint(mPaint);
+ invalidate();
+ }
+ }
+
+
+ /**
+ * 设置画笔颜色
+ *
+ * @param color 颜色
+ */
+ public void setPaintColor(int color) {
+ if (mPaint != null) {
+ mPaint.setColor(color);
+ mStokeBrushPen.setPaint(mPaint);
+ invalidate();
+ }
+ }
+
+ /**
+ * 构建Bitmap
+ *
+ * @return 所绘制的bitmap
+ */
+ public Bitmap buildAreaBitmap(boolean isCrop) {
+ if (!hasDraw) {
+ return null;
+ }
+ Bitmap result;
+ if (isCrop) {
+ result = BitmapUtil.clearBlank(mBitmap, 50, Color.TRANSPARENT);
+ } else {
+ result = mBitmap;
+ }
+ destroyDrawingCache();
+ return result;
+ }
+
+ public boolean isFingerEnable() {
+ return isFingerEnable;
+ }
+
+ public void setFingerEnable(boolean fingerEnable) {
+ isFingerEnable = fingerEnable;
+ }
+
+ public boolean isEraser() {
+ return isEraser;
+ }
+
+ public boolean canUndo() {
+ return mCanUndo;
+ }
+
+ public boolean canRedo() {
+ return mCanRedo;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ /**
+ * 图片大小调整适配画布宽高
+ *
+ * @param bitmap 源图
+ * @param width 新宽度
+ * @param height 新高度
+ */
+ public void resize(Bitmap bitmap, int width, int height) {
+
+ if (mBitmap != null) {
+ if (width >= this.mWidth) {
+ height = width * mBitmap.getHeight() / mBitmap.getWidth();
+ }
+ this.mWidth = width;
+ this.mHeight = height;
+
+ mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
+ restoreLastBitmap(bitmap, mBitmap);
+ initCanvas();
+ if (mStepOperation != null) {
+ mStepOperation.addBitmap(mBitmap);
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * 恢复最后画的bitmap
+ *
+ * @param srcBitmap 最后的bitmap
+ * @param newBitmap 新bitmap
+ */
+ private void restoreLastBitmap(Bitmap srcBitmap, Bitmap newBitmap) {
+ try {
+ if (srcBitmap == null || srcBitmap.isRecycled()) {
+ return;
+ }
+ srcBitmap = BitmapUtil.zoomImg(srcBitmap, newBitmap.getWidth());
+ //缩放后如果还是超出新图宽高,继续缩放
+ if (srcBitmap.getWidth() > newBitmap.getWidth() || srcBitmap.getHeight() > newBitmap.getHeight()) {
+ srcBitmap = BitmapUtil.zoomImage(srcBitmap, newBitmap.getWidth(), newBitmap.getHeight());
+ }
+ //保存所有的像素的数组,图片宽×高
+ int[] pixels = new int[srcBitmap.getWidth() * srcBitmap.getHeight()];
+ srcBitmap.getPixels(pixels, 0, srcBitmap.getWidth(), 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight());
+ newBitmap.setPixels(pixels, 0, srcBitmap.getWidth(), 0, 0,
+ srcBitmap.getWidth(), srcBitmap.getHeight());
+ } catch (OutOfMemoryError e) {
+ }
+
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = onMeasureR(0, widthMeasureSpec);
+ int height = onMeasureR(1, heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * 计算控件宽高
+ */
+ public int onMeasureR(int attr, int oldMeasure) {
+
+ int newSize = 0;
+ int mode = MeasureSpec.getMode(oldMeasure);
+ int oldSize = MeasureSpec.getSize(oldMeasure);
+
+ switch (mode) {
+ case MeasureSpec.EXACTLY:
+ newSize = oldSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.UNSPECIFIED:
+
+ float value = mWidth;
+
+ if (attr == 0) {
+ if (mBitmap != null) {
+ value = mBitmap.getWidth();
+ }
+ // 控件的宽度
+ newSize = (int) (getPaddingLeft() + value + getPaddingRight());
+
+ } else if (attr == 1) {
+ value = mHeight;
+ if (mBitmap != null) {
+ value = mBitmap.getHeight();
+ }
+ // 控件的高度
+ newSize = (int) (getPaddingTop() + value + getPaddingBottom());
+ }
+ break;
+ default:
+ break;
+ }
+ return newSize;
+ }
+
+ public Bitmap getLastBitmap() {
+ return mBitmap;
+ }
+
+}
diff --git a/servicing/src/main/java/com/za/ui/camera/CameraXPreviewViewTouchListener.java b/servicing/src/main/java/com/za/ui/camera/CameraXPreviewViewTouchListener.java
new file mode 100644
index 0000000..d8cc62e
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/camera/CameraXPreviewViewTouchListener.java
@@ -0,0 +1,103 @@
+package com.za.ui.camera;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+public class CameraXPreviewViewTouchListener implements View.OnTouchListener {
+
+ private final GestureDetector mGestureDetector;
+
+ private final ScaleGestureDetector mScaleGestureDetector;
+
+ public CameraXPreviewViewTouchListener(Context context) {
+ mGestureDetector = new GestureDetector(context, onGestureListener);
+ mScaleGestureDetector = new ScaleGestureDetector(context, onScaleGestureListener);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ mScaleGestureDetector.onTouchEvent(event);
+ if (!mScaleGestureDetector.isInProgress()) {
+ mGestureDetector.onTouchEvent(event);
+ }
+ return true;
+ }
+
+ /**
+ * 缩放监听
+ */
+ ScaleGestureDetector.OnScaleGestureListener onScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ float delta = detector.getScaleFactor();
+ if (mCustomTouchListener != null) {
+ mCustomTouchListener.zoom(delta);
+ }
+ return true;
+ }
+ };
+
+
+ GestureDetector.SimpleOnGestureListener onGestureListener = new GestureDetector.SimpleOnGestureListener() {
+
+ @Override
+ public boolean onDown(@NonNull MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(@NonNull MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mCustomTouchListener != null) {
+ mCustomTouchListener.click(e);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mCustomTouchListener != null) {
+ mCustomTouchListener.doubleClick(e.getX(), e.getY());
+ }
+ return true;
+ }
+ };
+
+
+ private CustomTouchListener mCustomTouchListener;
+
+ public interface CustomTouchListener {
+ /**
+ * 放大
+ */
+ void zoom(float delta);
+
+ /**
+ * 点击
+ */
+ void click(MotionEvent event);
+
+ /**
+ * 双击
+ */
+ void doubleClick(float x, float y);
+ }
+
+ public void setCustomTouchListener(CustomTouchListener customTouchListener) {
+ mCustomTouchListener = customTouchListener;
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/camera/TakePhotoButtonView.java b/servicing/src/main/java/com/za/ui/camera/TakePhotoButtonView.java
new file mode 100644
index 0000000..ae59d60
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/camera/TakePhotoButtonView.java
@@ -0,0 +1,114 @@
+package com.za.ui.camera;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.za.common.log.LogUtil;
+
+
+/**
+ *
+ */
+public class TakePhotoButtonView extends View {
+ private Paint innerPaint;
+ private Paint outPaint;
+ private Paint animationPaint;
+ private float angle;
+ private ValueAnimator angleAnimation;
+ private int mWidth;
+ private int mHeight;
+ private RectF rectF;
+
+ public TakePhotoButtonView(Context context) {
+ super(context);
+ }
+
+ public TakePhotoButtonView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ innerPaint = new Paint();
+ innerPaint.setAntiAlias(true);
+ innerPaint.setColor(Color.WHITE);
+
+ outPaint = new Paint();
+ outPaint.setAntiAlias(true);
+ outPaint.setColor(Color.GRAY);
+
+ animationPaint = new Paint();
+ animationPaint.setAntiAlias(true);
+ animationPaint.setColor(Color.GREEN);
+ angleAnimation = ValueAnimator.ofFloat(0f, 360f);
+ angleAnimation.setDuration(1000)
+ .setRepeatCount(-1);
+ angleAnimation.addUpdateListener(animation -> {
+ angle = (Float) animation.getAnimatedValue();
+ invalidate();
+ });
+
+ rectF = new RectF(0f, 0f, 0f, 0f);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mWidth = getWidth();
+ mHeight = getHeight();
+
+ rectF.left = -mHeight / 2f;
+ rectF.top = -mHeight / 2f;
+ rectF.right = mWidth / 2f;
+ rectF.bottom = mHeight / 2f;
+ }
+
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.translate(mWidth / 2f, mHeight / 2f);
+ canvas.drawCircle(0f, 0f, mWidth / 2f, outPaint);
+ canvas.drawArc(rectF, 270f, angle, true, animationPaint);
+ canvas.drawCircle(0f, 0f, mWidth / 2f - 40f, innerPaint);
+ }
+
+ /**
+ * 开始动画
+ */
+ public void startAnimation() {
+ angleAnimation.start();
+ }
+
+ /**
+ * 暂停动画
+ */
+ public void cancelAnimation() {
+ try {
+ if (angleAnimation == null) {
+ return;
+ }
+ angleAnimation.setCurrentFraction(0f);
+ angleAnimation.pause();
+ } catch (Exception e) {
+ LogUtil.INSTANCE.print("cancelAnimation", e);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ angleAnimation.cancel();
+ angleAnimation.removeAllUpdateListeners();
+ angleAnimation = null;
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/camera/ZdCameraXActivity.kt b/servicing/src/main/java/com/za/ui/camera/ZdCameraXActivity.kt
new file mode 100644
index 0000000..5baaced
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/camera/ZdCameraXActivity.kt
@@ -0,0 +1,521 @@
+package com.za.ui.camera
+
+import android.Manifest
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.ContentValues
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.location.Location
+import android.location.LocationManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.OrientationEventListener
+import android.view.SoundEffectConstants
+import android.view.Surface
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.PopupWindow
+import android.widget.SeekBar
+import android.widget.SeekBar.OnSeekBarChangeListener
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatSeekBar
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ExposureState
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.Preview
+import androidx.camera.core.ZoomState
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.constraintlayout.utils.widget.ImageFilterView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Group
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.exifinterface.media.ExifInterface
+import androidx.lifecycle.LiveData
+import com.blankj.utilcode.util.ThreadUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.blankj.utilcode.util.UriUtils
+import com.bumptech.glide.Glide
+import com.permissionx.guolindev.PermissionX
+import com.za.common.log.LogUtil.print
+import com.za.common.util.ClickProxy
+import com.za.servicing.R
+import com.za.ui.camera.CameraXPreviewViewTouchListener.CustomTouchListener
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class ZdCameraXActivity : AppCompatActivity() {
+ private var exposurePopupWindow: PopupWindow? = null
+
+ internal enum class FlashMode {
+ auto, open, light, close
+ }
+
+ private var rootView: ConstraintLayout? = null
+ private var imageCapture: ImageCapture? = null
+ private var cameraInfo: CameraInfo? = null
+
+ private var exposureState: ExposureState? = null
+ private var cameraExecutor: ExecutorService? = null
+ private var zoomState: LiveData? = null
+ private var viewFinder: PreviewView? = null
+ private var takePhoto: TakePhotoButtonView? = null
+ private var ivFlash: ImageView? = null
+ private var ivChangeCamera: ImageView? = null
+ private var ivConfirm: ImageView? = null
+ private var ivCancel: ImageView? = null
+ private lateinit var ivPreview: ImageFilterView
+ private var ivBack: ImageView? = null
+ private var sliderExposure: AppCompatSeekBar? = null
+ private var groupOperation: Group? = null
+ private var groupPreview: Group? = null
+ private var uri: Uri? = null
+ private var animatorSet: AnimatorSet? = null
+ private var location: Location? = null
+ private var camera: Camera? = null
+ private var isBack = true
+
+ private var orientationEventListener: OrientationEventListener? = null
+ private var flashPopWindow: PopupWindow? = null
+ private var flashMode = FlashMode.close
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_zd_camera_xactivity)
+ initView()
+ initLocation()
+ startCamera()
+ setOnClick()
+
+ orientationEventListener = object : OrientationEventListener(this) {
+ override fun onOrientationChanged(orientation: Int) {
+ if (orientation == ORIENTATION_UNKNOWN || imageCapture == null) {
+ print("zdCamerax orientation", "orientation==$orientation")
+ return
+ }
+ when (orientation) {
+ in 45..134 -> {
+ imageCapture?.targetRotation = Surface.ROTATION_270
+ }
+ in 135..224 -> {
+ imageCapture?.targetRotation = Surface.ROTATION_180
+ }
+ in 225..314 -> {
+ imageCapture?.targetRotation = Surface.ROTATION_90
+ }
+ else -> {
+ imageCapture?.targetRotation = Surface.ROTATION_0
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("ObjectAnimatorBinding")
+ private fun initView() {
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ rootView = findViewById(R.id.viewGroup_parent)
+ viewFinder = findViewById(R.id.viewFinder)
+ takePhoto = findViewById(R.id.iv_takePhoto)
+ ivConfirm = findViewById(R.id.iv_confirm)
+ ivCancel = findViewById(R.id.iv_cancel)
+ ivBack = findViewById(R.id.iv_back)
+ sliderExposure = findViewById(R.id.slider_exposureState)
+ groupOperation = findViewById(R.id.group_opera)
+ groupPreview = findViewById(R.id.group_preview)
+ groupPreview?.visibility = View.GONE
+ ivChangeCamera = findViewById(R.id.iv_changeCamera)
+ ivPreview = findViewById(R.id.iv_preview)
+ ivFlash = findViewById(R.id.iv_flash)
+ animatorSet = AnimatorSet()
+ animatorSet?.playTogether(ObjectAnimator.ofFloat(ivCancel, "translationX", 0f, -200f), ObjectAnimator.ofFloat(ivConfirm, "translationX", 200f))
+
+ animatorSet?.setDuration(500)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun setOnClick() {
+ takePhoto?.setOnClickListener(ClickProxy { v: View? -> takePhoto() })
+
+ ivChangeCamera?.setOnClickListener(ClickProxy { v: View? ->
+ isBack = !isBack
+ if (!isBack) {
+ if (flashPopWindow != null && flashPopWindow?.isShowing == true) {
+ flashPopWindow?.dismiss()
+ }
+ flashMode = FlashMode.close
+ ivFlash?.setImageResource(R.drawable.picture_ic_flash_off)
+ }
+ startCamera()
+ })
+
+ ivFlash?.setOnClickListener {
+ if (!isBack) {
+ ToastUtils.showShort("前置模式下无法开启!")
+ return@setOnClickListener
+ }
+ if (flashPopWindow != null && flashPopWindow?.isShowing==true) {
+ flashPopWindow?.dismiss()
+ } else {
+ showFlashView()
+ }
+ }
+
+ ivCancel?.setOnClickListener {
+ groupPreview?.visibility = View.GONE
+ groupOperation?.visibility = View.VISIBLE
+ }
+
+ ivConfirm?.setOnClickListener {
+ val intent = Intent()
+ if (uri != null) {
+ intent.putExtra("path", UriUtils.uri2File(uri).absolutePath)
+ }
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+
+ ivBack?.setOnClickListener { finish() }
+
+ sliderExposure?.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
+ @SuppressLint("SetTextI18n")
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (exposureState != null) {
+ camera?.cameraControl?.setExposureCompensationIndex(Math.round(progress.toFloat()))
+ if (exposurePopupWindow != null) {
+ val textView = exposurePopupWindow?.contentView?.findViewWithTag("tv_exposureView")
+ textView?.text = progress.toString() + ""
+ }
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {
+ showExposureView()
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ viewFinder?.postDelayed({ exposurePopupWindow?.dismiss() }, 1000)
+ }
+ })
+
+ val cameraXPreviewViewTouchListener = CameraXPreviewViewTouchListener(this)
+ viewFinder?.setOnTouchListener(cameraXPreviewViewTouchListener)
+ cameraXPreviewViewTouchListener.setCustomTouchListener(object : CustomTouchListener {
+ override fun zoom(delta: Float) {
+ if (zoomState == null) {
+ return
+ }
+ val currentZoomRatio = zoomState?.value?.zoomRatio
+ currentZoomRatio?.let {
+ camera?.cameraControl?.setZoomRatio(it.times(delta))
+ }
+
+ }
+
+ override fun click(event: MotionEvent) {
+ val action = viewFinder?.meteringPointFactory?.createPoint(event.x, event.y)?.let { FocusMeteringAction.Builder(it).build() }
+ rootView?.post { showTapView(event.rawX.toInt(), event.rawY.toInt()) }
+ action?.let {
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+ }
+
+ override fun doubleClick(x: Float, y: Float) {
+ if (zoomState == null) {
+ return
+ }
+ val currentZoomRatio = zoomState?.value?.zoomRatio
+ zoomState?.value?.minZoomRatio?.let {
+ if (currentZoomRatio!! > it) {
+ camera?.cameraControl?.setLinearZoom(0f)
+ } else {
+ camera?.cameraControl?.setLinearZoom(0.5f)
+ }
+ }
+
+ }
+ })
+ }
+
+ private fun takePhoto() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ThreadUtils.runOnUiThread {
+ PermissionX.init(this@ZdCameraXActivity).permissions(Manifest.permission.ACCESS_MEDIA_LOCATION)
+ .request { allGranted: Boolean, grantedList: List?, deniedList: List? ->
+ print("ZDCamerax ACCESS_MEDIA_LOCATION 权限请求结果", "allGranted==$allGranted")
+ if (!allGranted) {
+ ToastUtils.showLong("权限获取失败")
+ finish()
+ }
+ }
+ }
+ }
+ if (imageCapture == null) {
+ ToastUtils.showShort("相机打开失败")
+ finish()
+ }
+// val filename = TimeUtils.date2String(Date(), "yyyyMMddHHmmss")
+// val contentValues = ContentValues()
+// contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, filename)
+// contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg")
+// contentValues.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis())
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+// contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
+// } else {
+// ImageUtils.save2Album()
+// contentValues.put(MediaStore.Images.ImageColumns.DATA, getTakePictureParentPath() + File.separator + filename + ".jpg")
+// }
+//
+// val outputOptions = ImageCapture.OutputFileOptions.Builder(contentResolver,
+// MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues).build()
+
+
+
+ // Create time stamped name and MediaStore entry.
+ val filename = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault())
+ .format(System.currentTimeMillis())
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/中道救援")
+ }
+ }
+
+ // Create output options object which contains file + metadata
+ val outputOptions = ImageCapture.OutputFileOptions
+ .Builder(contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ .build()
+
+
+ takePhoto?.startAnimation()
+
+ imageCapture?.takePicture(outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ this@ZdCameraXActivity.uri = outputFileResults.savedUri
+ //保存照片信息
+ val exifInterface: ExifInterface
+ try {
+ exifInterface = ExifInterface(UriUtils.uri2File(outputFileResults.savedUri).absolutePath)
+ exifInterface.setAttribute(ExifInterface.TAG_DATETIME, filename)
+ if (location != null) {
+ exifInterface.setGpsInfo(location)
+ if (location?.latitude!! > 0f && location?.longitude != null && location?.longitude!! > 0f) {
+ exifInterface.setLatLong(location?.latitude!!, location?.longitude!!)
+ }
+ }
+
+ exifInterface.saveAttributes()
+ takePhoto?.cancelAnimation()
+ groupPreview?.visibility = View.VISIBLE
+ if (!this@ZdCameraXActivity.isFinishing) {
+ Glide.with(this@ZdCameraXActivity).load(outputFileResults.savedUri).into(ivPreview)
+ }
+ groupOperation?.visibility = View.GONE
+ animatorSet?.start()
+ } catch (e: IOException) {
+ takePhoto?.cancelAnimation()
+ ToastUtils.showShort("照片保存失败" + e.message)
+ print("照片保存失败", e)
+ }
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ takePhoto?.cancelAnimation()
+ ToastUtils.showShort("Photo capture failed" + exception.message)
+ print("onCameraError", exception)
+ }
+ })
+ }
+
+ private fun startCamera() {
+ imageCapture = ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY).build()
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+ cameraProviderFuture.addListener({
+ try {
+ val cameraProvider = cameraProviderFuture.get()
+ val preview = Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+
+ preview.setSurfaceProvider(viewFinder?.surfaceProvider)
+ val cameraSelector = if (isBack) CameraSelector.DEFAULT_BACK_CAMERA else CameraSelector.DEFAULT_FRONT_CAMERA
+ preview.targetRotation = Surface.ROTATION_0
+ cameraProvider.unbindAll()
+ camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
+ cameraInfo = camera?.cameraInfo
+ zoomState = cameraInfo?.zoomState
+ exposureState = cameraInfo?.exposureState
+ if (exposureState?.isExposureCompensationSupported == true) {
+ sliderExposure?.visibility = View.VISIBLE
+ sliderExposure?.max = exposureState?.exposureCompensationRange?.upper ?: 1
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ sliderExposure?.min = exposureState?.exposureCompensationRange?.lower ?: 1
+ }
+ } else {
+ sliderExposure?.visibility = View.GONE
+ }
+ } catch (e: ExecutionException) {
+ finish()
+ print("相机初始化失败", e)
+ } catch (e: InterruptedException) {
+ finish()
+ print("相机初始化失败", e)
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun initLocation() {
+ val locationManager = ContextCompat.getSystemService(this, LocationManager::class.java)
+ if ((ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA), 102)
+ } else {
+ if (locationManager != null) {
+ location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ }
+ }
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private fun showTapView(x: Int, y: Int) {
+ if (takePhoto?.visibility == View.GONE) {
+ return
+ }
+ try {
+ val popupWindow = PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ popupWindow.width = 200
+ popupWindow.height = 200
+ val imageView = ImageView(this)
+ imageView.setImageResource(R.drawable.focus_focusing)
+ imageView.scaleType = ImageView.ScaleType.CENTER_CROP
+ popupWindow.contentView = imageView
+ popupWindow.showAtLocation(rootView, Gravity.LEFT or Gravity.TOP, x, y)
+ viewFinder?.postDelayed({ popupWindow.dismiss() }, 1000)
+ viewFinder?.playSoundEffect(SoundEffectConstants.CLICK)
+ } catch (e: Exception) {
+ print("zdCameraX showTapView", e)
+ }
+ }
+
+ @SuppressLint("RtlHardcoded", "SetTextI18n")
+ private fun showExposureView() {
+ if (takePhoto?.visibility == View.GONE) {
+ return
+ }
+ exposurePopupWindow = PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ exposurePopupWindow?.width = 300
+ exposurePopupWindow?.height = 200
+ val textView = TextView(this)
+ textView.setTextColor(Color.YELLOW)
+ textView.textSize = 50f
+ textView.tag = "tv_exposureView"
+ textView.gravity = Gravity.CENTER
+ textView.text = exposureState?.exposureCompensationIndex.toString() + ""
+ exposurePopupWindow?.contentView = textView
+ exposurePopupWindow?.showAtLocation(rootView, Gravity.CENTER, 0, 0)
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private fun showFlashView() {
+ if (flashPopWindow != null) {
+ flashPopWindow = null
+ }
+ flashPopWindow = PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ @SuppressLint("InflateParams") val view = LayoutInflater.from(this).inflate(R.layout.item_flash_mode, null)
+ val tvFlashAuto = view.findViewById(R.id.tvFlashAuto)
+ val tvFlashOpen = view.findViewById(R.id.tvFlashOpen)
+ val tvFlashLight = view.findViewById(R.id.tvFlashLight)
+ val tvFlashClose = view.findViewById(R.id.tvFlashClose)
+
+ when (flashMode) {
+ FlashMode.auto -> tvFlashAuto.setTextColor(Color.YELLOW)
+ FlashMode.open -> tvFlashOpen.setTextColor(Color.YELLOW)
+ FlashMode.light -> tvFlashLight.setTextColor(Color.YELLOW)
+ FlashMode.close -> tvFlashClose.setTextColor(Color.YELLOW)
+ }
+ tvFlashAuto.setOnClickListener { v: View? ->
+ if (imageCapture != null) {
+ camera?.cameraControl?.enableTorch(false)
+ imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO
+ ivFlash?.setImageResource(R.drawable.picture_ic_flash_auto)
+ flashMode = FlashMode.auto
+ }
+ flashPopWindow?.dismiss()
+ }
+ tvFlashOpen.setOnClickListener { v: View? ->
+ if (imageCapture != null) {
+ camera?.cameraControl?.enableTorch(false)
+ imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
+ ivFlash?.setImageResource(R.drawable.picture_ic_flash_on)
+ flashMode = FlashMode.open
+ }
+ flashPopWindow?.dismiss()
+ }
+
+ tvFlashLight.setOnClickListener { v: View? ->
+ if (camera != null) {
+ camera?.cameraControl?.enableTorch(true)
+ ivFlash?.setImageResource(R.drawable.ic_flash_light)
+ flashMode = FlashMode.light
+ }
+ flashPopWindow?.dismiss()
+ }
+
+ tvFlashClose.setOnClickListener { v: View? ->
+ if (imageCapture != null) {
+ camera?.cameraControl?.enableTorch(false)
+ imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
+ ivFlash?.setImageResource(R.drawable.picture_ic_flash_off)
+ flashMode = FlashMode.close
+ }
+ flashPopWindow?.dismiss()
+ }
+
+ flashPopWindow?.contentView = view
+ flashPopWindow?.showAtLocation(ivFlash, Gravity.LEFT or Gravity.TOP, 0, ivFlash?.y?.toInt()?.minus(view.height)
+ ?: 0)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (camera != null && imageCapture != null) {
+ camera?.cameraControl?.enableTorch(false)
+ imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
+ ivFlash?.setImageResource(R.drawable.picture_ic_flash_off)
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ orientationEventListener?.enable()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ orientationEventListener?.disable()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cameraExecutor?.shutdown()
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/h5/CommonH5Activity.kt b/servicing/src/main/java/com/za/ui/h5/CommonH5Activity.kt
new file mode 100644
index 0000000..3b0d58c
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/h5/CommonH5Activity.kt
@@ -0,0 +1,275 @@
+package com.za.ui.h5
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.view.ViewGroup
+import android.webkit.JavascriptInterface
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.blankj.utilcode.util.ToastUtils
+import com.tencent.smtt.sdk.WebChromeClient
+import com.tencent.smtt.sdk.WebSettings
+import com.tencent.smtt.sdk.WebView
+import com.tencent.smtt.sdk.WebView.setWebContentsDebuggingEnabled
+import com.tencent.smtt.sdk.WebViewClient
+import com.za.base.AppConfig
+import com.za.base.BaseActivity
+import com.za.base.theme.headPadding
+import com.za.base.view.HeadView
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.finish
+
+class CommonH5Activity : BaseActivity() {
+ private var webView : WebView? = null
+ private var isDestroyed = false
+
+ @Composable
+ override fun ContentView() {
+ val url = intent.getStringExtra(EXTRA_URL)
+ val title = intent.getStringExtra(EXTRA_TITLE)
+ val isCanBack = intent.getBooleanExtra(EXTRA_CAN_BACK, true)
+
+ if (url.isNullOrBlank()) {
+ ToastUtils.showLong("无效的URL")
+ finish()
+ return
+ }
+
+ CommonH5Screen(url = url,
+ title = title ?: "",
+ isCanBack = isCanBack,
+ onWebViewCreated = { web ->
+ if (! isDestroyed) {
+ webView = web
+ setupJavascriptInterface(web)
+ }
+ })
+ }
+
+ private fun setupJavascriptInterface(webView : WebView) {
+ webView.addJavascriptInterface(JavaScriptInterface(), "android")
+ }
+
+ private inner class JavaScriptInterface {
+ @JavascriptInterface
+ fun sendMessage(message : String) {
+ LogUtil.print("commonH5 message", message)
+ runOnUiThread {
+ handleJavascriptMessage(message)
+ }
+ }
+ }
+
+ private fun handleJavascriptMessage(message : String) {
+ if (isDestroyed) return
+ when {
+ message == "goBack" -> finish()
+ message.contains("articleId") -> handleArticleId(message)
+ }
+ }
+
+ private fun handleArticleId(message : String) {
+ val id = message.split("=").lastOrNull()?.takeIf { it.isNotEmpty() } ?: run {
+ LogUtil.print("articleId error", "Invalid article ID format")
+ return
+ }
+
+ val url = buildTrainingUrl(id)
+ webView?.loadUrl(url) // goH5Activity(this, url, title = "")
+ }
+
+ private fun buildTrainingUrl(articleId : String) = buildString {
+ append(AppConfig.Resource_URL)
+ append("/training/training.html")
+ append("?id=").append(articleId)
+ append("&source=driverApp")
+ append("&driverId=").append(GlobalData.driverInfo?.userId)
+ append("&userId=").append(GlobalData.driverInfo?.userId)
+ }
+
+ override fun onDestroy() {
+ isDestroyed = true
+ cleanupWebView()
+ super.onDestroy()
+ }
+
+ private fun cleanupWebView() {
+ webView?.apply {
+ loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
+ clearHistory()
+ clearCache(true)
+ (parent as? ViewGroup)?.removeView(this)
+ webChromeClient = null
+ webViewClient = null
+ destroy()
+ }
+ webView = null
+ }
+
+ companion object {
+ private const val EXTRA_URL = "url"
+ private const val EXTRA_TITLE = "title"
+ private const val EXTRA_CAN_BACK = "isCanBack"
+ private const val DEFAULT_TIMEOUT = 15000
+
+ fun goH5Activity(context : Context,
+ url : String?,
+ title : String,
+ isCanBack : Boolean = true) {
+ if (url.isNullOrBlank()) {
+ ToastUtils.showLong("路径为空!!")
+ return
+ }
+ val intent = Intent(context, CommonH5Activity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(EXTRA_URL, url)
+ putExtra(EXTRA_TITLE, title)
+ putExtra(EXTRA_CAN_BACK, isCanBack)
+ }
+ context.startActivity(intent)
+ }
+ }
+}
+
+@Composable
+private fun CommonH5Screen(url : String,
+ title : String,
+ isCanBack : Boolean,
+ onWebViewCreated : (WebView) -> Unit) {
+ val context = LocalContext.current
+ var progress by remember { mutableFloatStateOf(0f) }
+ var isError by remember { mutableStateOf(false) }
+ var webView by remember { mutableStateOf(null) }
+
+ BackHandler(enabled = isCanBack || webView?.canGoBack() == true) {
+ handleBackPress(context, webView)
+ }
+
+ Scaffold(topBar = {
+ if (title.isNotBlank()) {
+ HeadView(title = title, onBack = { handleBackPress(context, webView) })
+ } else {
+ Spacer(modifier = Modifier
+ .fillMaxWidth()
+ .statusBarsPadding()
+ .padding(vertical = headPadding))
+ }
+ }) { paddingValues ->
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)) {
+ AndroidView(modifier = Modifier.fillMaxSize(), factory = { ctx ->
+ WebView(ctx).apply {
+ setupWebView(this, context)
+ setupWebViewClients(webView = this,
+ onProgressChanged = { progress = it / 100f },
+ onError = { _, desc ->
+ isError = true
+ ToastUtils.showLong("加载失败:$desc")
+ })
+ webView = this
+ onWebViewCreated(this)
+ loadUrl(url)
+ }
+ })
+
+ if (progress < 1f && ! isError) {
+ LinearProgressIndicator(progress = { progress },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(2.dp))
+ }
+ }
+ }
+}
+
+private fun setupWebViewClients(webView : WebView,
+ onProgressChanged : (Int) -> Unit,
+ onError : (Int, String?) -> Unit) {
+ webView.webChromeClient = object : WebChromeClient() {
+ override fun onProgressChanged(view : WebView?, newProgress : Int) {
+ onProgressChanged(newProgress)
+ }
+ }
+
+ webView.webViewClient = object : WebViewClient() {
+ override fun onReceivedError(view : WebView?,
+ errorCode : Int,
+ description : String?,
+ failingUrl : String?) {
+ onError(errorCode, description)
+ }
+
+ override fun onPageFinished(view : WebView?, url : String?) {
+ super.onPageFinished(view, url)
+ view?.settings?.blockNetworkImage = false
+ }
+
+ override fun shouldOverrideUrlLoading(p0 : WebView?, p1 : String?) : Boolean {
+ p0?.loadUrl(p1)
+ return false
+ }
+ }
+}
+
+private fun setupWebView(webView : WebView, context : Context) {
+ webView.settings.apply {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ allowFileAccess = true
+
+ setAppCacheEnabled(true)
+ setAppCacheMaxSize(Long.MAX_VALUE)
+ setAppCachePath(context.getDir("appcache", 0).path)
+ cacheMode = WebSettings.LOAD_DEFAULT
+
+ layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
+ useWideViewPort = true
+ loadWithOverviewMode = true
+ defaultFontSize = 16
+ defaultTextEncodingName = "utf-8"
+
+ setSupportZoom(true)
+ builtInZoomControls = true
+ displayZoomControls = false
+
+ blockNetworkImage = true
+ mixedContentMode = 0
+
+ setSupportMultipleWindows(false)
+ setGeolocationEnabled(true)
+ databasePath = context.getDir("databases", 0).path
+
+ setWebContentsDebuggingEnabled(! AppConfig.isRelease)
+ }
+
+ webView.setBackgroundColor(Color.TRANSPARENT)
+}
+
+private fun handleBackPress(context : Context, webView : WebView?) {
+ if (webView?.canGoBack() == true) {
+ webView.goBack()
+ } else {
+ context.finish()
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/main/ServiceLauncherActivity.kt b/servicing/src/main/java/com/za/ui/main/ServiceLauncherActivity.kt
new file mode 100644
index 0000000..de50dda
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/main/ServiceLauncherActivity.kt
@@ -0,0 +1,229 @@
+package com.za.ui.main
+
+import android.Manifest
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import coil.compose.AsyncImage
+import com.blankj.utilcode.util.ToastUtils
+import com.permissionx.guolindev.PermissionX
+import com.za.base.BaseActivity
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.goStatusPage
+import com.za.service.ServiceManager
+import com.za.service.location.ZdLocationManager
+import com.za.servicing.R
+
+class ServiceLauncherActivity : BaseActivity() {
+
+ // 添加设置页面返回结果处理
+ private val settingsLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { // 从设置页面返回后,重新检查权限并尝试导航
+ checkPermissionsAndNavigate(true)
+ }
+
+ @Composable
+ override fun ContentView() {
+ LaunchedEffect(key1 = Unit) {
+ checkPermissionsAndNavigate()
+ }
+ LauncherScreen()
+ }
+
+ private fun checkPermissionsAndNavigate(fromSettings : Boolean = false) {
+ val permissions = getRequiredPermissions()
+
+ // 首先检查是否已经拥有所有基础权限
+ if (permissions.all { PermissionX.isGranted(this, it) }) { // 检查后台定位权限
+ checkBackgroundLocation(fromSettings)
+ return
+ }
+
+ // 请求基础权限
+ PermissionX.init(this).permissions(permissions)
+ .onExplainRequestReason { scope, deniedList ->
+ val message = buildPermissionMessage(deniedList)
+ scope.showRequestReasonDialog(deniedList, message, "确定", "取消")
+ }.onForwardToSettings { scope, deniedList ->
+ scope.showForwardToSettingsDialog(deniedList,
+ "您需要在设置中手动授予所有权限才能使用应用",
+ "去设置",
+ "取消")
+ }.request { allGranted, _, _ ->
+ if (allGranted) { // 基础权限获取成功后,请求后台定位权限
+ checkBackgroundLocation(fromSettings)
+ } else {
+ ToastUtils.showLong("请授予所有必需的权限才能使用应用")
+ finish()
+ }
+ }
+ }
+
+ private fun checkBackgroundLocation(fromSettings : Boolean = false) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 检查是否有前台定位权限
+ val hasForegroundLocation =
+ PermissionX.isGranted(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ val hasBackgroundLocation =
+ PermissionX.isGranted(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
+
+ if (hasForegroundLocation && hasBackgroundLocation) {
+ navigateToNextScreen()
+ return
+ }
+
+ // 如果是从设置页面返回,且已经有前台权限,直接导航(即使没有后台权限也允许使用)
+ if (fromSettings && hasForegroundLocation) {
+ navigateToNextScreen()
+ return
+ }
+
+ // 如果已经有前台权限但没有后台权限,直接跳转到设置页面
+ if (hasForegroundLocation) {
+ openAppSettings("请在设置中将位置权限设置为【始终允许】")
+ return
+ }
+
+ // 如果连前台权限都没有,先请求前台权限
+ PermissionX.init(this).permissions(Manifest.permission.ACCESS_FINE_LOCATION)
+ .onExplainRequestReason { scope, deniedList ->
+ scope.showRequestReasonDialog(deniedList,
+ "需要位置权限来确定您的当前位置,以便为您提供准确的服务",
+ "确定",
+ "取消")
+ }.request { allGranted, _, _ ->
+ if (allGranted) { // 获得前台权限后,引导用户去设置页面开启后台权限
+ openAppSettings("请在设置中将位置权限设置为【始终允许】")
+ } else {
+ ToastUtils.showLong("需要位置权限才能使用应用")
+ finish()
+ }
+ }
+ } else { // Android 10以下版本不需要特别申请后台定位权限
+ navigateToNextScreen()
+ }
+ }
+
+ private fun openAppSettings(message : String) {
+ ToastUtils.showLong(message)
+ try {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", packageName, null)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ settingsLauncher.launch(intent)
+ } catch (e : Exception) {
+ ToastUtils.showLong("无法打开应用设置页面,请手动前往设置")
+ finish()
+ }
+ }
+
+ private fun buildPermissionMessage(permissions : List) : String {
+ return when {
+ permissions.contains(Manifest.permission.ACCESS_FINE_LOCATION) -> "需要位置权限来确定您的当前位置,以便为您提供准确的服务"
+
+ permissions.contains(Manifest.permission.CAMERA) -> "需要相机权限来拍摄照片和扫描二维码"
+
+ permissions.contains(Manifest.permission.RECORD_AUDIO) -> "需要录音权限来进行语音通话"
+
+ permissions.any {
+ it in listOf(Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO)
+ } -> "需要存储权限来保存必要的文件和图片"
+
+ else -> "这些权限是应用正常运行所必需的"
+ }
+ }
+
+ private fun getRequiredPermissions() : List = buildList { // 基础权限
+ add(Manifest.permission.ACCESS_FINE_LOCATION)
+ add(Manifest.permission.CAMERA)
+ add(Manifest.permission.CALL_PHONE)
+ add(Manifest.permission.RECORD_AUDIO)
+ add(Manifest.permission.READ_CALL_LOG)
+
+ // 存储权限适配优化
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ add(Manifest.permission.POST_NOTIFICATIONS)
+ add(Manifest.permission.READ_MEDIA_IMAGES)
+ add(Manifest.permission.READ_MEDIA_VIDEO)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ 使用分区存储,不再需要 WRITE_EXTERNAL_STORAGE
+ add(Manifest.permission.READ_EXTERNAL_STORAGE)
+ add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ } else { // Android 9以下保留完整存储权限
+ add(Manifest.permission.READ_EXTERNAL_STORAGE)
+ add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ }
+
+ private fun navigateToNextScreen() {
+ val driverName = intent.getStringExtra("driverName")
+ val driverPhone = intent.getStringExtra("driverPhone")
+ val taskCode = intent.getStringExtra("taskCode")
+ val rescueVehicle = intent.getStringExtra("rescueVehicle")
+ if (driverName.isNullOrBlank() || driverPhone.isNullOrBlank() || taskCode.isNullOrBlank() || rescueVehicle.isNullOrBlank()) {
+ ToastUtils.showLong("缺少必要参数,请重新启动应用")
+ LogUtil.print("navigateToNextScreen",
+ "driverName=$driverName driverPhone=$driverPhone taskCode=$taskCode rescueVehicle=$rescueVehicle")
+ Log.e("navigateToNextScreen",
+ "driverName=$driverName driverPhone=$driverPhone taskCode=$taskCode rescueVehicle=$rescueVehicle")
+ finish()
+ return
+ }
+
+ if (GlobalData.token.isNullOrBlank()) {
+ ServicingMainActivity.goToMain(
+ this,
+ driverName = driverName,
+ driverPhone = driverPhone,
+ taskCode = taskCode,
+ rescueVehicle = rescueVehicle,
+ )
+ return
+ }
+
+ if (GlobalData.currentOrder != null) {
+ GlobalData.currentOrder?.goStatusPage(this)
+ ServiceManager.initialize(GlobalData.application)
+ ZdLocationManager.startContinuousLocation(GlobalData.application)
+ finish()
+ return
+ }
+
+ ServicingMainActivity.goToMain(
+ this,
+ driverName = driverName,
+ driverPhone = driverPhone,
+ taskCode = taskCode,
+ rescueVehicle = rescueVehicle,
+ )
+ }
+}
+
+@Composable
+private fun LauncherScreen() {
+ Scaffold { paddingValues ->
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center) {
+ AsyncImage(model = R.mipmap.ic_launcher,
+ contentDescription = "Launch Screen Background",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds)
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/main/ServicingMainActivity.kt b/servicing/src/main/java/com/za/ui/main/ServicingMainActivity.kt
new file mode 100644
index 0000000..a05f35d
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/main/ServicingMainActivity.kt
@@ -0,0 +1,87 @@
+package com.za.ui.main
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.blankj.utilcode.util.ActivityUtils
+import com.za.base.BaseActivity
+import com.za.common.GlobalData
+import com.za.common.util.DeviceUtil
+import com.za.ext.finish
+import com.za.ext.goStatusPage
+import com.za.service.ServiceManager
+import com.za.service.location.ZdLocationManager
+import kotlinx.coroutines.launch
+
+class ServicingMainActivity : BaseActivity() {
+
+ @Composable
+ override fun ContentView() {
+ ServicingMainScreen(
+ jobCode = intent.getStringExtra("driverName"),
+ phone = intent.getStringExtra("driverPhone"),
+ taskCode = intent.getStringExtra("taskCode"),
+ deviceId = DeviceUtil.getAndroidId(this),
+ rescueVehicle = intent.getStringExtra("rescueVehicle"),
+ )
+ }
+
+ companion object {
+ fun goToMain(context : Context,
+ driverName : String? = null,
+ driverPhone : String? = null,
+ taskCode : String? = null,
+ rescueVehicle : String? = null) {
+ val intent = Intent(context, ServicingMainActivity::class.java)
+ intent.putExtra("driverName", driverName)
+ intent.putExtra("driverPhone", driverPhone)
+ intent.putExtra("taskCode", taskCode)
+ intent.putExtra("rescueVehicle", rescueVehicle)
+ context.startActivity(intent)
+ context.finish()
+ }
+ }
+}
+
+@Composable
+private fun ServicingMainScreen(jobCode : String? = null,
+ phone : String? = null,
+ taskCode : String? = null,
+ vehicleId : String? = null,
+ deviceId : String? = null,
+ rescueVehicle : String? = null,
+ vm : ServicingMainVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(Unit) {
+ vm.dispatch(ServicingMainVm.Action.Init(jobCode,
+ phone,
+ taskCode,
+ vehicleId,
+ deviceId,
+ rescueVehicle))
+ scope.launch {
+ ServiceManager.initialize(GlobalData.application)
+ ZdLocationManager.startContinuousLocation(GlobalData.application)
+ }
+ }
+
+ if (uiState.value.state == 2) {
+ Box {
+ Text(text = "加载失败")
+ }
+ } else {
+ GlobalData.currentOrder?.goStatusPage(ActivityUtils.getTopActivity())
+ context.finish()
+ }
+
+}
diff --git a/servicing/src/main/java/com/za/ui/main/ServicingMainVm.kt b/servicing/src/main/java/com/za/ui/main/ServicingMainVm.kt
new file mode 100644
index 0000000..a25eec7
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/main/ServicingMainVm.kt
@@ -0,0 +1,107 @@
+package com.za.ui.main
+
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.LoginWithTaskBean
+import com.za.bean.LoginWithTaskRequest
+import com.za.bean.db.order.OrderInfo
+import com.za.common.GlobalData
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class ServicingMainVm : BaseVm() {
+
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init(action.jobCode,
+ action.phone,
+ action.taskCode,
+ action.vehicleId,
+ action.deviceId,
+ action.rescueVehicle)
+ }
+ }
+
+ private fun init(jobCode : String? = null,
+ phone : String? = null,
+ taskCode : String? = null,
+ vehicleId : String? = null,
+ deviceId : String? = null,
+ rescueVehicle : String? = null) {
+ LoadingManager.showLoading()
+ login(jobCode, phone, taskCode, vehicleId, deviceId, rescueVehicle, success = {
+ CommonMethod.queryOrderList(context = ActivityUtils.getTopActivity(),
+ success = { orderInfo : OrderInfo?, orderInfos : List? ->
+ LoadingManager.hideLoading()
+ GlobalData.currentOrder = orderInfo
+ updateState(uiState.value.copy(state = 1))
+ },
+ failed = {
+ updateState(uiState.value.copy(state = 2))
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }, failure = {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(state = 2))
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun login(jobCode : String? = null,
+ phone : String? = null,
+ taskCode : String? = null,
+ vehicleId : String? = null,
+ deviceId : String? = null,
+ rescueVehicle : String? = null,
+ success : () -> Unit,
+ failure : (String) -> Unit) {
+ val loginWithTaskRequest =
+ LoginWithTaskRequest(jobCode, phone, taskCode, rescueVehicle, deviceId, vehicleId)
+ RetrofitHelper.getDefaultService().loginWithTask(loginWithTaskRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : LoginWithTaskBean?) {
+ if (it == null) {
+ ToastUtils.showLong("登录失败")
+ failure("登录失败")
+ return
+ }
+ GlobalData.token = it.token
+ CommonMethod.getGenerateInfo(vehicleId = it.vehicleId,
+ userId = it.userId,
+ success = { success() },
+ failed = { failure(it ?: "") })
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ failure(msg ?: "")
+ }
+ })
+ }
+
+ sealed class Action {
+ data class Init(val jobCode : String? = null,
+ val phone : String? = null,
+ val taskCode : String? = null,
+ val vehicleId : String? = null,
+ val deviceId : String? = null,
+ val rescueVehicle : String? = null) : Action()
+ }
+
+ data class UiState(
+ val state : Int? = null, //1 成功 2 失败
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/map_search/MapSearchActivity.kt b/servicing/src/main/java/com/za/ui/map_search/MapSearchActivity.kt
new file mode 100644
index 0000000..4ba33be
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/map_search/MapSearchActivity.kt
@@ -0,0 +1,344 @@
+package com.za.ui.map_search
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import com.amap.api.location.AMapLocationClient
+import com.amap.api.location.AMapLocationClientOption
+import com.amap.api.maps.CameraUpdateFactory
+import com.amap.api.maps.MapView
+import com.amap.api.maps.model.LatLng
+import com.amap.api.services.core.AMapException
+import com.amap.api.services.core.LatLonPoint
+import com.amap.api.services.core.PoiItem
+import com.amap.api.services.geocoder.GeocodeResult
+import com.amap.api.services.geocoder.GeocodeSearch
+import com.amap.api.services.geocoder.RegeocodeResult
+import com.amap.api.services.poisearch.PoiResult
+import com.amap.api.services.poisearch.PoiSearch
+import com.za.base.BaseActivity
+import com.za.base.view.CommonButton
+import com.za.base.view.HeadView
+import com.za.ext.finish
+import kotlinx.coroutines.launch
+import java.io.Serializable
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class MapSearchActivity : BaseActivity() {
+ @SuppressLint("MissingPermission")
+ @Composable
+ override fun ContentView() {
+ MapSearchScreen(onLocationSelected = ::onLocationSelected)
+ }
+
+ private fun onLocationSelected(poiData: PoiData) {
+ val intent = Intent()
+ intent.putExtra("poiData", poiData)
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+}
+
+@SuppressLint("MissingPermission")
+@Composable
+fun MapSearchScreen(onLocationSelected: (PoiData) -> Unit) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ var searchText by remember { mutableStateOf("") }
+ var selectLatLng by remember { mutableStateOf(LatLonPoint(0.0, 0.0)) }
+ var mapInitialized by remember { mutableStateOf(false) }
+ val mapView = remember { MapView(context) } // Remember the MapView instance
+ var locationClient: AMapLocationClient? by remember { mutableStateOf(null) }
+ var isLoading by remember { mutableStateOf(false) }
+ var errorMessage by remember { mutableStateOf(null) }
+ var searchResults by remember { mutableStateOf>(emptyList()) }
+
+ // Permission handling
+ val locationPermissionGranted = remember { mutableStateOf(false) }
+ val locationPermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ locationPermissionGranted.value = isGranted
+ }
+
+ // Check if location permission is granted
+ val checkPermission = {
+ if (ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ locationPermissionGranted.value = true
+ } else {
+ locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+ }
+
+ errorMessage?.let { message ->
+ Snackbar(
+ action = {
+ Button(onClick = { errorMessage = null }) {
+ Text("关闭")
+ }
+ },
+ modifier = Modifier.padding(8.dp)
+ ) {
+ Text(message)
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ checkPermission()
+ }
+
+ LaunchedEffect(key1 = locationPermissionGranted.value) {
+ if (locationPermissionGranted.value) {
+ locationClient = AMapLocationClient(context).apply {
+ setLocationOption(AMapLocationClientOption().apply {
+ isOnceLocation = true // Only need the location once
+ isNeedAddress = true
+ })
+ setLocationListener { location ->
+ if (location != null) {
+ val latLng = LatLng(location.latitude, location.longitude)
+ selectLatLng = LatLonPoint(location.latitude, location.longitude)
+ scope.launch {
+ val address = reverseGeocode(context, selectLatLng)
+ searchText = address
+ }
+ mapView.map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f))
+ } else {
+ Log.e("MapSearchScreen", "Location failed")
+ errorMessage = "位置获取失败"
+ }
+ locationClient?.apply {
+ stopLocation() // 停止位置更新
+ onDestroy() // 销毁位置客户端
+ }
+ locationClient = null
+ }
+ startLocation()
+ }
+ } else {
+ Log.e("MapSearchScreen", "Location permission not granted")
+ errorMessage = "位置权限未授予"
+ }
+ }
+
+ DisposableEffect(key1 = mapView) {
+ onDispose {
+ locationClient?.onDestroy()
+ mapView.onDestroy()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ HeadView(title = "地图选点", onBack = { context.finish() })
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(16.dp)
+ .verticalScroll(state = rememberScrollState())
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ TextField(
+ value = searchText,
+ onValueChange = { searchText = it },
+ label = { Text("搜索地址") },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ modifier = Modifier.fillMaxWidth()
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = {
+ if (searchText.isNotBlank()) {
+ isLoading = true
+ errorMessage = null
+ searchResults = emptyList() // Clear previous results
+ scope.launch {
+ val results = searchPoi(context, searchText)
+ searchResults = results
+ isLoading = false
+ }
+ } else {
+ errorMessage = "请输入地址"
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("搜索")
+ }
+ }
+ }
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+ }
+ if (searchResults.isNotEmpty()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(180.dp)
+ .padding(bottom = 16.dp)
+ ) {
+ items(items = searchResults) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ selectLatLng = it.latLonPoint
+ searchText = it.title
+ }
+ .padding(5.dp)) {
+ Text(it.title, color = Color.Black)
+ }
+ }
+ }
+ }
+ // Wrap MapView with AndroidView
+ AndroidView(
+ factory = {
+ mapView.apply {
+ onCreate(null)
+ onResume()
+ }
+ },
+ update = {
+ it.map.setOnMapClickListener { latLng ->
+ selectLatLng = LatLonPoint(latLng.latitude, latLng.longitude)
+ scope.launch {
+ val address = reverseGeocode(context, selectLatLng)
+ searchText = address
+ }
+ }
+ if (!mapInitialized) {
+ mapInitialized = true
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ CommonButton(text = "确定") {
+ onLocationSelected(PoiData(name = searchText, lat = selectLatLng.latitude, lng = selectLatLng.longitude))
+ }
+ }
+ }
+}
+
+private suspend fun searchPoi(context: Context, query: String): List {
+ return suspendCoroutine { continuation ->
+ val poiSearchQuery = PoiSearch.Query(query, "", "") // 第二个参数是城市代码,留空表示全国搜索
+ poiSearchQuery.pageNum = 0
+ poiSearchQuery.pageSize = 10
+ val poiSearch = PoiSearch(context, poiSearchQuery)
+
+ poiSearch.setOnPoiSearchListener(object : PoiSearch.OnPoiSearchListener {
+ override fun onPoiSearched(result: PoiResult?, rCode: Int) {
+ if (rCode == AMapException.CODE_AMAP_SUCCESS) {
+ val pois = result?.pois ?: emptyList()
+ continuation.resume(pois)
+ } else {
+ Log.e("MapSearchScreen", "POI 搜索失败:$rCode")
+ continuation.resume(emptyList())
+ }
+ }
+
+ override fun onPoiItemSearched(poiItem: PoiItem?, rCode: Int) {
+ // Not used in this case
+ }
+ })
+
+ poiSearch.searchPOIAsyn()
+ }
+}
+
+private suspend fun reverseGeocode(context: Context, latLng: LatLonPoint): String {
+ return suspendCoroutine { continuation ->
+ val geocodeSearch = GeocodeSearch(context)
+ geocodeSearch.setOnGeocodeSearchListener(object : GeocodeSearch.OnGeocodeSearchListener {
+ override fun onRegeocodeSearched(result: RegeocodeResult?, rCode: Int) {
+ if (rCode == AMapException.CODE_AMAP_SUCCESS) {
+ val address = result?.regeocodeAddress?.formatAddress
+ continuation.resume(address ?: "")
+ } else {
+ Log.e("MapSearchScreen", "Reverse Geocode failed: $rCode")
+ continuation.resume("")
+ }
+ }
+
+ override fun onGeocodeSearched(result: GeocodeResult?, rCode: Int) {
+ // Not used in reverse geocoding
+ }
+ })
+
+ // Perform Reverse Geocoding (LatLng -> address)
+ val query = com.amap.api.services.geocoder.RegeocodeQuery(latLng, 200f, GeocodeSearch.AMAP)
+ geocodeSearch.getFromLocationAsyn(query) // Use the async version
+ }
+}
+
+data class PoiData(val name: String? = null, val lat: Double? = null, val lng: Double? = null) : Serializable
+
+@Preview
+@Composable
+fun PreviewMapSearchScreen() {
+ MapSearchScreen(onLocationSelected = {})
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/new_order/NewOrderActivity.kt b/servicing/src/main/java/com/za/ui/new_order/NewOrderActivity.kt
new file mode 100644
index 0000000..a13d33b
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/new_order/NewOrderActivity.kt
@@ -0,0 +1,584 @@
+package com.za.ui.new_order
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.amap.api.location.AMapLocationClient
+import com.amap.api.maps.CameraUpdateFactory
+import com.amap.api.maps.MapView
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.LatLngBounds
+import com.amap.api.maps.model.MarkerOptions
+import com.amap.api.maps.model.PolylineOptions
+import com.za.base.BaseActivity
+import com.za.base.theme.headBgColor
+import com.za.base.view.CommonDialog
+import com.za.base.view.HeadViewNotBack
+import com.za.bean.JpushBean
+import com.za.call.CallLogManager
+import com.za.call.ContactRecordBean
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.common.util.ImageUtil
+import com.za.ext.callPhone
+import com.za.ext.copy
+import com.za.ext.finish
+import com.za.servicing.R
+
+class NewOrderActivity : BaseActivity() {
+
+ @Composable
+ override fun ContentView() {
+ val jpushBean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getSerializableExtra("jpushBean", JpushBean::class.java)
+ } else {
+ intent.getSerializableExtra("jpushBean")
+ }
+ AcceptOrderScreen(jpushBean = jpushBean as JpushBean)
+ }
+
+ companion object {
+ fun goNewOrderActivity(context : Context, jpushBean : JpushBean) {
+ val intent = Intent(context, NewOrderActivity::class.java)
+ intent.putExtra("jpushBean", jpushBean)
+ context.startActivity(intent)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AcceptOrderScreen(jpushBean : JpushBean?, vm : NewOrderVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val mapView = remember { MapView(context) }
+
+ // 将状态提升到 Composable 层级
+ val lastRoutePoints = remember { mutableStateOf?>(null) }
+ val lastMarkers = remember { mutableStateOf?>(null) }
+ val lastCurrentLocation = remember { mutableStateOf(null) }
+
+ val bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
+ val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
+
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(NewOrderVm.Action.Init(jpushBean as JpushBean))
+ }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ try {
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> mapView.onCreate(null)
+ Lifecycle.Event.ON_RESUME -> mapView.onResume()
+ Lifecycle.Event.ON_PAUSE -> mapView.onPause()
+ Lifecycle.Event.ON_DESTROY -> {
+ mapView.onDestroy()
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(remainingTime = 0)))
+ }
+
+ else -> {}
+ }
+ } catch (e : Exception) {
+ LogUtil.print("LifecycleObserver", "生命周期事件处理异常: ${e.message}")
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ try {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(remainingTime = 0)))
+ } catch (e : Exception) {
+ LogUtil.print("DisposableEffect", "清理资源异常: ${e.message}")
+ }
+ }
+ }
+
+ if (uiState.value.showCallPhoneDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "确定",
+ title = "是否联系客户?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false)))
+ context.finish()
+ },
+ dismiss = {
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false)))
+ context.callPhone(uiState.value.jpushBean?.customerPhone)
+ CallLogManager.phoneCallContactBean =
+ ContactRecordBean(taskId = uiState.value.jpushBean?.taskId,
+ taskCode = uiState.value.jpushBean?.taskCode,
+ phone = uiState.value.jpushBean?.customerPhone)
+ context.finish()
+ })
+ }
+
+ if (uiState.value.refuseSuccess == true) {
+ context.finish()
+ }
+
+ // 添加超时对话框
+ if (uiState.value.showTimeoutDialog) {
+ CommonDialog(cancelText = "关闭",
+ confirmText = "确定",
+ title = "订单已超时",
+ message = "当前订单已超时,请等待新的订单",
+ cancelEnable = false,
+ dismiss = {
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(showTimeoutDialog = false)))
+ context.finish()
+ },
+ confirm = {
+ vm.dispatch(NewOrderVm.Action.UpdateState(uiState.value.copy(showTimeoutDialog = false)))
+ context.finish()
+ })
+ }
+
+ BottomSheetScaffold(scaffoldState = scaffoldState,
+ topBar = { HeadViewNotBack(title = "新订单") },
+ sheetContent = {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .background(Color.White)
+ .verticalScroll(rememberScrollState())) { // 滑动指示器
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ .background(Color.White), contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier
+ .width(32.dp)
+ .height(4.dp)
+ .background(color = Color(0xFFE0E0E0), shape = RoundedCornerShape(2.dp)))
+ }
+
+ // 滑动指示器和订单类型行
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = uiState.value.jpushBean?.serviceTypeName ?: "",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+
+ // 修改倒计时显示
+ Text(text = if (uiState.value.isTimeout) "已超时" else "${uiState.value.remainingTime}S",
+ color = Color(0xFFFF4D4F),
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold)
+ }
+
+ // 添加距离和时间信息
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "预计到达: ${uiState.value.estimatedArrivalTime}",
+ color = Color(0xFF666666),
+ fontSize = 14.sp)
+
+ Text(text = "总里程: %.1fkm".format(uiState.value.remainingDistance / 1000f),
+ color = Color(0xFFFF4D4F),
+ fontSize = 14.sp)
+ }
+
+ HorizontalDivider(modifier = Modifier
+ .fillMaxWidth()
+ .alpha(0.1f))
+
+ // 订单信息
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)) { // 订单标签
+ Row(verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+
+ Text(text = uiState.value.jpushBean?.addressProperty ?: "",
+ color = Color(0xFFFD8205),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // 订单号
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ uiState.value.jpushBean?.taskCode?.copy(context)
+ }) {
+ Text(text = "单号", color = Color(0xFF999999), fontSize = 13.sp)
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.jpushBean?.taskCode ?: "",
+ color = Color(0xFF666666),
+ fontSize = 14.sp)
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ AsyncImage(model = R.drawable.sv_copy,
+ contentDescription = "copy",
+ modifier = Modifier.size(16.dp))
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 地址信息
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { // 救援地
+ Row(verticalAlignment = Alignment.Top,
+ modifier = Modifier.clickable { // 点击救援地时移动地图到救援位置
+ uiState.value.jpushBean?.let { order ->
+ if (order.lat != null && order.lat != 0.0 && order.lng != null && order.lng != 0.0) {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.lat !!, order.lng !!),
+ 16f))
+ }
+ }
+ }) {
+ AsyncImage(model = R.drawable.sv_rescuing,
+ contentDescription = "rescue",
+ modifier = Modifier.size(16.dp))
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.jpushBean?.address ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium)
+ }
+
+ // 目的地
+ if (! uiState.value.jpushBean?.distAddress.isNullOrBlank()) {
+ Row(verticalAlignment = Alignment.Top,
+ modifier = Modifier.clickable { // 点击目的地时移动地图到目的地位置
+ uiState.value.jpushBean?.let { order ->
+ if (order.distLat != null && order.distLat != 0.0 && order.distLng != null && order.distLng != 0.0) {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.distLat !!, order.distLng !!),
+ 16f))
+ }
+ }
+ }) {
+ AsyncImage(model = R.drawable.sv_dist,
+ contentDescription = "destination",
+ modifier = Modifier.size(16.dp))
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.jpushBean?.distAddress ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 修改按钮状态
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)) { // 拒单按钮
+ Button(onClick = { vm.dispatch(NewOrderVm.Action.RefuseOrder) },
+ modifier = Modifier
+ .weight(1f)
+ .height(44.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = Color.White,
+ contentColor = Color(0xFF666666)),
+ border = BorderStroke(1.dp, Color(0xFFE5E5E5)),
+ shape = RoundedCornerShape(8.dp)) {
+ Text(text = "拒绝", fontSize = 16.sp, fontWeight = FontWeight.Medium)
+ }
+
+ // 接单按钮
+ Button(onClick = { vm.dispatch(NewOrderVm.Action.AcceptOrder) },
+ modifier = Modifier
+ .weight(1f)
+ .height(44.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = headBgColor),
+ shape = RoundedCornerShape(8.dp)) {
+ Text(text = "接单",
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ }
+ }
+ }
+ },
+ sheetPeekHeight = 180.dp,
+ sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
+ sheetContainerColor = Color.White,
+ sheetShadowElevation = 8.dp,
+ sheetDragHandle = null,
+ sheetSwipeEnabled = true) { paddingValues ->
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)) {
+ AndroidView(modifier = Modifier.fillMaxSize(), factory = {
+ AMapLocationClient.updatePrivacyShow(context, true, true)
+ AMapLocationClient.updatePrivacyAgree(context, true)
+ mapView.apply {
+ map.apply {
+ isTrafficEnabled = false
+ isMyLocationEnabled = false
+ uiSettings.isMyLocationButtonEnabled = false
+ uiSettings.setLogoBottomMargin(- 100)
+ uiSettings.isZoomControlsEnabled = false
+
+ setOnMarkerClickListener { marker ->
+ marker.showInfoWindow()
+ Handler(Looper.getMainLooper()).postDelayed({
+ marker.hideInfoWindow()
+ }, 800)
+ true
+ }
+ }
+ }
+ }, update = { mapView ->
+ val currentLocation = if (GlobalData.currentLocation != null) {
+ LatLng(GlobalData.currentLocation?.latitude !!,
+ GlobalData.currentLocation?.longitude !!)
+ } else null
+
+ val needUpdate =
+ lastRoutePoints.value != uiState.value.routePoints || lastMarkers.value != uiState.value.markers || lastCurrentLocation.value != currentLocation
+
+ if (needUpdate) {
+ mapView.map.clear()
+
+ // 绘制路线
+ uiState.value.routePoints?.let { points ->
+ if (points.isNotEmpty()) {
+ mapView.map.addPolyline(PolylineOptions().addAll(points).width(15f)
+ .color(Color(0xFF3D4B7C).toArgb()).zIndex(1f))
+ }
+ }
+
+ // 添加标记点
+ val allPoints = mutableListOf()
+
+ // 添加当前位置标记
+ currentLocation?.let {
+ mapView.map.addMarker(MarkerOptions().position(it).title("当前位置")
+ .icon(ImageUtil.vectorToBitmap(context,R.drawable.ic_current_location))
+ .anchor(0.5f, 0.5f).visible(true))
+ allPoints.add(it)
+ }
+
+ // 添加其他标记点
+ uiState.value.markers?.let { markers ->
+ mapView.map.addMarkers(markers, true)
+ markers.forEach { marker ->
+ allPoints.add(marker.position)
+ }
+ }
+
+ // 添加路线点
+ uiState.value.routePoints?.let { points ->
+ allPoints.addAll(points)
+ }
+
+ // 计算边界
+ if (allPoints.isNotEmpty()) {
+ try {
+ val boundsBuilder = LatLngBounds.builder()
+ allPoints.forEach { point ->
+ boundsBuilder.include(point)
+ }
+
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngBounds(
+ boundsBuilder.build(),
+ 100))
+ } catch (e : Exception) { // 如果边界计算失败,使用救援点作为中心
+ jpushBean?.let { bean ->
+ if (bean.lat != null && bean.lat != 0.0 && bean.lng != null && bean.lng != 0.0) {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ LatLng(bean.lat, bean.lng),
+ 15f))
+ }
+ }
+ }
+ }
+
+ // 更新状态
+ lastRoutePoints.value = uiState.value.routePoints
+ lastMarkers.value = uiState.value.markers
+ lastCurrentLocation.value = currentLocation
+ }
+ })
+ }
+ }
+}
+
+@Composable
+private fun OrderItemView(jpushBean : JpushBean?, remainingTime : Int = 50) {
+ val context = LocalContext.current
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(color = Color.White)
+ .padding(bottom = 16.dp)) { // 订单类型和倒计时
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .background(brush = Brush.verticalGradient(colors = arrayListOf(Color(0xFFFFEDE3),
+ Color.White)))
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = jpushBean?.serviceTypeName ?: "",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+ Text(text = "${remainingTime}s",
+ fontSize = 15.sp,
+ color = Color(0xFFFF4200),
+ fontWeight = FontWeight.Medium)
+ }
+
+ HorizontalDivider(modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .alpha(0.08f))
+
+ // 订单标签
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier
+ .background(Color(0xFF9BA1B2), RoundedCornerShape(4.dp))
+ .padding(horizontal = 6.dp, vertical = 2.dp)) {
+ Text(text = "月结", color = Color.White, fontSize = 12.sp)
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = jpushBean?.contract ?: "", color = Color.Black, fontSize = 12.sp)
+ }
+ Text(text = jpushBean?.addressProperty ?: "",
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color(0xFFFD8205))
+ }
+
+ // 订单号
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .clickable { jpushBean?.taskCode?.copy(context) },
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "单号", color = Color(0xFF999999), fontSize = 13.sp)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = jpushBean?.taskCode ?: "",
+ color = Color(0xFF666666),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium)
+ Spacer(modifier = Modifier.width(8.dp))
+ AsyncImage(model = R.drawable.sv_copy,
+ contentDescription = "copy",
+ modifier = Modifier.size(16.dp))
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 救援地址
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.Top) {
+ AsyncImage(model = R.drawable.sv_rescuing,
+ contentDescription = "rescue",
+ modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = jpushBean?.address ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium)
+ }
+
+ // 目的地地址
+ if (! jpushBean?.distAddress.isNullOrBlank()) {
+ Spacer(modifier = Modifier.height(16.dp))
+ HorizontalDivider(modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .alpha(0.1f))
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.Top) {
+ AsyncImage(model = R.drawable.sv_dist,
+ contentDescription = "destination",
+ modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = jpushBean?.distAddress ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/new_order/NewOrderVm.kt b/servicing/src/main/java/com/za/ui/new_order/NewOrderVm.kt
new file mode 100644
index 0000000..27fa400
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/new_order/NewOrderVm.kt
@@ -0,0 +1,272 @@
+package com.za.ui.new_order
+
+import androidx.lifecycle.viewModelScope
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.MarkerOptions
+import com.amap.api.services.core.LatLonPoint
+import com.amap.api.services.route.BusRouteResult
+import com.amap.api.services.route.DriveRouteResult
+import com.amap.api.services.route.RideRouteResult
+import com.amap.api.services.route.RouteSearch
+import com.amap.api.services.route.WalkRouteResult
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.JpushBean
+import com.za.bean.request.AcceptOrderRequest
+import com.za.bean.request.RefuseOrderRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.common.util.DeviceUtil
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import com.za.service.location.ZdLocationManager
+import com.za.servicing.R
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class NewOrderVm : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+
+ private var timerJob: Job? = null
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init(action.jpushBean)
+ is Action.AcceptOrder -> acceptOrder()
+ is Action.RefuseOrder -> refuseOrder()
+ is Action.StartTimer -> startTimer()
+ is Action.UpdateTimer -> updateTimer(action.remainingTime)
+ else -> {}
+ }
+ }
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ private fun init(jpushBean: JpushBean?) {
+ updateState(uiState.value.copy(jpushBean = jpushBean))
+ buildMarkers(jpushBean)
+ searchDrivingRoute(jpushBean)
+ startTimer()
+ }
+
+ private fun buildMarkers(jpushBean: JpushBean?) {
+ val markers = arrayListOf()
+ if (jpushBean?.lat != null && jpushBean.lat != 0.0 && jpushBean.lng != null && jpushBean.lng != 0.0) {
+ val startMarkers = MarkerOptions()
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_rescuing_map))
+ .position(LatLng(jpushBean.lat, jpushBean.lng))
+ .title(jpushBean.address)
+ .snippet("救援地点")
+ .visible(true)
+ markers.add(startMarkers)
+ }
+
+ if (jpushBean?.distLat != null && jpushBean.distLat != 0.0 && jpushBean.distLng != null && jpushBean.distLng != 0.0) {
+ val startMarkers = MarkerOptions()
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_dist_map))
+ .position(LatLng(jpushBean.distLat, jpushBean.distLng))
+ .title(jpushBean.distAddress)
+ .snippet("目的地")
+ .visible(true)
+ markers.add(startMarkers)
+ }
+ updateState(uiState.value.copy(markers = markers))
+ }
+
+ private fun acceptOrder() {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ val orderInfo = uiState.value.jpushBean
+ val acceptOrderRequest = AcceptOrderRequest()
+ acceptOrderRequest.taskId = orderInfo?.taskId
+ acceptOrderRequest.vehicleId = GlobalData.vehicleInfo?.vehicleId
+ acceptOrderRequest.userId = GlobalData.driverInfo?.userId
+ acceptOrderRequest.taskCode = orderInfo?.taskCode
+ acceptOrderRequest.deviceId = DeviceUtil.getAndroidId(ActivityUtils.getTopActivity())
+ acceptOrderRequest.lat = it.latitude
+ acceptOrderRequest.lng = it.longitude
+ RetrofitHelper.getDefaultService().acceptOrder(acceptOrderRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ LogUtil.print("接单成功", "request=$acceptOrderRequest")
+ updateState(uiState.value.copy(showCallPhoneDialog = orderInfo?.isNeedCallCustomPhone()))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ LogUtil.print("接单失败", "request=$acceptOrderRequest msg=$msg")
+ }
+ })
+ }, failed = {
+ LoadingManager.hideLoading()
+ LogUtil.print("接单时获取定位失败", it)
+ })
+ }
+
+ private fun refuseOrder() {
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().refuseOrder(RefuseOrderRequest(taskId = uiState.value.jpushBean?.taskId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ taskCode = uiState.value.jpushBean?.taskCode,
+ userId = GlobalData.driverInfo?.userId))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(refuseSuccess = true))
+ LogUtil.print("refuseOrder", "订单拒绝成功")
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ LogUtil.print("refuseOrder", "failed=$msg")
+ }
+ })
+ }
+
+ private fun startTimer() {
+ timerJob?.cancel()
+ timerJob = viewModelScope.launch {
+ try {
+ var timeLeft = 50
+ while (timeLeft > 0 && isActive) {
+ delay(1000)
+ timeLeft--
+ updateState(uiState.value.copy(remainingTime = timeLeft))
+ }
+ if (timeLeft == 0 && isActive) {
+ // 倒计时结束,显示订单超时
+ updateState(uiState.value.copy(
+ isTimeout = true,
+ showTimeoutDialog = true
+ ))
+ }
+ } catch (e: Exception) {
+ LogUtil.print("startTimer", "倒计时异常: ${e.message}")
+ }
+ }
+ }
+
+ private fun updateTimer(remainingTime: Int) {
+ updateState(uiState.value.copy(remainingTime = remainingTime))
+ }
+
+ private fun searchDrivingRoute(jpushBean: JpushBean?) {
+ if (GlobalData.currentLocation == null) {
+ ToastUtils.showShort("获取当前位置失败")
+ return
+ }
+
+ updateState(uiState.value.copy(isLoading = true))
+
+ val startPoint = LatLonPoint(
+ GlobalData.currentLocation?.latitude ?: 0.0,
+ GlobalData.currentLocation?.longitude ?: 0.0
+ )
+
+ val endPoint = when {
+ jpushBean?.distLat != null && jpushBean.distLat != 0.0 &&
+ jpushBean.distLng != null && jpushBean.distLng != 0.0 -> {
+ LatLonPoint(jpushBean.distLat, jpushBean.distLng)
+ }
+ jpushBean?.lat != null && jpushBean.lat != 0.0 &&
+ jpushBean.lng != null && jpushBean.lng != 0.0 -> {
+ LatLonPoint(jpushBean.lat, jpushBean.lng)
+ }
+ else -> null
+ }
+
+ if (endPoint == null) return
+
+ val fromAndTo = RouteSearch.FromAndTo(startPoint, endPoint)
+ val query = RouteSearch.DriveRouteQuery(fromAndTo, RouteSearch.DrivingDefault, null, null, "")
+
+ RouteSearch(GlobalData.application).apply {
+ setRouteSearchListener(object : RouteSearch.OnRouteSearchListener {
+ override fun onDriveRouteSearched(result: DriveRouteResult?, errorCode: Int) {
+ updateState(uiState.value.copy(isLoading = false))
+
+ if (errorCode == 1000 && result != null && result.paths.isNotEmpty()) {
+ val path = result.paths[0]
+ val points = path.steps.flatMap { step ->
+ step.polyline.map { LatLng(it.latitude, it.longitude) }
+ }
+
+ val duration = path.duration
+ val arrivalTime = System.currentTimeMillis() + duration * 1000
+ val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
+ val estimatedTime = dateFormat.format(Date(arrivalTime))
+
+ updateState(uiState.value.copy(
+ routePoints = points,
+ remainingDistance = path.distance.toDouble(),
+ estimatedArrivalTime = estimatedTime
+ ))
+ } else {
+ ToastUtils.showShort("路线规划失败,请重试")
+ LogUtil.print("searchDrivingRoute", "路径规划失败: errorCode=$errorCode")
+ }
+ }
+
+ override fun onBusRouteSearched(p0: BusRouteResult?, p1: Int) {}
+ override fun onWalkRouteSearched(p0: WalkRouteResult?, p1: Int) {}
+ override fun onRideRouteSearched(p0: RideRouteResult?, p1: Int) {}
+ })
+ calculateDriveRouteAsyn(query)
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ try {
+ timerJob?.cancel()
+ timerJob = null
+ } catch (e: Exception) {
+ LogUtil.print("onCleared", "取消倒计时异常: ${e.message}")
+ }
+ }
+
+ sealed class Action {
+ data class Init(val jpushBean: JpushBean?) : Action()
+ data object AcceptOrder : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object RefuseOrder : Action()
+ data object StartTimer : Action()
+ data class UpdateTimer(val remainingTime: Int) : Action()
+ }
+
+ data class UiState(
+ val jpushBean: JpushBean? = null,
+ val showCallPhoneDialog: Boolean? = false,
+ val markers: ArrayList? = null,
+ val refuseSuccess: Boolean? = false,
+ val remainingTime: Int = 50,
+ val routePoints: List? = null,
+ val remainingDistance: Double = 0.0,
+ val estimatedArrivalTime: String = "",
+ val isLoading: Boolean = false,
+ val isTimeout: Boolean = false,
+ val showTimeoutDialog: Boolean = false
+ )
+
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/order_report/HistoryReportActivity.kt b/servicing/src/main/java/com/za/ui/order_report/HistoryReportActivity.kt
new file mode 100644
index 0000000..4144f36
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/order_report/HistoryReportActivity.kt
@@ -0,0 +1,133 @@
+package com.za.ui.order_report
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.blankj.utilcode.util.TimeUtils
+import com.za.base.BaseActivity
+import com.za.base.theme.black5
+import com.za.base.view.EmptyView
+import com.za.base.view.HeadView
+import com.za.ext.finish
+
+class HistoryReportActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ HistoryReportScreen()
+ }
+}
+
+@Composable
+fun HistoryReportScreen(vm : HistoryReportVm = viewModel()) {
+ val uiState by vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ vm.dispatch(HistoryReportVm.Action.Init)
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ ReportFloatingManager.startService(context)
+ }
+
+ Scaffold(topBar = {
+ HeadView(title = "历史报备", onBack = { context.finish() })
+ }) {
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top) {
+
+ if (uiState.historyReportList.isNullOrEmpty()) {
+ item {
+ EmptyView()
+ }
+ return@LazyColumn
+ }
+
+ items(items = uiState.historyReportList ?: arrayListOf()) { item ->
+ Card(modifier = Modifier
+ .wrapContentSize()
+ .padding(10.dp),
+ colors = CardDefaults.cardColors().copy(containerColor = Color.White)) {
+ Row(modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier
+ .background(color = Color(0x99FF0000).takeIf { item.state == 1 }
+ ?: Color.Green, shape = RoundedCornerShape(5.dp))
+ .padding(horizontal = 10.dp, vertical = 2.dp)) {
+ Text("未处理".takeIf { item.state == 1 } ?: "已处理",
+ color = Color.Black,
+ fontSize = 13.sp)
+ }
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(TimeUtils.millis2String(item.createTime ?: System.currentTimeMillis())
+ ?: "", color = Color.Gray, fontSize = 13.sp)
+ }
+ HorizontalDivider(color = black5,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp, vertical = 2.dp))
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp),
+ contentAlignment = Alignment.Center) {
+ Text(item.reportTemplate ?: "",
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+ }
+
+ Spacer(modifier = Modifier.height(2.dp))
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewHistoryReport() {
+ HistoryReportScreen()
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/order_report/HistoryReportVm.kt b/servicing/src/main/java/com/za/ui/order_report/HistoryReportVm.kt
new file mode 100644
index 0000000..92169f7
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/order_report/HistoryReportVm.kt
@@ -0,0 +1,54 @@
+package com.za.ui.order_report
+
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.ReportHistoryBean
+import com.za.bean.ReportHistoryRequest
+import com.za.common.GlobalData
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class HistoryReportVm : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState.asStateFlow()
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ Action.Init -> init()
+ }
+ }
+
+ private fun init() {
+ LoadingManager.showLoading()
+ val request = ReportHistoryRequest(taskId = "${GlobalData.currentOrder?.taskId}")
+ RetrofitHelper.getDefaultService().getReportHistory(request)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver>() {
+ override fun doSuccess(it: List?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(historyReportList = it))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showLong(msg)
+ }
+ })
+ }
+
+ data class UiState(val historyReportList: List? = null)
+
+ sealed class Action {
+ data object Init : Action()
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/order_report/OrderReportActivity.kt b/servicing/src/main/java/com/za/ui/order_report/OrderReportActivity.kt
new file mode 100644
index 0000000..3b036b3
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/order_report/OrderReportActivity.kt
@@ -0,0 +1,210 @@
+package com.za.ui.order_report
+
+import android.content.Intent
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.BaseActivity
+import com.za.base.theme.bgColor
+import com.za.base.theme.white95
+import com.za.base.view.CommonButton
+import com.za.base.view.HeadView
+import com.za.ext.finish
+import com.za.ext.navigationActivity
+import com.za.servicing.R
+import com.za.ui.map_search.MapSearchActivity
+import com.za.ui.map_search.PoiData
+import kotlinx.coroutines.launch
+
+class OrderReportActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ OrderReportScreen()
+ }
+}
+
+private val PrimaryBlue = Color(0xFF1A73E8) // 更新为 Google Blue
+private val TextGrey = Color(0xFF5F6368)
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun OrderReportScreen(vm: OrderReportVm = viewModel()) {
+ val uiState by vm.uiState.collectAsStateWithLifecycle()
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
+ LaunchedEffect(Unit) {
+ vm.dispatch(OrderReportVm.Action.Init)
+ scaffoldState.bottomSheetState.expand()
+ }
+
+ if (uiState.success == true) {
+ context.finish()
+ }
+
+ val result = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val poiData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ it.data?.getSerializableExtra("poiData", PoiData::class.java)
+ } else {
+ it.data?.getSerializableExtra("poiData") as PoiData?
+ }
+ if (poiData != null) {
+ vm.updateState(uiState.copy(reportContent = poiData.name))
+ }
+ }
+
+ BottomSheetScaffold(topBar = {
+ HeadView(title = "添加报备", onBack = { context.finish() }, action = {
+ Icon(imageVector = ImageVector.vectorResource(R.drawable.sv_history),
+ contentDescription = "",tint = Color.White,
+ modifier = Modifier.clickable {
+ context.navigationActivity(HistoryReportActivity::class.java)
+ }.height(40.dp).padding(10.dp))
+ })
+ }, sheetPeekHeight = 100.dp, scaffoldState = scaffoldState, sheetContainerColor = bgColor, sheetContent = {
+ LazyVerticalGrid(columns = GridCells.Fixed(3), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, modifier = Modifier.padding(bottom = 20.dp)) {
+ itemsIndexed(uiState.reportList ?: arrayListOf()) { index, item ->
+ Box(modifier = Modifier
+ .size(120.dp, 70.dp)
+ .padding(2.dp)
+ .background(color = white95, shape = RoundedCornerShape(8.dp))
+ .border(width = 1.dp, color = if (uiState.selectReportItem?.reportType == item.reportType) {
+ Color.White
+ } else {
+ Color.Transparent
+ }, shape = RoundedCornerShape(8.dp))
+ .padding(10.dp)
+ .clickable {
+ scope.launch {
+ if (scaffoldState.bottomSheetState.hasExpandedState) {
+ scaffoldState.bottomSheetState.partialExpand()
+ vm.dispatch(OrderReportVm.Action.SelectReportItem(item))
+ }
+ }
+ }, contentAlignment = Alignment.Center) {
+ Text(text = item.reportType ?: "", color = Color.Black, fontSize = 13.sp)
+ }
+ }
+ }
+ }) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ Spacer(modifier = Modifier.height(30.dp))
+
+ AnimatedVisibility(uiState.selectReportItem == null) {
+ CommonButton(text = "请选择报备类型") {
+ scope.launch {
+ scaffoldState.bottomSheetState.expand()
+ }
+ }
+ }
+
+ AnimatedVisibility(uiState.selectReportItem != null) {
+ Column(modifier = Modifier
+ .wrapContentWidth()
+ .clickable {
+ scope.launch {
+ localSoftwareKeyboardController?.hide()
+ scaffoldState.bottomSheetState.expand()
+ }
+ }
+ .background(color = Color.White, shape = RoundedCornerShape(8.dp))
+ .padding(10.dp), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Center) {
+ Text("当前报备类型:", fontSize = 13.sp, color = Color.Gray)
+ Spacer(Modifier.height(10.dp))
+ Box(modifier = Modifier.fillMaxWidth()) {
+ Text(text = uiState.selectReportItem?.reportType
+ ?: "", fontSize = 18.sp, color = Color.Black, fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+ AnimatedVisibility(uiState.selectReportItem != null) {
+ OutlinedTextField(
+ value = uiState.reportContent ?: "",
+ onValueChange = { vm.updateState(uiState.copy(reportContent = it)) },
+ label = { Text("备注内容") },
+ modifier = Modifier.fillMaxWidth(),
+ minLines = 3,
+ textStyle = TextStyle.Default.copy(fontSize = 16.sp, color = Color.Black),
+ shape = RoundedCornerShape(12.dp),
+ trailingIcon = {
+ if (uiState.selectReportItem?.reportType?.contains("事发地") == true
+ || uiState.selectReportItem?.reportType?.contains("目的地") == true
+ )
+ Icon(imageVector = Icons.Default.LocationOn, contentDescription = null,
+ tint = PrimaryBlue,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable {
+ result.launch(Intent(context, MapSearchActivity::class.java))
+ })
+ },
+ prefix = { Text("${uiState.selectReportItem?.reportTemplate?.replace("_", "")}", fontSize = 13.sp) },
+ colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = PrimaryBlue, unfocusedBorderColor = PrimaryBlue.copy(alpha = 0.5f), focusedLabelColor = PrimaryBlue, unfocusedLabelColor = TextGrey),
+ )
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+ AnimatedVisibility(uiState.selectReportItem != null) {
+ CommonButton("提交报备") {
+ vm.dispatch(OrderReportVm.Action.Submit)
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun OrderReportPreview(modifier: Modifier = Modifier) {
+ OrderReportScreen()
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/order_report/OrderReportVm.kt b/servicing/src/main/java/com/za/ui/order_report/OrderReportVm.kt
new file mode 100644
index 0000000..def3bd9
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/order_report/OrderReportVm.kt
@@ -0,0 +1,106 @@
+package com.za.ui.order_report
+
+import androidx.compose.ui.graphics.Color
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.ReportInfoRequest
+import com.za.bean.ReportItem
+import com.za.common.GlobalData
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class OrderReportVm : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState.asStateFlow()
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> getReportList()
+ is Action.SelectReportItem -> {
+ updateState(uiState.value.copy(selectReportItem = action.item, reportContent = null))
+ }
+
+ is Action.Submit -> {
+ submitReport()
+ }
+ }
+ }
+
+ private fun submitReport() {
+ if (uiState.value.selectReportItem == null) {
+ ToastUtils.showLong("请选择需要报备的类型")
+ return
+ }
+
+ if (uiState.value.reportContent.isNullOrEmpty()) {
+ ToastUtils.showLong("请输入报备内容")
+ return
+ }
+
+ LoadingManager.showLoading()
+ val reportInfoRequest = ReportInfoRequest(
+ taskId = "${GlobalData.currentOrder?.taskId}",
+ reportType = uiState.value.selectReportItem?.reportType ?: "",
+ reportTemplate = "${uiState.value.selectReportItem?.reportTemplate?.replace("_", "")}${uiState.value.reportContent}")
+ RetrofitHelper.getDefaultService().submitReport(reportInfoRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort("提交成功")
+ updateState(uiState.value.copy(success = true))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ }
+ })
+ }
+
+ private fun getReportList() {
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().getReportTemplates()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver>() {
+ override fun doSuccess(it: List?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(reportList = it))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ }
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class SelectReportItem(val item: ReportItem) : Action()
+ data object Submit : Action()
+ }
+
+ data class UiState(
+ val itemColors: ArrayList = arrayListOf(Color(color = 0xFFFEE2E2),
+ Color(color = 0xFFDBEAFE),
+ Color(0xFFFCE7F3),
+ Color(0xFFE0E7FF),
+ Color(color = 0xFFDCFCE7),
+ Color(color = 0xFFFEF9C3),
+ Color(0xFFF3E8FF)),
+ val reportList: List? = null,
+ val selectReportItem: ReportItem? = null,
+ val reportContent: String? = null,
+ val success: Boolean? = false
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/order_report/ReportFloatingManager.kt b/servicing/src/main/java/com/za/ui/order_report/ReportFloatingManager.kt
new file mode 100644
index 0000000..15be03c
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/order_report/ReportFloatingManager.kt
@@ -0,0 +1,296 @@
+package com.za.ui.order_report
+
+import android.annotation.SuppressLint
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.graphics.PixelFormat
+import android.os.Build
+import android.os.IBinder
+import android.provider.Settings
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_PHONE
+import android.view.WindowManager.LayoutParams.WRAP_CONTENT
+import android.view.animation.OvershootInterpolator
+import android.widget.ImageView
+import androidx.core.app.NotificationCompat
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.ServiceUtils
+import com.za.servicing.R
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.math.abs
+
+class ReportFloatingManager : Service() {
+ private var windowManager : WindowManager? = null
+ private var floatingView : View? = null
+ private var touchJob : Job? = null
+ private var initialX : Int = 0
+ private var initialY : Int = 0
+ private var initialTouchX : Float = 0f
+ private var initialTouchY : Float = 0f
+ private var isMoving = false
+ private var startClickTime = 0L
+ private lateinit var prefs : SharedPreferences
+ private var lastUpdateTime = 0L
+ private var screenWidth = 0
+ private var screenHeight = 0
+ private var viewWidth = 0
+ private var viewHeight = 0
+ private var isDragging = false
+
+ private val params = WindowManager.LayoutParams(WRAP_CONTENT,
+ WRAP_CONTENT,
+ getWindowType(),
+ FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCH_MODAL,
+ PixelFormat.TRANSLUCENT)
+
+ private fun getWindowType() : Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ TYPE_APPLICATION_OVERLAY
+ } else {
+ TYPE_PHONE
+ }
+ }
+
+ override fun onBind(intent : Intent?) : IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ lastUpdateTime = System.currentTimeMillis()
+ prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
+ initializeFloatingWindow()
+ setupTouchListener()
+ createNotificationChannel()
+ startForeground(NOTIFICATION_ID, createNotification())
+ }
+
+ private fun initializeFloatingWindow() {
+ windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
+ floatingView = LayoutInflater.from(this).inflate(R.layout.floating_view_layout, null)
+
+ // 获取屏幕尺寸
+ val metrics = resources.displayMetrics
+ screenWidth = metrics.widthPixels
+ screenHeight = metrics.heightPixels
+
+ // 测量视图尺寸
+ floatingView?.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+ viewWidth = floatingView?.measuredWidth ?: 0
+ viewHeight = floatingView?.measuredHeight ?: 0
+
+ // 确保初始位置在屏幕内
+ params.apply {
+ gravity = Gravity.TOP or Gravity.START
+ x = validateX(prefs.getInt(PREF_X, 0))
+ y = validateY(prefs.getInt(PREF_Y, 100))
+ }
+
+ // 设置应用图标
+ floatingView?.findViewById(R.id.floating_image)
+ ?.setImageResource(R.mipmap.ic_customer)
+
+ windowManager?.addView(floatingView, params)
+ animateWindowAppear()
+ }
+
+ private fun animateWindowAppear() {
+ floatingView?.apply {
+ alpha = 0f
+ scaleX = 0.5f
+ scaleY = 0.5f
+ animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(300)
+ .setInterpolator(OvershootInterpolator()).start()
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun setupTouchListener() {
+ floatingView?.setOnTouchListener { _, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> handleTouchDown(event)
+ MotionEvent.ACTION_MOVE -> handleTouchMove(event)
+ MotionEvent.ACTION_UP -> handleTouchUp()
+ else -> false
+ }
+ }
+ }
+
+ private fun handleTouchDown(event : MotionEvent) : Boolean {
+ isDragging = false
+ isMoving = false
+ startClickTime = System.currentTimeMillis()
+ initialX = params.x
+ initialY = params.y
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+
+ // 缩小效果
+ floatingView?.animate()?.scaleX(0.95f)?.scaleY(0.95f)?.setDuration(100)?.start()
+
+ return true
+ }
+
+ private fun handleTouchMove(event : MotionEvent) : Boolean {
+ touchJob?.cancel()
+ touchJob = CoroutineScope(Dispatchers.Main).launch {
+ val deltaX = (event.rawX - initialTouchX).toInt()
+ val deltaY = (event.rawY - initialTouchY).toInt()
+
+ if (! isDragging && (abs(deltaX) > MOVE_THRESHOLD || abs(deltaY) > MOVE_THRESHOLD)) {
+ isDragging = true
+ isMoving = true // 开始拖动时的视觉反馈
+ floatingView?.alpha = 0.8f
+ }
+
+ if (isDragging) {
+ params.x = validateX(initialX + deltaX)
+ params.y = validateY(initialY + deltaY)
+ try {
+ windowManager?.updateViewLayout(floatingView, params)
+ } catch (e : Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+ return true
+ }
+
+ private fun handleTouchUp() : Boolean {
+ touchJob?.cancel()
+
+ // 恢复原始状态
+ floatingView?.apply {
+ animate().scaleX(1f).scaleY(1f).alpha(1f).setDuration(100).start()
+ }
+
+ if (! isMoving && System.currentTimeMillis() - startClickTime < CLICK_THRESHOLD) {
+ openMainActivity()
+ } else if (isDragging) { // 只保存位置,不执行吸附
+ savePosition()
+ }
+
+ isDragging = false
+ return true
+ }
+
+ private fun openMainActivity() { // 否则打开主页面
+ if (ActivityUtils.getTopActivity() is OrderReportActivity) {
+ return
+ }
+ val intent = Intent(this, OrderReportActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ }
+ startActivity(intent)
+ }
+
+ override fun onStartCommand(intent : Intent?, flags : Int, startId : Int) : Int {
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ touchJob?.cancel()
+ windowManager?.removeView(floatingView)
+ }
+
+ private fun validateX(x : Int) : Int {
+ return x.coerceIn(- viewWidth / 2, screenWidth - viewWidth / 2)
+ }
+
+ private fun validateY(y : Int) : Int {
+ val statusBarHeight = getStatusBarHeight()
+ return y.coerceIn(statusBarHeight, screenHeight - viewHeight - getNavigationBarHeight())
+ }
+
+ private fun getStatusBarHeight() : Int {
+ val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
+ return if (resourceId > 0) {
+ resources.getDimensionPixelSize(resourceId)
+ } else {
+ 0
+ }
+ }
+
+ private fun getNavigationBarHeight() : Int {
+ val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
+ return if (resourceId > 0) {
+ resources.getDimensionPixelSize(resourceId)
+ } else {
+ 0
+ }
+ }
+
+ private fun savePosition() {
+ prefs.edit().putInt(PREF_X, params.x).putInt(PREF_Y, params.y).apply()
+ }
+
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(CHANNEL_ID,
+ "悬浮窗服务",
+ NotificationManager.IMPORTANCE_LOW).apply {
+ description = "保持应用在后台运行"
+ }
+
+ val notificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification() =
+ NotificationCompat.Builder(this, CHANNEL_ID).setContentTitle("应用正在后台运行")
+ .setContentText("点击返回应用").setSmallIcon(R.mipmap.ic_launcher)
+ .setPriority(NotificationCompat.PRIORITY_LOW).build()
+
+ // 检查悬浮窗权限
+
+ companion object {
+ private const val PREFS_NAME = "report_floating_window_prefs"
+ private const val PREF_X = "report_window_x"
+ private const val PREF_Y = "report_window_y"
+ private const val MOVE_THRESHOLD = 10
+ private const val CLICK_THRESHOLD = 200L
+ private const val NOTIFICATION_ID = 1
+ private const val CHANNEL_ID = "floating_window_channel"
+
+ @SuppressLint("ObsoleteSdkInt")
+ private fun checkOverlayPermission(context : Context) : Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Settings.canDrawOverlays(context)
+ } else {
+ true
+ }
+ }
+
+ fun startService(context : Context) {
+ if (ServiceUtils.isServiceRunning(ReportFloatingManager::class.java)) {
+ return
+ }
+ if (checkOverlayPermission(context)) {
+ ServiceUtils.startService(ReportFloatingManager::class.java)
+ }
+ }
+
+ fun stopService() {
+ if (ServiceUtils.isServiceRunning(ReportFloatingManager::class.java)) {
+ ServiceUtils.stopService(ReportFloatingManager::class.java)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/InServicingPhotoView.kt b/servicing/src/main/java/com/za/ui/servicing/InServicingPhotoView.kt
new file mode 100644
index 0000000..c4fd9b8
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/InServicingPhotoView.kt
@@ -0,0 +1,804 @@
+package com.za.ui.servicing
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import coil.compose.AsyncImage
+import com.amap.api.location.AMapLocation
+import com.blankj.utilcode.util.TimeUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.permissionx.guolindev.PermissionX
+import com.tencent.smtt.sdk.WebChromeClient
+import com.tencent.smtt.sdk.WebSettings
+import com.tencent.smtt.sdk.WebView
+import com.za.base.Const
+import com.za.base.theme.buttonBgColor
+import com.za.base.view.CommonDialog
+import com.za.base.view.LoadingManager
+import com.za.base.view.ReTakePhotoDialog
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.OrderPhotoOcrRecognizeRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getEleOrderH5Url
+import com.za.ext.noDoubleClick
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import com.za.room.RoomHelper
+import com.za.service.location.ZdLocationManager
+import com.za.servicing.R
+import com.za.ui.camera.ZdCameraXActivity
+import com.za.ui.h5.CommonH5Activity
+import com.za.water_marker.PhotoMarkerManager
+import com.za.water_marker.bean.PhotoMarkerInfo
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import java.io.File
+import java.util.concurrent.Executors
+
+@Synchronized
+private fun handlerInServicingPhoto(
+ context : Context,
+ aMapLocation : AMapLocation?,
+ path : String?,
+ photoTemplateInfo : PhotoTemplateInfo,
+ success : (PhotoTemplateInfo) -> Unit,
+) {
+ LogUtil.print("handlerPhoto", "photoTemplateInfo==${photoTemplateInfo.toJson()}")
+ val time = System.currentTimeMillis().minus(photoTemplateInfo.advanceTime ?: 0L)
+ val photoTemplateInfoTemp = photoTemplateInfo.copy(photoLocalPath = path,
+ photoUploadPath = null,
+ realTakePhotoTime = TimeUtils.getNowString(),
+ photoSource = 1,
+ time = TimeUtils.millis2String(time),
+ lat = aMapLocation?.latitude?.toFloat(),
+ lng = aMapLocation?.longitude?.toFloat(),
+ address = aMapLocation?.address)
+ LogUtil.print("本地照片拍好返回结果",
+ photoTemplateInfoTemp.toJson() ?: photoTemplateInfoTemp.toString())
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = photoTemplateInfoTemp)
+ success(photoTemplateInfoTemp)
+
+ var localPhotoTemplateInfo = photoTemplateInfoTemp
+
+ //当需要添加水印并且地址不为空的情况下,才添加水印
+ val file =
+ if (localPhotoTemplateInfo.needWaterMarker == true && ! aMapLocation?.address.isNullOrBlank()) {
+ val photoMarkerInfo = PhotoMarkerInfo(localPhotoTemplateInfo.needWaterMarker,
+ needShowPhoneBrand = localPhotoTemplateInfo.needShowPhoneBrand,
+ path = localPhotoTemplateInfo.photoLocalPath,
+ from = "(相机)",
+ lat = localPhotoTemplateInfo.lat?.toDouble(),
+ lng = localPhotoTemplateInfo.lng?.toDouble(),
+ address = localPhotoTemplateInfo.address,
+ time = localPhotoTemplateInfo.time,
+ driverName = GlobalData.driverInfo?.userName,
+ taskCode = localPhotoTemplateInfo.taskCode)
+ File(PhotoMarkerManager.addPhotoMarker(context = context, photoMarkerInfo))
+ } else {
+ File(localPhotoTemplateInfo.photoLocalPath !!)
+ }
+
+ if (! file.exists()) {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ return
+ }
+
+ CommonMethod.uploadImage(file = file, preview = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 4,
+ photoLocalWaterMarkerPath = file.absolutePath)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ }, success = { it ->
+ if (photoTemplateInfo.recognizeType == null || photoTemplateInfo.recognizeType == 0) {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ return@uploadImage
+ }
+ ocrRecognition(localPhotoTemplateInfo, uploadPath = it, success = {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ }, failed = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 7)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+ }, {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ RoomHelper.db?.photoTemplateDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+}
+
+
+private fun handlerChangeBatteryPhoto(context : Context,
+ aMapLocation : AMapLocation?,
+ path : String?,
+ photoTemplateInfo : PhotoTemplateInfo,
+ success : (PhotoTemplateInfo) -> Unit) {
+ LogUtil.print("handlerPhoto", "photoTemplateInfo==${photoTemplateInfo.toJson()}")
+ val time = System.currentTimeMillis().minus(photoTemplateInfo.advanceTime ?: 0L)
+ val photoTemplateInfoTemp = photoTemplateInfo.copy(photoLocalPath = path,
+ photoUploadPath = null,
+ realTakePhotoTime = TimeUtils.getNowString(),
+ photoSource = 1,
+ time = TimeUtils.millis2String(time),
+ lat = aMapLocation?.latitude?.toFloat(),
+ lng = aMapLocation?.longitude?.toFloat(),
+ address = aMapLocation?.address)
+ LogUtil.print("本地照片拍好返回结果",
+ photoTemplateInfoTemp.toJson() ?: photoTemplateInfoTemp.toString())
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = photoTemplateInfoTemp)
+ success(photoTemplateInfoTemp)
+
+ var localPhotoTemplateInfo = photoTemplateInfoTemp
+ val file =
+ if (localPhotoTemplateInfo.needWaterMarker == true && ! aMapLocation?.address.isNullOrBlank()) {
+ val photoMarkerInfo = PhotoMarkerInfo(localPhotoTemplateInfo.needWaterMarker,
+ needShowPhoneBrand = localPhotoTemplateInfo.needShowPhoneBrand,
+ path = localPhotoTemplateInfo.photoLocalPath,
+ from = "(相机)",
+ lat = localPhotoTemplateInfo.lat?.toDouble(),
+ lng = localPhotoTemplateInfo.lng?.toDouble(),
+ address = localPhotoTemplateInfo.address,
+ time = localPhotoTemplateInfo.time,
+ driverName = GlobalData.driverInfo?.userName,
+ taskCode = localPhotoTemplateInfo.taskCode)
+ File(PhotoMarkerManager.addPhotoMarker(context = context, photoMarkerInfo))
+ } else {
+ File(localPhotoTemplateInfo.photoLocalPath !!)
+ }
+
+ if (! file.exists()) {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ return
+ }
+
+ CommonMethod.uploadImage(file = file, preview = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 4,
+ photoLocalWaterMarkerPath = file.absolutePath)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ }, success = { it ->
+ if (photoTemplateInfo.recognizeType == null || photoTemplateInfo.recognizeType == 0) {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ return@uploadImage
+ }
+
+ ocrRecognition(localPhotoTemplateInfo, uploadPath = it, success = {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ }, failed = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 7)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+
+ }, {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ RoomHelper.db?.changeBatteryDao()?.update(photoTemplateInfo = localPhotoTemplateInfo)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+}
+
+
+private fun handlerNormalPhoto(context : Context,
+ aMapLocation : AMapLocation?,
+ path : String?,
+ photoTemplateInfo : PhotoTemplateInfo,
+ success : (PhotoTemplateInfo) -> Unit) {
+ LogUtil.print("handlerNormalPhoto", "photoTemplateInfo==${photoTemplateInfo.toJson()}")
+ val time = System.currentTimeMillis().minus(photoTemplateInfo.advanceTime ?: 0L)
+ val photoTemplateInfoTemp = photoTemplateInfo.copy(photoLocalPath = path,
+ photoUploadPath = null,
+ realTakePhotoTime = TimeUtils.getNowString(),
+ photoSource = 1,
+ photoUploadStatus = 1,
+ time = TimeUtils.millis2String(time),
+ lat = aMapLocation?.latitude?.toFloat(),
+ lng = aMapLocation?.longitude?.toFloat(),
+ address = aMapLocation?.address)
+ LogUtil.print("本地照片拍好返回结果",
+ photoTemplateInfoTemp.toJson() ?: photoTemplateInfoTemp.toString())
+ success(photoTemplateInfoTemp)
+
+ var localPhotoTemplateInfo = photoTemplateInfoTemp
+ val file =
+ if (localPhotoTemplateInfo.needWaterMarker == true && ! aMapLocation?.address.isNullOrBlank()) {
+ val photoMarkerInfo = PhotoMarkerInfo(localPhotoTemplateInfo.needWaterMarker,
+ needShowPhoneBrand = localPhotoTemplateInfo.needShowPhoneBrand,
+ path = localPhotoTemplateInfo.photoLocalPath,
+ from = "(相机)",
+ lat = localPhotoTemplateInfo.lat?.toDouble(),
+ lng = localPhotoTemplateInfo.lng?.toDouble(),
+ address = localPhotoTemplateInfo.address,
+ time = localPhotoTemplateInfo.time,
+ driverName = GlobalData.driverInfo?.userName,
+ taskCode = localPhotoTemplateInfo.taskCode)
+ File(PhotoMarkerManager.addPhotoMarker(context = context, photoMarkerInfo))
+ } else {
+ File(localPhotoTemplateInfo.photoLocalPath !!)
+ }
+
+ if (! file.exists()) {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ success(localPhotoTemplateInfo)
+ return
+ }
+
+ CommonMethod.uploadImage(file = file, preview = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 4,
+ photoLocalWaterMarkerPath = file.absolutePath)
+ success(localPhotoTemplateInfo)
+ }, success = { it ->
+ if (photoTemplateInfo.recognizeType == null || photoTemplateInfo.recognizeType == 0) {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ return@uploadImage
+ }
+
+ ocrRecognition(localPhotoTemplateInfo, uploadPath = it, success = {
+ localPhotoTemplateInfo =
+ localPhotoTemplateInfo.copy(photoUploadStatus = 2, photoUploadPath = it)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传成功", it ?: "")
+ }, failed = {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 7)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+
+ }, {
+ localPhotoTemplateInfo = localPhotoTemplateInfo.copy(photoUploadStatus = 3)
+ success(localPhotoTemplateInfo)
+ LogUtil.print("图片上传失败", it ?: "")
+ })
+}
+
+private fun ocrRecognition(photoTemplateInfo : PhotoTemplateInfo,
+ uploadPath : String?,
+ success : () -> Unit,
+ failed : (String?) -> Unit) {
+ if (uploadPath.isNullOrBlank()) {
+ failed("照片路径为空!")
+ return
+ }
+ val orderPhotoOcrRecognizeRequest =
+ OrderPhotoOcrRecognizeRequest(GlobalData.currentOrder?.userOrderId,
+ photoTemplateInfo.recognizeType,
+ uploadPath)
+ RetrofitHelper.getDefaultService().orderPhotoOcrRecognize(orderPhotoOcrRecognizeRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : String?) {
+ success()
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ failed(msg)
+ }
+ })
+}
+
+
+@Composable
+fun InServicingPhotoView(modifier : Modifier = Modifier,
+ photoTemplateInfo : PhotoTemplateInfo,
+ index : Int? = null,
+ success : (PhotoTemplateInfo) -> Unit) {
+ Column(modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(6.dp))
+ .padding(10.dp)) {
+
+ if (photoTemplateInfo.photoType == 2) {
+ InServicingEleView()
+ return@Column
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ if (index != null) {
+ Box(modifier = Modifier
+ .size(20.dp)
+ .background(color = buttonBgColor, shape = RoundedCornerShape(4.dp)),
+ contentAlignment = Alignment.Center) {
+ Text(text = "$index",
+ fontWeight = FontWeight.Medium,
+ style = TextStyle.Default.copy(),
+ color = Color.White,
+ fontSize = 14.sp)
+ }
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(text = photoTemplateInfo.imageTitle ?: "其他",
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ color = Color.Black)
+ Spacer(modifier = Modifier.width(5.dp))
+ if (photoTemplateInfo.doHaveFilm == 1) {
+ Text(text = "* 必拍",
+ color = Color.Red,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = photoTemplateInfo.convertPhotoStatusStr(status = photoTemplateInfo.photoUploadStatus
+ ?: 0),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = photoTemplateInfo.getPhotoStatusColor())
+
+ Spacer(modifier = Modifier.width(10.dp))
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+
+ InServicingPhotoItemView(modifier = Modifier.fillMaxWidth(),
+ photoTemplateInfo = photoTemplateInfo,
+ success = { success(it) })
+
+ if (! photoTemplateInfo.imageDescription.isNullOrBlank()) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(color = Color(0xFFFEF5EB), shape = (RoundedCornerShape(4.dp)))
+ .padding(horizontal = 10.dp, vertical = 2.dp),
+ horizontalAlignment = Alignment.Start) {
+ Text(text = "提示:",
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color(0xFFFE5656))
+ Text(text = photoTemplateInfo.imageDescription,
+ color = Color.Black,
+ fontSize = 11.sp,
+ lineHeight = TextUnit(15f, TextUnitType.Sp))
+ }
+ }
+ }
+}
+
+
+@Composable
+fun InServicingPhotoViewIsCanClick(modifier : Modifier = Modifier,
+ photoTemplateInfo : PhotoTemplateInfo,
+ index : Int? = null,
+ isCanClick : Boolean = true,
+ success : (PhotoTemplateInfo) -> Unit) {
+ Column(modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(6.dp))
+ .padding(10.dp)) {
+
+ if (photoTemplateInfo.photoType == 2) {
+ InServicingEleView()
+ return
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ if (index != null) {
+ Box(modifier = Modifier
+ .size(20.dp)
+ .background(color = buttonBgColor, shape = RoundedCornerShape(4.dp)),
+ contentAlignment = Alignment.Center) {
+ Text(text = "$index",
+ fontWeight = FontWeight.Medium,
+ style = TextStyle.Default.copy(),
+ color = Color.White,
+ fontSize = 14.sp)
+ }
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(text = photoTemplateInfo.imageTitle ?: "其他",
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ fontSize = 14.sp,
+ color = Color.Black)
+ Spacer(modifier = Modifier.width(5.dp))
+ if (photoTemplateInfo.doHaveFilm == 1 && isCanClick) {
+ Text(text = "* 必拍",
+ color = Color.Red,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = photoTemplateInfo.convertPhotoStatusStr(status = photoTemplateInfo.photoUploadStatus
+ ?: 0),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = photoTemplateInfo.getPhotoStatusColor())
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+
+ InServicingPhotoItemView(modifier = Modifier.fillMaxWidth(),
+ photoTemplateInfo = photoTemplateInfo,
+ isCanClick = isCanClick,
+ success = { success(it) })
+
+ if (! photoTemplateInfo.imageDescription.isNullOrBlank()) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(color = Color(0xFFFEF5EB), shape = (RoundedCornerShape(4.dp)))
+ .padding(horizontal = 10.dp, vertical = 2.dp),
+ horizontalAlignment = Alignment.Start) {
+ Text(text = "提示:",
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color(0xFFFE5656))
+ Text(text = photoTemplateInfo.imageDescription,
+ color = Color.Black,
+ fontSize = 11.sp,
+ lineHeight = TextUnit(15f, TextUnitType.Sp))
+ }
+ }
+ }
+}
+
+
+@Composable
+fun InServicingPhotoWithoutTitleView(modifier : Modifier = Modifier,
+ photoTemplateInfo : PhotoTemplateInfo,
+ index : Int? = null,
+ success : (PhotoTemplateInfo) -> Unit) {
+ Column(modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(6.dp))
+ .padding(10.dp)) {
+
+ if (photoTemplateInfo.photoType == 2) {
+ InServicingEleView()
+ return
+ }
+
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ if (index != null) {
+ Box(modifier = Modifier
+ .size(20.dp)
+ .background(color = buttonBgColor, shape = RoundedCornerShape(4.dp)),
+ contentAlignment = Alignment.Center) {
+ Text(text = "$index",
+ fontWeight = FontWeight.Medium,
+ style = TextStyle.Default.copy(),
+ color = Color.White,
+ fontSize = 14.sp)
+ }
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+ if (photoTemplateInfo.doHaveFilm == 1) {
+ Text(text = "* 必拍",
+ color = Color.Red,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = photoTemplateInfo.convertPhotoStatusStr(status = photoTemplateInfo.photoUploadStatus
+ ?: 0),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = photoTemplateInfo.getPhotoStatusColor())
+
+ Spacer(modifier = Modifier.width(10.dp))
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+
+ InServicingPhotoItemView(modifier = Modifier.fillMaxWidth(),
+ photoTemplateInfo = photoTemplateInfo,
+ success = { success(it) })
+ }
+}
+
+@Composable
+fun InServicingPhotoItemView(modifier : Modifier = Modifier,
+ photoTemplateInfo : PhotoTemplateInfo,
+ isCanClick : Boolean = true,
+ success : (PhotoTemplateInfo) -> Unit) {
+ val context = LocalContext.current
+ val aMapLocation = remember { mutableStateOf(null) }
+ val executors = Executors.newFixedThreadPool(4)
+ val getResult =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { it ->
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra("path")
+ LogUtil.print("takePhoto", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ executors.execute {
+ when (photoTemplateInfo.myCustomPhotoType) {
+ Const.PhotoType.InServicing -> handlerInServicingPhoto(context,
+ aMapLocation.value,
+ value,
+ photoTemplateInfo) { success(it) }
+
+ Const.PhotoType.ChangeBattery -> handlerChangeBatteryPhoto(context,
+ aMapLocation.value,
+ value,
+ photoTemplateInfo) { success(it) }
+
+ Const.PhotoType.NormalImage -> handlerNormalPhoto(context,
+ aMapLocation.value,
+ value,
+ photoTemplateInfo) { success(it) }
+ }
+ }
+ }
+ }
+
+ val showPreviewPhotoDialog = remember { mutableStateOf(false) }
+ if (showPreviewPhotoDialog.value) {
+ CommonDialog(title = photoTemplateInfo.imageTitle ?: "预览",
+ confirm = { showPreviewPhotoDialog.value = false },
+ cancelText = "取消",
+ confirmText = "确定",
+ cancelEnable = true,
+ cancel = {
+ showPreviewPhotoDialog.value = false
+ },
+ dismiss = {
+ showPreviewPhotoDialog.value = false
+ },
+ content = {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp),
+ contentAlignment = Alignment.Center) {
+ AsyncImage(model = photoTemplateInfo.photoUrl,
+ contentDescription = "",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillWidth)
+ }
+ })
+ }
+
+ //重拍弹窗
+ val showReTakePhotoDialog = remember { mutableStateOf(false) }
+ if (showReTakePhotoDialog.value) {
+ ReTakePhotoDialog(title = photoTemplateInfo.imageTitle ?: "预览",
+ confirm = {
+ showReTakePhotoDialog.value = false
+ if (! PermissionX.isGranted(context,
+ android.Manifest.permission.ACCESS_FINE_LOCATION)
+ ) {
+ ToastUtils.showShort("定位权限未开启!")
+ return@ReTakePhotoDialog
+ }
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ aMapLocation.value = it
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ getResult.launch(intent)
+ }, failed = {
+ LoadingManager.hideLoading()
+ aMapLocation.value = GlobalData.currentLocation
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ getResult.launch(intent)
+ ToastUtils.showShort(it)
+ LogUtil.print("上传图片定位获取失败",
+ "使用全局定位,location==${GlobalData.currentLocation.toJson()}")
+ })
+ },
+ cancel = {
+ showReTakePhotoDialog.value = false
+ },
+ againSubmit = {
+ showReTakePhotoDialog.value = false
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = { it ->
+ LoadingManager.hideLoading()
+ when (photoTemplateInfo.myCustomPhotoType) {
+ Const.PhotoType.InServicing -> handlerInServicingPhoto(context,
+ it,
+ photoTemplateInfo.photoLocalPath,
+ photoTemplateInfo) { success(it) }
+
+ Const.PhotoType.ChangeBattery -> handlerChangeBatteryPhoto(context,
+ it,
+ photoTemplateInfo.photoLocalPath,
+ photoTemplateInfo) { success(it) }
+ }
+ }, failed = {
+ LoadingManager.hideLoading()
+ aMapLocation.value = GlobalData.currentLocation
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ getResult.launch(intent)
+ ToastUtils.showShort(it)
+ LogUtil.print("上传图片定位获取失败",
+ "使用全局定位,location==${GlobalData.currentLocation.toJson()}")
+ })
+ },
+ dismiss = {
+ showReTakePhotoDialog.value = false
+ },
+ path = photoTemplateInfo.photoLocalPath,
+ showAgain = photoTemplateInfo.photoUploadPath.isNullOrBlank())
+ }
+
+ Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
+ Box(contentAlignment = Alignment.TopStart, modifier = Modifier
+ .weight(1f)
+ .height(100.dp)) {
+ if (photoTemplateInfo.myCustomPhotoType == Const.PhotoType.ChangeBattery) {
+ AsyncImage(model = photoTemplateInfo.photoUrl?.toIntOrNull(),
+ contentDescription = "",
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable { showPreviewPhotoDialog.value = true }
+ .clip(shape = RoundedCornerShape(3.dp)),
+ contentScale = ContentScale.FillBounds)
+ } else {
+ AsyncImage(model = photoTemplateInfo.photoUrl,
+ contentDescription = "",
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable { showPreviewPhotoDialog.value = true }
+ .clip(shape = RoundedCornerShape(3.dp)),
+ contentScale = ContentScale.FillBounds)
+ }
+
+ AsyncImage(model = R.drawable.sv_photo_example,
+ contentDescription = "",
+ modifier = Modifier.size(52.dp, 14.dp),
+ contentScale = ContentScale.FillWidth)
+ }
+
+ Spacer(modifier = Modifier.width(10.dp))
+ Box(modifier = Modifier
+ .weight(1f)
+ .height(100.dp)) {
+ AsyncImage(model = photoTemplateInfo.photoLocalPath ?: photoTemplateInfo.photoUploadPath
+ ?: R.drawable.sv_servicing_take_photo,
+ contentDescription = "",
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable {
+ if (! isCanClick) {
+ return@clickable
+ }
+ if (photoTemplateInfo.photoLocalPath.isNullOrBlank()) {
+ if (! PermissionX.isGranted(context,
+ android.Manifest.permission.ACCESS_FINE_LOCATION)
+ ) {
+ ToastUtils.showShort("定位权限未开启!")
+ return@clickable
+ }
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ aMapLocation.value = it
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ getResult.launch(intent)
+ }, failed = {
+ LoadingManager.hideLoading()
+ aMapLocation.value = GlobalData.currentLocation
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ getResult.launch(intent)
+ ToastUtils.showShort(it)
+ LogUtil.print("上传图片定位获取失败",
+ "使用全局定位,location==${GlobalData.currentLocation.toJson()}")
+ })
+ return@clickable
+ }
+ showReTakePhotoDialog.value = true
+ }
+ .clip(shape = RoundedCornerShape(3.dp)),
+ contentScale = ContentScale.FillBounds)
+ }
+ }
+}
+
+@Composable
+private fun InServicingEleView() {
+ val context = LocalContext.current
+ val webView = WebView(context)
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)) {
+ AndroidView(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp), factory = {
+ webView.webChromeClient = WebChromeClient()
+ val webSetting = webView.settings
+ webSetting.allowFileAccess = true
+ webSetting.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
+ webSetting.setSupportZoom(true)
+ webSetting.builtInZoomControls = true
+ webSetting.useWideViewPort = true
+ webSetting.loadWithOverviewMode = true
+ webSetting.setSupportMultipleWindows(false)
+ webSetting.setAppCacheEnabled(true)
+ webSetting.domStorageEnabled = true
+ webSetting.javaScriptEnabled = true
+ webSetting.setGeolocationEnabled(true)
+ webSetting.setAppCacheMaxSize(Long.MAX_VALUE)
+ webSetting.blockNetworkImage = false // 解决图片不显示
+ webSetting.mixedContentMode = 0
+ webSetting.setAppCachePath(context.getDir("appcache", 0).path)
+ webSetting.databasePath = context.getDir("databases", 0).path
+ webView
+ }, update = { webView.loadUrl(GlobalData.currentOrder?.getEleOrderH5Url()) })
+ Box(Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .noDoubleClick {
+ CommonH5Activity.goH5Activity(context,
+ url = GlobalData.currentOrder?.getEleOrderH5Url(),
+ title = "电子工单",
+ isCanBack = true)
+ })
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleActivity.kt b/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleActivity.kt
new file mode 100644
index 0000000..8872771
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleActivity.kt
@@ -0,0 +1,102 @@
+package com.za.ui.servicing.check_vehicle
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.BaseActivity
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.ui.servicing.InServicingPhotoView
+import com.za.ui.servicing.view.InServicingHeadView
+
+class CheckVehicleActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ CheckVehicleScreen()
+ }
+}
+
+@Composable
+fun CheckVehicleScreen(vm: CheckVehicleVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(CheckVehicleVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = { vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false))) },
+ confirm = {
+ vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(CheckVehicleVm.Action.UpdateTask)
+ })
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "离线上传",
+ title = "任务提交失败,是否进行离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = { vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false))) },
+ confirm = {
+ vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(CheckVehicleVm.Action.UpdateOffline)
+ })
+ }
+
+
+ Scaffold(topBar = { InServicingHeadView(title = "现场验车", orderInfo = uiState.value.orderInfo, onBack = { context.finish() }) }, bottomBar = {
+ CommonButton(text = "验车完成") {
+ vm.dispatch(CheckVehicleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ contentPadding = PaddingValues(10.dp)) {
+
+ itemsIndexed(items = uiState.value.photoTemplateList
+ ?: arrayListOf(), key = { _, item -> item.hashCode() }) { index: Int, item: PhotoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = item, index = index + 1, success = {
+ vm.dispatch(CheckVehicleVm.Action.UpdatePhotoTemplate(it))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
+
diff --git a/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleVm.kt b/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleVm.kt
new file mode 100644
index 0000000..d149d37
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/check_vehicle/CheckVehicleVm.kt
@@ -0,0 +1,258 @@
+package com.za.ui.servicing.check_vehicle
+
+import com.alibaba.fastjson.JSONObject
+import com.amap.api.location.AMapLocation
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getNextStatus
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.service.location.ZdLocationManager
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class CheckVehicleVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ is Action.UpdateOffline -> updateOffline()
+ }
+ }
+
+ private fun updateOffline() {
+ uiState.value.photoTemplateList?.forEachIndexed { index, photoTemplateInfo ->
+ if (photoTemplateInfo.photoType == 2) {
+ return@forEachIndexed
+ }
+
+ if (! photoTemplateInfo.photoLocalWaterMarkerPath.isNullOrBlank() && photoTemplateInfo.photoUploadPath.isNullOrBlank()) {
+ val offlineUpdateTaskBean =
+ OfflineUpdateTaskBean(realTakePhotoTime = photoTemplateInfo.realTakePhotoTime,
+ photoSource = photoTemplateInfo.photoSource,
+ time = photoTemplateInfo.time,
+ imageLat = photoTemplateInfo.lat,
+ imageLng = photoTemplateInfo.lng,
+ imageAddress = photoTemplateInfo.address,
+ imageIndex = index,
+ needWater = photoTemplateInfo.needWaterMarker,
+ needPhoneBrand = photoTemplateInfo.needShowPhoneBrand,
+ imageLocalPath = photoTemplateInfo.photoLocalPath,
+ advanceTime = getCurrentOrder()?.advanceTime,
+ photoLocalWaterMarkerPath = photoTemplateInfo.photoLocalWaterMarkerPath,
+ offlineMode = 1,
+ taskId = getCurrentOrder()?.taskId,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "${photoTemplateInfo.imageTitle}",
+ offlineType = 2)
+ insertOfflineTask(offlineUpdateTaskBean)
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ doUploadOfflineTask(it, tempPhotoList = tempPhotoList)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ LogUtil.print("$tag updateOffline", "定位获取失败$it")
+ })
+ }
+
+ private fun doUploadOfflineTask(it : AMapLocation?, tempPhotoList : ArrayList) {
+ val taskRequest = UpdateTaskRequest(type = "CHECK_VEHICLE",
+ taskId = getCurrentOrder()?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "EXAMINE",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it?.latitude,
+ lng = it?.longitude,
+ address = it?.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(type = taskRequest.type,
+ taskId = taskRequest.taskId,
+ userId = taskRequest.userId,
+ vehicleId = taskRequest.vehicleId,
+ currentState = taskRequest.currentState,
+ offlineMode = 1,
+ operateTime = taskRequest.operateTime,
+ updateTaskLat = taskRequest.lat,
+ updateTaskLng = taskRequest.lng,
+ updateTaskAddress = taskRequest.address,
+ templatePhotoInfoList = taskRequest.templatePhotoInfoList,
+ advanceTime = getCurrentOrder()?.advanceTime,
+ taskCode = getCurrentOrder()?.taskCode,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ offlineTitle = "现场验车",
+ offlineType = 1)
+ insertOfflineTask(offlineUpdateTaskBean)
+ updateOrder(getCurrentOrder()?.copy(taskState = getCurrentOrder()?.getNextStatus()))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(nextState = getCurrentOrder()?.taskState),
+ orderInfo = getCurrentOrder()))
+ }
+ }
+
+ private fun updateTemplate() {
+ getCurrentPhotoTemplate(success = {
+ updateState(uiState.value.copy(photoTemplateList = it))
+ })
+ }
+
+ private fun updateTask() {
+ if (uiState.value.photoTemplateList.isNullOrEmpty()) {
+ ToastUtils.showShort("现场照片不能为空!")
+ return
+ }
+
+ //先检查照片是否是为空
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.doHaveFilm == 1 && it.photoLocalWaterMarkerPath.isNullOrBlank()) {
+ ToastUtils.showLong("请上传 ${it.imageTitle}!!")
+ return
+ }
+ }
+
+ //先判断是否有离线任务
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateOffline()
+ return
+ }
+
+
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.photoUploadStatus == 4) {
+ ToastUtils.showLong("${it.photoName}正在上传,请等待照片上传完成!")
+ return
+ }
+ if (! it.photoLocalWaterMarkerPath.isNullOrBlank() && it.photoUploadPath.isNullOrBlank()) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ return
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(type = "CHECK_VEHICLE",
+ taskId = getCurrentOrder()?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "EXAMINE",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude,
+ address = it.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ LogUtil.print("$tag updateTask", "定位获取失败==$it")
+ })
+ }
+
+ private fun doUploadTask(request : UpdateTaskRequest) {
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = {
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ LogUtil.print("$tag doUploadTask", "状态更新成功==$request")
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ getCurrentPhotoTemplate(success = {
+ updateState(uiState.value.copy(photoTemplateList = it, orderInfo = getCurrentOrder()))
+ }, failure = {
+ ToastUtils.showShort(it)
+ })
+ }
+
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo : PhotoTemplateInfo) : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ data object UpdateOffline : Action()
+ }
+
+ data class UiState(val orderInfo : OrderInfo? = null,
+ val verifyValue : String? = null,
+ val goNextPage : UpdateTaskBean? = null,
+ val isGoNextPageDialog : Boolean? = null,
+ val showCallPhoneDialog : Boolean? = null,
+ val showOfflineDialog : Boolean? = null,
+ val photoTemplateList : List? = null)
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoActivity.kt b/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoActivity.kt
new file mode 100644
index 0000000..3f3c16a
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoActivity.kt
@@ -0,0 +1,91 @@
+package com.za.ui.servicing.departure_photo
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.BaseActivity
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.ui.servicing.InServicingPhotoView
+import com.za.ui.servicing.view.InServicingHeadView
+
+class DeparturePhotoActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ DeparturePhotoScreen()
+ }
+}
+
+@Composable
+fun DeparturePhotoScreen(vm : DeparturePhotoVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(DeparturePhotoVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(DeparturePhotoVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(DeparturePhotoVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(DeparturePhotoVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ vm.dispatch(DeparturePhotoVm.Action.UpdateTask)
+ })
+ }
+
+ Scaffold(topBar = {
+ InServicingHeadView(title = "发车前照片",
+ orderInfo = uiState.value.orderInfo,
+ onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "验车完成") {
+ vm.dispatch(DeparturePhotoVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ contentPadding = PaddingValues(10.dp)) {
+
+ itemsIndexed(items = uiState.value.photoTemplateList ?: arrayListOf(),
+ key = { _, item -> item.hashCode() }) { index : Int, item : PhotoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = item, index = index + 1, success = {
+ vm.dispatch(DeparturePhotoVm.Action.UpdatePhotoTemplate(it))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoVm.kt b/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoVm.kt
new file mode 100644
index 0000000..c5cbe55
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/departure_photo/DeparturePhotoVm.kt
@@ -0,0 +1,145 @@
+package com.za.ui.servicing.departure_photo
+
+import com.alibaba.fastjson.JSONObject
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.service.location.ZdLocationManager
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class DeparturePhotoVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ }
+ }
+
+ private fun updateTemplate() {
+ getDeparturePhotoTemplate(success = {
+ updateState(uiState.value.copy(photoTemplateList = it))
+ }, failure = { ToastUtils.showShort(it) })
+ }
+
+ private fun updateTask() {
+ if (uiState.value.photoTemplateList.isNullOrEmpty()) {
+ ToastUtils.showShort("现场照片不能为空!")
+ return
+ }
+
+ //先检查照片是否是为空
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.doHaveFilm == 1 && it.photoLocalWaterMarkerPath.isNullOrBlank()) {
+ ToastUtils.showLong("请上传 ${it.imageTitle}!!")
+ return
+ }
+ }
+
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.photoUploadStatus == 4) {
+ ToastUtils.showLong("${it.photoName}正在上传,请等待照片上传完成!")
+ return
+ }
+ if (! it.photoLocalWaterMarkerPath.isNullOrBlank() && it.photoUploadPath.isNullOrBlank()) {
+ ToastUtils.showLong("请上传 ${it.imageTitle}!!")
+ return
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(type = "START",
+ taskId = getCurrentOrder()?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = GlobalData.currentOrder?.taskState,
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude,
+ address = it.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ LogUtil.print("$tag updateTask", "定位获取失败==$it")
+ })
+ }
+
+ private fun doUploadTask(request : UpdateTaskRequest) {
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = {
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ LogUtil.print("$tag doUploadTask", "状态更新成功==$request")
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ getDeparturePhotoTemplate(success = {
+ updateState(uiState.value.copy(photoTemplateList = it, orderInfo = getCurrentOrder()))
+ }, failure = { ToastUtils.showShort(it) })
+ }
+
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo : PhotoTemplateInfo) : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ }
+
+ data class UiState(val orderInfo : OrderInfo? = null,
+ val verifyValue : String? = null,
+ val goNextPage : UpdateTaskBean? = null,
+ val isGoNextPageDialog : Boolean? = null,
+ val showCallPhoneDialog : Boolean? = null,
+ val photoTemplateList : List? = null)
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoActivity.kt b/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoActivity.kt
new file mode 100644
index 0000000..755fde6
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoActivity.kt
@@ -0,0 +1,103 @@
+package com.za.ui.servicing.destination_photo
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.BaseActivity
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.ui.servicing.InServicingPhotoView
+import com.za.ui.servicing.view.InServicingHeadView
+
+class DestinationPhotoActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ DestinationPhotoScreen()
+ }
+}
+
+@Composable
+fun DestinationPhotoScreen(vm: DestinationPhotoVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(DestinationPhotoVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = { vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false))) },
+ confirm = {
+ vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(DestinationPhotoVm.Action.UpdateTask)
+ })
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "离线上传",
+ title = "任务提交失败,是否离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = { vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false))) },
+ confirm = {
+ vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(DestinationPhotoVm.Action.UpdateOffline)
+ })
+ }
+
+ Scaffold(topBar = {
+ InServicingHeadView(title = "到达目的地照片",
+ orderInfo = uiState.value.orderInfo,
+ onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "上传照片") {
+ vm.dispatch(DestinationPhotoVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ contentPadding = PaddingValues(10.dp)) {
+ itemsIndexed(items = uiState.value.photoTemplateList
+ ?: arrayListOf(), key = { _, item -> item.hashCode() }) { index: Int, item: PhotoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = item, index = index + 1, success = {
+ vm.dispatch(DestinationPhotoVm.Action.UpdatePhotoTemplate(it))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoVm.kt b/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoVm.kt
new file mode 100644
index 0000000..802e365
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/destination_photo/DestinationPhotoVm.kt
@@ -0,0 +1,257 @@
+package com.za.ui.servicing.destination_photo
+
+import com.alibaba.fastjson.JSONObject
+import com.amap.api.location.AMapLocation
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getNextStatus
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.service.location.ZdLocationManager
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class DestinationPhotoVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ is Action.UpdateOffline -> updateOffline()
+ }
+ }
+
+ private fun updateTemplate() {
+ getCurrentPhotoTemplate(success = { updateState(uiState.value.copy(photoTemplateList = it)) })
+ }
+
+
+ private fun updateOffline() {
+ uiState.value.photoTemplateList?.forEachIndexed { index, photoTemplateInfo ->
+ if (photoTemplateInfo.photoType == 2) {
+ return@forEachIndexed
+ }
+
+ if (!photoTemplateInfo.photoLocalWaterMarkerPath.isNullOrBlank() && photoTemplateInfo.photoUploadPath.isNullOrBlank()) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ realTakePhotoTime = photoTemplateInfo.realTakePhotoTime,
+ photoSource = photoTemplateInfo.photoSource,
+ time = photoTemplateInfo.time,
+ imageLat = photoTemplateInfo.lat,
+ imageLng = photoTemplateInfo.lng,
+ imageAddress = photoTemplateInfo.address,
+ imageIndex = index,
+ needWater = photoTemplateInfo.needWaterMarker,
+ needPhoneBrand = photoTemplateInfo.needShowPhoneBrand,
+ imageLocalPath = photoTemplateInfo.photoLocalPath,
+ advanceTime = uiState.value.orderInfo?.advanceTime,
+ photoLocalWaterMarkerPath = photoTemplateInfo.photoLocalWaterMarkerPath,
+ offlineMode = 1,
+ taskId = uiState.value.orderInfo?.taskId,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ offlineTitle = "${photoTemplateInfo.imageTitle}",
+ offlineType = 2)
+ insertOfflineTask(offlineUpdateTaskBean)
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true,
+ success = {
+ LoadingManager.hideLoading()
+ doUploadOfflineTask(it, tempPhotoList = tempPhotoList)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ LogUtil.print("$tag updateOffline", "定位获取失败$it")
+ })
+ }
+
+ private fun doUploadOfflineTask(it: AMapLocation?, tempPhotoList: ArrayList) {
+ val taskRequest = UpdateTaskRequest(type = "DEST_PHOTO",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ flowType = GlobalData.currentOrder?.flowType,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "DESTPHOTO",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it?.latitude,
+ lng = it?.longitude,
+ address = it?.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ type = taskRequest.type,
+ taskId = taskRequest.taskId,
+ userId = taskRequest.userId,
+ vehicleId = taskRequest.vehicleId,
+ currentState = taskRequest.currentState,
+ offlineMode = 1,
+ operateTime = taskRequest.operateTime,
+ updateTaskLat = taskRequest.lat,
+ updateTaskLng = taskRequest.lng,
+ flowType = taskRequest.flowType,
+ templatePhotoInfoList = taskRequest.templatePhotoInfoList,
+ updateTaskAddress = taskRequest.address,
+ advanceTime = uiState.value.orderInfo?.advanceTime,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ offlineTitle = "目的地照片",
+ offlineType = 1)
+ insertOfflineTask(offlineUpdateTaskBean)
+ updateOrder(getCurrentOrder()?.copy(taskState = getCurrentOrder()?.getNextStatus()))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(nextState = getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }
+
+ private fun updateTask() {
+ if (uiState.value.photoTemplateList.isNullOrEmpty()) {
+ ToastUtils.showShort("作业照片不能为空!")
+ return
+ }
+
+ //先检查照片是否是为空
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.doHaveFilm == 1 && it.photoLocalWaterMarkerPath.isNullOrBlank()) {
+ ToastUtils.showLong("请上传 ${it.imageTitle}!!")
+ return
+ }
+ }
+
+ //先判断是否有离线任务
+ if (!getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateOffline()
+ return
+ }
+
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+
+ if (it.photoUploadStatus == 4) {
+ ToastUtils.showLong("${it.photoName}正在上传,请等待照片上传完成!")
+ return
+ }
+
+ if (!it.photoLocalWaterMarkerPath.isNullOrBlank() && it.photoUploadPath.isNullOrBlank()) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ return
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(type = "DEST_PHOTO",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ flowType = GlobalData.currentOrder?.flowType,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "DESTPHOTO",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude,
+ address = it.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun doUploadTask(request: UpdateTaskRequest) {
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = {
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ getCurrentPhotoTemplate({
+ updateState(uiState.value.copy(photoTemplateList = it, orderInfo = getCurrentOrder()))
+ }, failure = {
+ ToastUtils.showShort(it)
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo: PhotoTemplateInfo) : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object UpdateOffline : Action()
+ }
+
+ data class UiState(val orderInfo: OrderInfo? = null,
+ val verifyValue: String? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val showCallPhoneDialog: Boolean? = null,
+ val showOfflineDialog: Boolean? = null,
+ val photoTemplateList: List? = null)
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckScreen.kt b/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckScreen.kt
new file mode 100644
index 0000000..da2f382
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckScreen.kt
@@ -0,0 +1,305 @@
+package com.za.ui.servicing.ele_check
+
+import android.app.Activity
+import android.content.Intent
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.RadioButtonDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.theme.black90
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.bean.db.ele.EleCarDamagePhotoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.common.log.LogUtil
+import com.za.common.util.ServicingSpeechManager
+import com.za.ext.callPhone
+import com.za.ext.finish
+import com.za.ext.goStatusPage
+import com.za.servicing.R
+import com.za.ui.camera.ZdCameraXActivity
+import com.za.ui.servicing.view.InServicingHeadView
+
+@Composable
+fun EleSignCheckScreen(vm: EleSignCheckVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(EleSignCheckVm.Action.Init)
+ ServicingSpeechManager.playNoUploadEleOrderWork(context)
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = { vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false))) },
+ confirm = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(EleSignCheckVm.Action.Upload)
+ })
+ }
+
+ if (uiState.value.goNextPage != null) {
+ uiState.value.orderInfo?.goStatusPage(context)
+ context.finish()
+ }
+
+ if (uiState.value.showCallPhoneDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "确定",
+ title = "是否联系中道客服?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false)))
+ },
+ dismiss = { vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false))) },
+ confirm = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showCallPhoneDialog = false)))
+ context.callPhone(uiState.value.orderInfo?.hotline)
+ })
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "离线上传",
+ title = "上传失败,是否进行离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = { vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false))) },
+ confirm = {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(EleSignCheckVm.Action.UploadOffline)
+ })
+ }
+
+
+ Scaffold(topBar = {
+ InServicingHeadView(title = "现场验车", orderInfo = uiState.value.orderInfo, onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "生成电子工单") {
+ vm.dispatch(EleSignCheckVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(it)) {
+ EleSignCheckContent(eleWorkOrderBean = uiState.value.eleWorkOrderBean,
+ photoList = uiState.value.damagePhoto,
+ dispatch = { vm.dispatch(it) })
+ }
+ }
+}
+
+@Composable
+private fun EleSignCheckContent(eleWorkOrderBean: EleWorkOrderBean?,
+ photoList: List?,
+ dispatch: (EleSignCheckVm.Action) -> Unit) {
+
+ val context = LocalContext.current
+ val takePhoto = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { it ->
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra("path")
+ LogUtil.print("takePhoto", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ dispatch(EleSignCheckVm.Action.AddDamagePhoto(value))
+ }
+ }
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(4.dp))) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(brush = Brush.verticalGradient(arrayListOf(Color(0xFF97A0BA), Color(0xFFE1E8F3))),
+ shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
+ .padding(10.dp)) {
+ Text(text = "验车拍照", color = Color(0xFF213B54), fontWeight = FontWeight.Bold, fontSize = 16.sp)
+ Row(modifier = Modifier
+ .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
+ AsyncImage(model = R.drawable.sv_warn_yellow, contentDescription = "", modifier = Modifier.size(12.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "请仔细检查车辆是否有损伤,如有损伤请仔细拍摄损伤部位照片",
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ color = Color(0xFFFF6337))
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(text = "车辆是否有损伤?", fontWeight = FontWeight.Medium, color = Color.Black, fontSize = 14.sp, modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 10.dp))
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start) {
+ RadioButton(selected = (eleWorkOrderBean?.hasBad != null && eleWorkOrderBean.hasBad == false),
+ onClick = {
+ dispatch(EleSignCheckVm.Action.ChangeDamageState(false))
+ },
+ colors = RadioButtonDefaults.colors()
+ .copy(selectedColor = Color.Red))
+ Text(text = "无损伤", fontSize = 14.sp, color = black90, fontWeight = FontWeight.Medium)
+ }
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start) {
+ RadioButton(selected = (eleWorkOrderBean?.hasBad != null && eleWorkOrderBean.hasBad == true),
+ onClick = {
+ dispatch(EleSignCheckVm.Action.ChangeDamageState(true))
+ },
+ colors = RadioButtonDefaults.colors()
+ .copy(selectedColor = Color.Red))
+
+ Text(text = "有损伤", fontSize = 14.sp, color = black90, fontWeight = FontWeight.Medium)
+ }
+
+ Text(text = "请拍摄损伤照片:", fontSize = 14.sp, color = Color.Black, fontWeight = FontWeight.Medium, modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 10.dp))
+
+ if (!photoList.isNullOrEmpty()) {
+ LazyVerticalGrid(columns = GridCells.Fixed(3), contentPadding = PaddingValues(10.dp)) {
+ itemsIndexed(items = photoList) { _, item ->
+ DamageItemView(item, dispatch = { dispatch(it) })
+ }
+ }
+ }
+ AsyncImage(model = R.drawable.svg_take_photo_orange,
+ contentDescription = "",
+ modifier = Modifier
+ .size(99.dp, 67.dp)
+ .padding(10.dp)
+ .clickable {
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ takePhoto.launch(intent)
+ },
+ contentScale = ContentScale.FillBounds)
+ }
+}
+
+@Composable
+private fun DamageItemView(damagePhotoBean: EleCarDamagePhotoBean,
+ dispatch: (EleSignCheckVm.Action) -> Unit) {
+ val context = LocalContext.current
+ val showReloadDialog = remember { mutableStateOf(false) }
+ val reTakePhoto = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { it ->
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra("path")
+ LogUtil.print("takePhoto", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ dispatch(EleSignCheckVm.Action.ReUploadPhoto(damagePhotoBean.copy(path = value, serverPath = null, uploadStatus = 2)))
+ }
+ }
+ //重拍弹窗
+ if (showReloadDialog.value) {
+ CommonDialog(title = "预览",
+ confirm = {
+ showReloadDialog.value = false
+ val intent = Intent(context, ZdCameraXActivity::class.java)
+ reTakePhoto.launch(intent)
+ },
+
+ cancelText = "取消",
+ confirmText = "重拍",
+ cancelEnable = true,
+ cancel = {
+ showReloadDialog.value = false
+ },
+ dismiss = {
+ showReloadDialog.value = false
+ }, content = {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp), contentAlignment = Alignment.Center) {
+ AsyncImage(model = damagePhotoBean.path,
+ contentDescription = "",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillWidth)
+ }
+ })
+ }
+
+ Box(modifier = Modifier
+ .size(110.dp, 76.dp)
+ .padding(5.dp)
+ .clickable { showReloadDialog.value = true },
+ contentAlignment = Alignment.BottomCenter) {
+ AsyncImage(model = damagePhotoBean.path, contentDescription = "", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillWidth)
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(18.dp)
+ .background(color = Color(0xFF16B276).takeIf { damagePhotoBean.uploadStatus != 3 }
+ ?: Color(0xFFC5331F))
+ .alpha(0.7f), contentAlignment = Alignment.Center) {
+ Text(text = damagePhotoBean.getStatusStr(),
+ fontWeight = FontWeight.Medium,
+ style = TextStyle.Default.copy(),
+ color = Color.White, fontSize = 10.sp,
+ textAlign = TextAlign.Center)
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckVm.kt b/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckVm.kt
new file mode 100644
index 0000000..1264487
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/ele_check/EleSignCheckVm.kt
@@ -0,0 +1,266 @@
+package com.za.ui.servicing.ele_check
+
+import com.blankj.utilcode.util.TimeUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.ele.EleCarDamagePhotoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.SaveEleOrderRequest
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.room.RoomHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.io.File
+import java.util.Date
+
+class EleSignCheckVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.ChangeDamageState -> changeDamageState(action.hasDamage)
+ is Action.AddDamagePhoto -> addDamagePhoto(action.path)
+ is Action.ReUploadPhoto -> reUploadPhoto(action.eleCarDamagePhotoBean)
+ is Action.Upload -> upload()
+ is Action.UploadOffline -> uploadOffline()
+ }
+ }
+
+
+ private fun uploadOffline() {
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(GlobalData.currentOrder?.taskId
+ ?: 0)
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+ val photos = RoomHelper.db?.eleCarDamagePhotoDao()?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId
+ ?: 0)
+
+ var list: ArrayList? = null
+ if (!photos.isNullOrEmpty()) {
+ list = arrayListOf()
+ photos.forEachIndexed { index, eleCarDamagePhotoBean ->
+ if (eleCarDamagePhotoBean.serverPath.isNullOrBlank() && !eleCarDamagePhotoBean.path.isNullOrBlank()) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ imageLocalPath = eleCarDamagePhotoBean.path,
+ imageIndex = index, eleState = 1,
+ userOrderId = GlobalData.currentOrder?.userOrderId,
+ taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "损伤照片-$index",
+ offlineType = 4)
+ insertOfflineTask(offlineUpdateTaskBean)
+ list.add(null)
+ } else {
+ list.add(eleCarDamagePhotoBean.serverPath)
+ }
+ }
+ }
+
+ val offlineEleWorkOrderBean = OfflineUpdateTaskBean(eleState = 1,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ taskId = getCurrentOrder()?.taskId,
+ damageFileList = list,
+ hasDamage = if (uiState.value.eleWorkOrderBean?.hasBad == true) {
+ 1
+ } else {
+ 2
+ },
+ offlineType = 3,
+ offlineTitle = "现场验车-电子工单损伤照片")
+ insertOfflineTask(offlineEleWorkOrderBean)
+
+ val ele = eleWorkOrderBean.copy(orderWorkStatus = 1)
+ RoomHelper.db?.eleWorkOrderDao()?.update(ele)
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(GlobalData.currentOrder?.taskState)))
+ }
+
+ private fun upload() {
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(GlobalData.currentOrder?.taskId
+ ?: 0)
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+ val photos = RoomHelper.db?.eleCarDamagePhotoDao()?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId
+ ?: 0)
+
+ if (eleWorkOrderBean.hasBad == true && photos.isNullOrEmpty()) {
+ ToastUtils.showLong("请先拍摄损伤照片!")
+ return
+ }
+ var list: ArrayList? = null
+ if (!photos.isNullOrEmpty()) {
+ list = arrayListOf()
+ photos.forEach {
+ if (!it.serverPath.isNullOrBlank()) {
+ list.add(it.serverPath)
+ }
+ }
+ }
+
+ val saveEleOrderRequest = SaveEleOrderRequest(
+ state = 1,
+ userOrderId = GlobalData.currentOrder?.userOrderId,
+ taskOrderId = GlobalData.currentOrder?.taskId,
+ damageFileList = list,
+ hasDamage = if (uiState.value.eleWorkOrderBean?.hasBad == true) {
+ 1
+ } else {
+ 2
+ },
+ )
+
+ if (!getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ uploadOffline()
+ return
+ }
+
+ LoadingManager.showLoading()
+ LogUtil.print("eleSign_check upload request", saveEleOrderRequest.toJson() ?: "")
+ RetrofitHelper.getDefaultService().saveElectronOrder(saveEleOrderRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(orderWorkStatus = 1))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showLong(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("eleSign_check upload failed", msg ?: "")
+ }
+ })
+ }
+
+ private fun reUploadPhoto(eleCarDamagePhotoBean: EleCarDamagePhotoBean) {
+ if (eleCarDamagePhotoBean.path.isNullOrBlank()) {
+ RoomHelper.db?.eleCarDamagePhotoDao()?.insert(eleCarDamagePhotoBean.copy(path = null, uploadStatus = null, serverPath = null))
+ return
+ }
+ RoomHelper.db?.eleCarDamagePhotoDao()?.insert(eleCarDamagePhotoBean)
+ updateState(uiState.value.copy(damagePhoto = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId ?: 0)))
+ CommonMethod.uploadImage(file = File(eleCarDamagePhotoBean.path),
+ success = {
+ RoomHelper.db?.eleCarDamagePhotoDao()?.update(eleCarDamagePhotoBean.copy(uploadStatus = 1, serverPath = it))
+ updateState(uiState.value.copy(damagePhoto = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId ?: 0)))
+ }, failed = {
+ RoomHelper.db?.eleCarDamagePhotoDao()?.update(eleCarDamagePhotoBean.copy(uploadStatus = 3))
+ updateState(uiState.value.copy(damagePhoto = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId ?: 0)))
+ LogUtil.print("addDamagePhoto failed", it ?: "")
+ })
+ }
+
+ private fun addDamagePhoto(localPath: String) {
+ val eleCarDamagePhotoBean = EleCarDamagePhotoBean(
+ path = localPath,
+ orderId = GlobalData.currentOrder?.taskId,
+ uploadStatus = 2,
+ userOrderId = GlobalData.currentOrder?.userOrderId,
+ isPhoto = true)
+ var list = uiState.value.damagePhoto
+ if (list.isNullOrEmpty()) {
+ list = arrayListOf(eleCarDamagePhotoBean)
+ } else {
+ list.plus(eleCarDamagePhotoBean)
+ }
+ updateState(uiState.value.copy(damagePhoto = list))
+ CommonMethod.uploadImage(file = File(localPath),
+ success = {
+ RoomHelper.db?.eleCarDamagePhotoDao()?.insert(eleCarDamagePhotoBean.copy(uploadStatus = 1, serverPath = it))
+ updateState(uiState.value.copy(damagePhoto = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId ?: 0)))
+ }, failed = {
+ RoomHelper.db?.eleCarDamagePhotoDao()?.insert(eleCarDamagePhotoBean.copy(uploadStatus = 3))
+ updateState(uiState.value.copy(damagePhoto = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId ?: 0)))
+ LogUtil.print("addDamagePhoto failed", it ?: "")
+ })
+ }
+
+ private fun changeDamageState(hasDamage: Boolean?) {
+ uiState.value.eleWorkOrderBean?.copy(hasBad = hasDamage)?.let {
+ updateState(uiState.value.copy(eleWorkOrderBean = it))
+ RoomHelper.db?.eleWorkOrderDao()?.update(it)
+ }
+ }
+
+ private fun init() {
+ val eleWorkOrderDao = RoomHelper.db?.eleWorkOrderDao()
+ var eleWorkOrderBean = eleWorkOrderDao?.getEleWorkOrder(GlobalData.currentOrder?.taskId
+ ?: 0)
+ val photoList = RoomHelper.db?.eleCarDamagePhotoDao()?.getEleCarDamagePhotos(GlobalData.currentOrder?.taskId
+ ?: 0)
+
+ if (eleWorkOrderBean == null) {
+ try {
+ var carVin = GlobalData.currentOrder?.carVin ?: ""
+ if (carVin.length > 6) {
+ carVin = carVin.substring(carVin.length - 6)
+ }
+ eleWorkOrderBean = EleWorkOrderBean(
+ orderId = GlobalData.currentOrder?.taskId ?: 0,
+ userOrderId = GlobalData.currentOrder?.userOrderId,
+ carNO = GlobalData.currentOrder?.carNo,
+ date = TimeUtils.date2String(Date(), "yyyy-MM-dd"),
+ carVin = carVin,
+ orderType = GlobalData.currentOrder?.serviceTypeName)
+ eleWorkOrderDao?.insertEleWorkOrder(eleWorkOrderBean)
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean, damagePhoto = photoList, orderInfo = getCurrentOrder()))
+ LogUtil.print("EleSignCheckVm init 电子表单创建成功", "eleWorkOrderBean==$eleWorkOrderBean")
+ } catch (e: Exception) {
+ LogUtil.print("EleSignCheckVm init 电子表单创建异常", e)
+ }
+ } else {
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean, damagePhoto = photoList, orderInfo = getCurrentOrder()))
+ LogUtil.print("电子表单更新车辆损伤状况", "eleWorkOrderBean==$eleWorkOrderBean")
+ }
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class ChangeDamageState(val hasDamage: Boolean) : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data class AddDamagePhoto(val path: String) : Action()
+ data class ReUploadPhoto(val eleCarDamagePhotoBean: EleCarDamagePhotoBean) : Action()
+ data object Upload : Action()
+ data object UploadOffline : Action()
+ }
+
+ data class UiState(
+ val orderInfo: OrderInfo? = null,
+ val eleWorkOrderBean: EleWorkOrderBean? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val showCallPhoneDialog: Boolean? = null,
+ val showOfflineDialog: Boolean? = null,
+ val damagePhoto: List? = null,
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignScreen.kt b/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignScreen.kt
new file mode 100644
index 0000000..1116342
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignScreen.kt
@@ -0,0 +1,288 @@
+package com.za.ui.servicing.ele_sign
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.blankj.utilcode.util.TimeUtils
+import com.za.base.theme.black65
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.common.GlobalData
+import com.za.common.util.ServicingSpeechManager
+import com.za.ext.finish
+import com.za.ext.goStatusPage
+import com.za.servicing.R
+import com.za.ui.servicing.view.InServicingHeadView
+import com.za.ui.view.SignatureView
+import kotlin.math.ceil
+
+@Composable
+fun EleSignScreen(vm : EleSignVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(EleSignVm.Action.Init)
+ ServicingSpeechManager.playOrderCustomSign(context)
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(EleSignVm.Action.Upload)
+ })
+ }
+
+ if (uiState.value.goNextPage != null) {
+ uiState.value.orderInfo?.goStatusPage(context)
+ context.finish()
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "离线上传",
+ title = "上传失败,是否进行离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(EleSignVm.Action.UploadOffline)
+ })
+ }
+
+ Scaffold(topBar = {
+ InServicingHeadView(title = "车况检查表",
+ orderInfo = uiState.value.orderInfo,
+ onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "签名完成,开始作业") {
+ vm.dispatch(EleSignVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxWidth()
+ .padding(it)
+ .padding(horizontal = 10.dp)) {
+ item {
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Bottom,
+ horizontalArrangement = Arrangement.SpaceBetween) {
+ AsyncImage(model = R.mipmap.ic_company_brand,
+ contentDescription = "",
+ modifier = Modifier.size(86.dp, 25.dp),
+ contentScale = ContentScale.FillWidth)
+ Spacer(modifier = Modifier.weight(1f))
+ Text(text = "日期:${
+ TimeUtils.millis2String(System.currentTimeMillis(), "yyyy-MM-dd")
+ }", color = Color.Black, fontWeight = FontWeight.Normal, fontSize = 11.sp)
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+ }
+
+ item {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .border(0.5.dp, color = Color(0xFF989898))
+ .padding(10.dp)) {
+ Text(text = "一、车辆信息",
+ color = Color.Black,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp)
+
+ Spacer(modifier = Modifier.height(5.dp))
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+
+ Text(text = "车牌号:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = GlobalData.currentOrder?.carNo ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = "车架号后6位:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = uiState.value.eleWorkOrderBean?.carVin ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+
+ Text(text = "救援类型:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = uiState.value.eleWorkOrderBean?.orderType ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+
+ Text(text = "车辆损伤情况:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "有损伤".takeIf { uiState.value.eleWorkOrderBean?.hasBad == true }
+ ?: "无损伤",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ }
+
+ if (! uiState.value.damagePhoto.isNullOrEmpty()) {
+ val height = uiState.value.damagePhoto?.size?.div(3.0) ?: 1
+ LazyVerticalGrid(modifier = Modifier
+ .fillMaxWidth()
+ .height(80.times(ceil(height.toDouble())).dp),
+ columns = GridCells.Fixed(3)) {
+ items(items = uiState.value.damagePhoto !!) { item ->
+ Box(modifier = Modifier
+ .size(110.dp, 76.dp)
+ .padding(5.dp),
+ contentAlignment = Alignment.BottomCenter) {
+ AsyncImage(model = item.serverPath.takeIf { item.path.isNullOrBlank() }
+ ?: item.path,
+ contentDescription = "",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillWidth)
+ }
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+
+ item {
+
+ }
+
+ item {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .border(0.5.dp, color = Color(0xFF989898))
+ .padding(10.dp)) {
+ Row {
+ Text(text = "二、服务须知",
+ color = Color.Black,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp)
+ Text(text = "(请顾客认证阅读,并在理解且无异议后签字确认)",
+ color = Color.Red,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 10.sp)
+ }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(250.dp)
+ .verticalScroll(rememberScrollState())) {
+ Text(text = uiState.value.eleWorkOrderBean?.serviceContent.takeIf { ! uiState.value.eleWorkOrderBean?.serviceContent.isNullOrBlank() }
+ ?: context.getString(R.string.service_content),
+ fontSize = 11.sp,
+ color = Color(0xE6252525))
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AsyncImage(model = R.drawable.sv_star,
+ contentDescription = "",
+ modifier = Modifier.size(15.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "客户对上述内容知晓并接受,签字准许实施救援服务。",
+ color = Color.Red,
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp)
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(text = "困境救援案件,为了完成救援而导致的不可避免的车辆损伤,客户自行承担此类风险和损失。",
+ color = Color.Black,
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp)
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "客户签名:",
+ fontSize = 10.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+ Text(text = "(正楷)",
+ fontSize = 8.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Red)
+ Spacer(modifier = Modifier.width(5.dp))
+ SignatureView(serverPath = uiState.value.eleWorkOrderBean?.serverCustomSignPath
+ ?: uiState.value.eleWorkOrderBean?.localCustomSignPath, success = {
+ if (it.isNullOrBlank()) {
+ return@SignatureView
+ }
+ vm.dispatch(EleSignVm.Action.UploadSignature(it))
+ })
+ }
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignVm.kt b/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignVm.kt
new file mode 100644
index 0000000..3f70930
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/ele_sign/EleSignVm.kt
@@ -0,0 +1,185 @@
+package com.za.ui.servicing.ele_sign
+
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.ele.EleCarDamagePhotoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.SaveEleOrderRequest
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.room.RoomHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.io.File
+
+class EleSignVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.Upload -> upload()
+ is Action.UploadSignature -> uploadSignature(action.path)
+ is Action.UploadOffline -> uploadOffline()
+ }
+ }
+
+
+ private fun uploadOffline() {
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(getCurrentOrder()?.taskId
+ ?: 0)
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+
+ if (uiState.value.eleWorkOrderBean?.serverCustomSignPath.isNullOrBlank()) {
+ val signOfflineUpdateTaskBean = OfflineUpdateTaskBean(
+ imageLocalPath = uiState.value.eleWorkOrderBean?.localCustomSignPath,
+ taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "电子工单-客户签名照片",
+ offlineType = 5
+ )
+ insertOfflineTask(signOfflineUpdateTaskBean)
+ }
+
+
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ customerSignPath = uiState.value.eleWorkOrderBean?.serverCustomSignPath,
+ offlineTitle = "电子工单-车况检查表",
+ offlineType = 3,
+ eleState = 2,
+ userOrderId = getCurrentOrder()?.userOrderId
+ )
+ insertOfflineTask(offlineUpdateTaskBean)
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(orderWorkStatus = 2))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }
+
+ private fun uploadSignature(path: String) {
+ LoadingManager.showLoading()
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(localCustomSignPath = path)
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean))
+ LogUtil.print("uploadSignature success", eleWorkOrderBean.toJson() ?: "")
+ }
+ CommonMethod.uploadImage(File(path),
+ success = {
+ LoadingManager.hideLoading()
+ if (eleWorkOrderBean != null) {
+ val temp = eleWorkOrderBean.copy(serverCustomSignPath = it)
+ RoomHelper.db?.eleWorkOrderDao()?.update(temp)
+ updateState(uiState.value.copy(eleWorkOrderBean = temp))
+ LogUtil.print("uploadSignature success", temp.toJson() ?: "")
+ }
+ },
+ failed = {
+ LoadingManager.hideLoading()
+ LogUtil.print("uploadSignature", "failed==$it")
+ })
+ }
+
+ private fun upload() {
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(getCurrentOrder()?.taskId
+ ?: 0)
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+
+ if (uiState.value.eleWorkOrderBean?.localCustomSignPath.isNullOrBlank()) {
+ ToastUtils.showLong("签名照不能为空!!")
+ return
+ }
+
+ if (getCurrentOrderOfflineTask() != null) {
+ uploadOffline()
+ return
+ }
+
+ if (!uiState.value.eleWorkOrderBean?.localCustomSignPath.isNullOrBlank() && uiState.value.eleWorkOrderBean?.serverCustomSignPath.isNullOrBlank()) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ return
+ }
+
+ val saveEleOrderRequest = SaveEleOrderRequest(
+ state = 2,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ taskOrderId = getCurrentOrder()?.taskId,
+ customerSignPath = uiState.value.eleWorkOrderBean?.serverCustomSignPath
+ )
+
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().saveElectronOrder(saveEleOrderRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(orderWorkStatus = 2))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("eleSign upload failed", msg ?: "")
+ }
+ })
+ }
+
+ private fun init() {
+ LoadingManager.showLoading()
+ CommonMethod.queryElectronOrder(getCurrentOrder(),
+ success = {
+ LoadingManager.hideLoading()
+ val photoList = RoomHelper.db?.eleCarDamagePhotoDao()?.getEleCarDamagePhotos(getCurrentOrder()?.taskId
+ ?: 0)
+ updateState(uiState.value.copy(eleWorkOrderBean = it, damagePhoto = photoList, orderInfo = getCurrentOrder()))
+ LogUtil.print("电子表单更新车辆损伤照片", "eleWorkOrderBean==${photoList.toJson()}")
+ },
+ failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort("数据加载异常,请返回重试!")
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object Upload : Action()
+ data object UploadOffline : Action()
+ data class UploadSignature(val path: String) : Action()
+ }
+
+ data class UiState(
+ val orderInfo: OrderInfo? = null,
+ val eleWorkOrderBean: EleWorkOrderBean? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val showCallPhoneDialog: Boolean? = null,
+ val showOfflineDialog: Boolean? = null,
+ val damagePhoto: List? = null,
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteActivity.kt b/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteActivity.kt
new file mode 100644
index 0000000..c045fb2
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteActivity.kt
@@ -0,0 +1,388 @@
+package com.za.ui.servicing.go_accident
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.amap.api.location.AMapLocationClient
+import com.amap.api.maps.CameraUpdateFactory
+import com.amap.api.maps.MapView
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.LatLngBounds
+import com.amap.api.maps.model.MarkerOptions
+import com.amap.api.maps.model.PolylineOptions
+import com.za.base.BaseActivity
+import com.za.base.theme.headBgColor
+import com.za.base.view.CommonDialog
+import com.za.common.GlobalData
+import com.za.common.util.ImageUtil
+import com.za.ext.copy
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.servicing.R
+import com.za.ui.servicing.view.InServicingHeadView
+
+class GoAccidentSiteActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ GoAccidentSiteScreen()
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GoAccidentSiteScreen(vm : GoAccidentSiteVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val mapView = remember { MapView(context) }
+
+ val bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
+ val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
+
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> {
+ mapView.onCreate(Bundle())
+ vm.dispatch(GoAccidentSiteVm.Action.Init)
+ }
+
+ Lifecycle.Event.ON_RESUME -> mapView.onResume()
+ Lifecycle.Event.ON_PAUSE -> mapView.onPause()
+ Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
+ else -> {}
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(GoAccidentSiteVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(GoAccidentSiteVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(GoAccidentSiteVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = false)))
+ vm.dispatch(GoAccidentSiteVm.Action.UpdateTask)
+ })
+ }
+
+ BottomSheetScaffold(scaffoldState = scaffoldState,
+ topBar = {
+ InServicingHeadView(title = "救援车发车,正在赶往现场",
+ onBack = { context.finish() },
+ orderInfo = uiState.value.orderInfo)
+ },
+ sheetContent = {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .verticalScroll(rememberScrollState())) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier
+ .width(32.dp)
+ .height(4.dp)
+ .background(color = Color(0xFFE0E0E0), shape = RoundedCornerShape(2.dp)))
+ }
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = if (! uiState.value.estimatedArrivalTime.isNullOrBlank()) "预计到达: ${uiState.value.estimatedArrivalTime}"
+ else "计算中...", color = Color(0xFF666666), fontSize = 14.sp)
+
+ Text(text = if (uiState.value.remainingDistance > 0) "距离救援地: %.1fkm".format(
+ uiState.value.remainingDistance / 1000f)
+ else "计算中...", color = Color(0xFFFF4D4F), fontSize = 14.sp)
+ }
+
+ HorizontalDivider(modifier = Modifier
+ .fillMaxWidth()
+ .alpha(0.1f))
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)) {
+ Row(modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = uiState.value.orderInfo?.serviceTypeName ?: "",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier
+ .background(Color(0xFF9BA1B2), RoundedCornerShape(4.dp))
+ .padding(horizontal = 6.dp, vertical = 2.dp)) {
+ Text(text = "月结".takeIf { uiState.value.orderInfo?.settleType == 1 }
+ ?: "现金", color = Color.White, fontSize = 12.sp)
+ }
+
+ Text(text = uiState.value.orderInfo?.addressProperty ?: "",
+ color = Color(0xFFFD8205),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ uiState.value.orderInfo?.taskCode?.copy(context)
+ }) {
+ Text(text = "单号", color = Color(0xFF999999), fontSize = 13.sp)
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.orderInfo?.taskCode ?: "",
+ color = Color(0xFF666666),
+ fontSize = 14.sp)
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ AsyncImage(model = R.drawable.sv_copy,
+ contentDescription = "copy",
+ modifier = Modifier.size(16.dp))
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(verticalAlignment = Alignment.Top,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 && order.lng != null && order.lng != 0.0) {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.lat !!, order.lng !!),
+ 16f))
+ }
+ }
+ }) {
+ AsyncImage(model = R.drawable.sv_rescuing,
+ contentDescription = "rescue",
+ modifier = Modifier.size(16.dp))
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.orderInfo?.address ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.weight(1f))
+ }
+
+ if (! uiState.value.orderInfo?.distAddress.isNullOrBlank()) {
+ Row(verticalAlignment = Alignment.Top,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ uiState.value.orderInfo?.let { order ->
+ if (order.distLat != null && order.distLat != 0.0 && order.distLng != null && order.distLng != 0.0) {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.distLat !!, order.distLng !!),
+ 16f))
+ }
+ }
+ }) {
+ AsyncImage(model = R.drawable.sv_dist,
+ contentDescription = "destination",
+ modifier = Modifier.size(16.dp))
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(text = uiState.value.orderInfo?.distAddress ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+
+ Button(onClick = {
+ vm.dispatch(GoAccidentSiteVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = true)))
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .padding(horizontal = 16.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = headBgColor),
+ shape = RoundedCornerShape(8.dp)) {
+ Text(text = "到达事发地",
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ },
+ sheetPeekHeight = 180.dp,
+ sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
+ sheetContainerColor = Color.White,
+ sheetDragHandle = null,
+ sheetSwipeEnabled = true) { paddingValues ->
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)) {
+ AndroidView(modifier = Modifier.fillMaxSize(), factory = {
+ AMapLocationClient.updatePrivacyShow(context, true, true)
+ AMapLocationClient.updatePrivacyAgree(context, true)
+ mapView.apply {
+ map.apply {
+ isTrafficEnabled = false
+ isMyLocationEnabled = false
+ uiSettings.isMyLocationButtonEnabled = false
+ uiSettings.setLogoBottomMargin(- 100)
+ uiSettings.isZoomControlsEnabled = false
+
+ setOnMarkerClickListener { marker ->
+ marker.showInfoWindow()
+ Handler(Looper.getMainLooper()).postDelayed({
+ marker.hideInfoWindow()
+ }, 800)
+ true
+ }
+ }
+ }
+ mapView
+ }, update = {
+ mapView.map.clear()
+
+ // 绘制路线
+ uiState.value.routePoints?.let { points ->
+ mapView.map.addPolyline(PolylineOptions().addAll(points).width(15f)
+ .setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.icon_road_green_arrow))
+ .zIndex(1f))
+ }
+
+ // 添加当前位置标记
+ if (GlobalData.currentLocation != null) {
+ mapView.map.addMarker(MarkerOptions().position(LatLng(GlobalData.currentLocation?.latitude !!,
+ GlobalData.currentLocation?.longitude !!)).title("当前位置")
+ .icon(ImageUtil.vectorToBitmap(context, R.drawable.ic_current_location))
+ .anchor(0.5f, 0.5f).visible(true))
+ }
+
+ // 添加救援地标记
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 && order.lng != null && order.lng != 0.0) {
+ mapView.map.addMarker(MarkerOptions().position(LatLng(order.lat !!,
+ order.lng !!)).title("救援地点")
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_rescuing_map))
+ .anchor(0.5f, 0.5f))
+ }
+
+ // 添加目的地标记
+ if (order.distLat != null && order.distLat != 0.0 && order.distLng != null && order.distLng != 0.0) {
+ mapView.map.addMarker(MarkerOptions().position(LatLng(order.distLat !!,
+ order.distLng !!)).title("目的地")
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_dist_map))
+ .anchor(0.5f, 0.5f))
+ }
+ }
+
+ // 调整地图显示范围
+ val bounds = LatLngBounds.Builder().apply {
+ GlobalData.currentLocation?.let {
+ include(LatLng(it.latitude, it.longitude))
+ }
+
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 && order.lng != null && order.lng != 0.0) {
+ include(LatLng(order.lat !!, order.lng !!))
+ }
+
+ if (order.distLat != null && order.distLat != 0.0 && order.distLng != null && order.distLng != 0.0) {
+ include(LatLng(order.distLat !!, order.distLng !!))
+ }
+ }
+ }.build()
+
+ try {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))
+ } catch (e : Exception) {
+ GlobalData.currentLocation?.let {
+ mapView.map.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(it.latitude,
+ it.longitude), 15f))
+ }
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteVm.kt b/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteVm.kt
new file mode 100644
index 0000000..8666194
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/go_accident/GoAccidentSiteVm.kt
@@ -0,0 +1,270 @@
+package com.za.ui.servicing.go_accident
+
+import androidx.lifecycle.viewModelScope
+import com.amap.api.maps.AMapUtils
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.MarkerOptions
+import com.amap.api.services.core.LatLonPoint
+import com.amap.api.services.route.BusRouteResult
+import com.amap.api.services.route.DriveRouteResult
+import com.amap.api.services.route.RideRouteResult
+import com.amap.api.services.route.RouteSearch
+import com.amap.api.services.route.WalkRouteResult
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getNextStatus
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.service.location.ZdLocationManager
+import com.za.servicing.R
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+class GoAccidentSiteVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdateOffline -> updateOfflineTask()
+ is Action.StartTimer -> startTimer()
+ }
+ }
+
+ private fun updateOfflineTask(taskRequest : UpdateTaskRequest? = null) {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val temp = taskRequest ?: UpdateTaskRequest(type = "VERIFY",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = GlobalData.currentOrder?.taskState,
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude)
+
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(type = temp.type,
+ taskId = temp.taskId,
+ userId = temp.userId,
+ vehicleId = temp.vehicleId,
+ currentState = temp.currentState,
+ offlineMode = 1,
+ operateTime = temp.operateTime,
+ updateTaskLat = temp.lat,
+ updateTaskLng = temp.lng,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ offlineTitle = "救援车发车,正在赶往现场",
+ offlineType = 1)
+ insertOfflineTask(offlineUpdateTaskBean)
+
+ updateOrder(getCurrentOrder()?.copy(taskState = getCurrentOrder()?.getNextStatus()))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(nextState = getCurrentOrder()?.taskState),
+ orderInfo = getCurrentOrder()))
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun updateTask() {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(type = "VERIFY",
+ taskId = getCurrentOrder()?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = getCurrentOrder()?.taskState,
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude)
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun doUploadTask(request : UpdateTaskRequest) {
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateOfflineTask(taskRequest = request)
+ return
+ }
+
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = { it ->
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ updateState(uiState = uiState.value.copy(orderInfo = getCurrentOrder()))
+ buildMarkers(getCurrentOrder())
+ searchDrivingRoute(getCurrentOrder())
+// dispatch(Action.StartTimer)
+ }
+
+ private fun buildMarkers(orderInfo : OrderInfo?) {
+ val markers = arrayListOf()
+ if (orderInfo?.lat != null && orderInfo.lat != 0.0 && orderInfo.lng != null && orderInfo.lng != 0.0) { // 获取矢量图资源文件
+ val startMarkers =
+ MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_rescuing_map))
+ .position(LatLng(orderInfo.lat !!, orderInfo.lng !!)).infoWindowEnable(false)
+ .visible(true)
+ markers.add(startMarkers)
+ }
+
+ if (orderInfo?.distLat != null && orderInfo.distLat != 0.0 && orderInfo.distLng != null && orderInfo.distLng != 0.0) {
+ val startMarkers =
+ MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_dist_map))
+ .position(LatLng(orderInfo.distLat !!, orderInfo.distLng !!)).visible(true)
+ markers.add(startMarkers)
+ }
+ updateState(uiState.value.copy(markers = markers))
+ }
+
+ private fun searchDrivingRoute(orderInfo : OrderInfo?) { // 如果没有当前位置,则不进行路径规划
+ if (GlobalData.currentLocation == null) return
+
+ val currentPoint = LatLonPoint(GlobalData.currentLocation?.latitude ?: 0.0,
+ GlobalData.currentLocation?.longitude ?: 0.0)
+
+ // 获取救援地点坐标
+ val rescuePoint =
+ if (orderInfo?.lat != null && orderInfo.lat != 0.0 && orderInfo.lng != null && orderInfo.lng != 0.0) {
+ LatLonPoint(orderInfo.lat !!, orderInfo.lng !!)
+ } else null
+
+ // 如果没有救援地点,则不进行规划
+ if (rescuePoint == null) return
+
+ // 使用 Application Context 创建路由搜索对象
+ val routeSearch = RouteSearch(GlobalData.application)
+ routeSearch.setRouteSearchListener(object : RouteSearch.OnRouteSearchListener {
+ override fun onDriveRouteSearched(result : DriveRouteResult?, errorCode : Int) {
+ if (errorCode == 1000 && result != null && result.paths.isNotEmpty()) {
+ val path = result.paths[0]
+ val points = path.steps.flatMap { step ->
+ step.polyline.map { LatLng(it.latitude, it.longitude) }
+ }
+ val durationInSeconds = path.duration
+ val arrivalTime = SimpleDateFormat("HH:mm:ss",
+ Locale.getDefault()).format(System.currentTimeMillis() + durationInSeconds * 1000)
+
+
+ LogUtil.print("arrivalTime", "arrivalTime: arrivalTime=$arrivalTime")
+ LogUtil.print("distance", "distance: distance=${path.distance.div(1000)}km")
+ updateState(uiState.value.copy(routePoints = points, estimatedArrivalTime = arrivalTime, remainingDistance = path.distance))
+ } else {
+ LogUtil.print("searchDrivingRoute", "路径规划失败: errorCode=$errorCode")
+ }
+ }
+
+ override fun onBusRouteSearched(p0 : BusRouteResult?, p1 : Int) {}
+ override fun onWalkRouteSearched(p0 : WalkRouteResult?, p1 : Int) {}
+ override fun onRideRouteSearched(p0 : RideRouteResult?, p1 : Int) {}
+ })
+
+ // 规划当前位置到救援地的路线
+ val query = RouteSearch.FromAndTo(currentPoint, rescuePoint)
+ val driveQuery =
+ RouteSearch.DriveRouteQuery(query, RouteSearch.DrivingDefault, null, null, "")
+ routeSearch.calculateDriveRouteAsyn(driveQuery)
+ }
+
+ private fun calculateRemainingDistance() : Pair {
+ val currentLocation = GlobalData.currentLocation ?: return Pair(0f, "")
+ val orderInfo = _uiState.value.orderInfo ?: return Pair(0f, "")
+
+ // 只计算到救援地点的距离
+ val distance = if (orderInfo.lat != null && orderInfo.lng != null) {
+ AMapUtils.calculateLineDistance(LatLng(currentLocation.latitude,
+ currentLocation.longitude), LatLng(orderInfo.lat !!, orderInfo.lng !!))
+ } else 0f
+
+ if (distance <= 0f) {
+ return Pair(0f, "")
+ }
+
+ // 计算预计到达时间(假设平均速度40km/h)
+ val timeInSeconds = ((distance / 1000.0 * 60.0 / 40.0) * 60).toInt()
+ val calendar = Calendar.getInstance()
+ calendar.add(Calendar.SECOND, timeInSeconds)
+ val arrivalTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(calendar.time)
+
+ return Pair(distance, arrivalTime)
+ }
+
+ private var timerJob : Job? = null
+
+ private fun startTimer() {
+ timerJob?.cancel()
+ timerJob = viewModelScope.launch {
+ while (isActive) {
+ val (distance, arrivalTime) = calculateRemainingDistance()
+ _uiState.update {
+ it.copy(remainingDistance = distance, estimatedArrivalTime = arrivalTime)
+ }
+ delay(1000)
+ }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ timerJob?.cancel()
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ data object UpdateOffline : Action()
+ data object StartTimer : Action()
+ }
+
+ data class UiState(val orderInfo : OrderInfo? = null,
+ val showCallPhoneDialog : Boolean? = false,
+ val markers : ArrayList? = null,
+ val goNextPage : UpdateTaskBean? = null,
+ val isGoNextPageDialog : Boolean? = null,
+ val showOfflineDialog : Boolean? = null,
+ val routePoints : List? = null,
+ val remainingDistance : Float = 0f,
+ val estimatedArrivalTime : String? = null)
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationActivity.kt b/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationActivity.kt
new file mode 100644
index 0000000..563b088
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationActivity.kt
@@ -0,0 +1,559 @@
+package com.za.ui.servicing.go_to_destination
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.amap.api.location.AMapLocationClient
+import com.amap.api.maps.CameraUpdateFactory
+import com.amap.api.maps.MapView
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.LatLngBounds
+import com.amap.api.maps.model.MarkerOptions
+import com.amap.api.maps.model.PolylineOptions
+import com.za.base.BaseActivity
+import com.za.base.theme.headBgColor
+import com.za.base.view.CommonDialog
+import com.za.common.GlobalData
+import com.za.common.util.ImageUtil
+import com.za.ext.copy
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.servicing.R
+import com.za.ui.servicing.view.InServicingHeadView
+
+class GoToDestinationActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ GoToDestinationScreen()
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GoToDestinationScreen(vm: GoToDestinationVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val mapView = remember { MapView(context) }
+
+ // 添加协程作用域
+ val scope = rememberCoroutineScope()
+
+ // 修改 BottomSheet 状态
+ val bottomSheetState = rememberStandardBottomSheetState(
+ initialValue = SheetValue.PartiallyExpanded,
+ confirmValueChange = { true }
+ )
+ val scaffoldState = rememberBottomSheetScaffoldState(
+ bottomSheetState = bottomSheetState
+ )
+
+ // 优化状态管理
+ val isExpanded by remember {
+ derivedStateOf { bottomSheetState.currentValue == SheetValue.Expanded }
+ }
+
+ // 记忆化常用值,减少重组
+ val orderInfo = remember(uiState.value.orderInfo) { uiState.value.orderInfo }
+ val estimatedTime = remember(uiState.value.estimatedArrivalTime) { uiState.value.estimatedArrivalTime }
+ val remainingDistance = remember(uiState.value.remainingDistance) { uiState.value.remainingDistance }
+
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> {
+ mapView.onCreate(Bundle())
+ vm.dispatch(GoToDestinationVm.Action.Init)
+ }
+
+ Lifecycle.Event.ON_RESUME -> mapView.onResume()
+ Lifecycle.Event.ON_PAUSE -> mapView.onPause()
+ Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
+ else -> {}
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ // 对话框处理
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = { vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false))) },
+ confirm = {
+ vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(GoToDestinationVm.Action.UpdateTask)
+ }
+ )
+ }
+
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "离线上传",
+ title = "任务提交失败,是否离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = { vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false))) },
+ confirm = {
+ vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(GoToDestinationVm.Action.UploadOffline)
+ }
+ )
+ }
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ topBar = {
+ InServicingHeadView(
+ title = "作业完成,正在拖往目的地",
+ onBack = { context.finish() },
+ orderInfo = orderInfo
+ )
+ },
+ sheetContent = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .verticalScroll(rememberScrollState())
+ ) {
+ // 滑动指示器
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .width(32.dp)
+ .height(4.dp)
+ .background(
+ color = Color(0xFFE0E0E0),
+ shape = RoundedCornerShape(2.dp)
+ )
+ )
+ }
+
+ // 距离和时间信息
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = if (estimatedTime.isNotEmpty())
+ "预计到达: $estimatedTime"
+ else
+ "计算中...",
+ color = Color(0xFF666666),
+ fontSize = 14.sp
+ )
+
+ Text(
+ text = if (remainingDistance > 0)
+ "目的地距离: %.1fkm".format(remainingDistance / 1000f)
+ else
+ "计算中...",
+ color = Color(0xFFFF4D4F),
+ fontSize = 14.sp
+ )
+ }
+
+ // 使用 HorizontalDivider 替代 Box
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .alpha(0.1f)
+ )
+
+ // 订单信息
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = uiState.value.orderInfo?.serviceTypeName ?: "",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .background(Color(0xFF9BA1B2), RoundedCornerShape(4.dp))
+ .padding(horizontal = 6.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "月结".takeIf { uiState.value.orderInfo?.settleType == 1 }
+ ?: "现金",
+ color = Color.White,
+ fontSize = 12.sp
+ )
+ }
+
+ Text(
+ text = uiState.value.orderInfo?.addressProperty ?: "",
+ color = Color(0xFFFD8205),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // 订单号
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable { uiState.value.orderInfo?.taskCode?.copy(context) }
+ ) {
+ Text(
+ text = "单号",
+ color = Color(0xFF999999),
+ fontSize = 13.sp
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = uiState.value.orderInfo?.taskCode ?: "",
+ color = Color(0xFF666666),
+ fontSize = 14.sp
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ AsyncImage(
+ model = R.drawable.sv_copy,
+ contentDescription = "copy",
+ modifier = Modifier.size(16.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // 地址信息
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // 救援地
+ Row(
+ verticalAlignment = Alignment.Top,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 &&
+ order.lng != null && order.lng != 0.0
+ ) {
+ mapView.map.animateCamera(
+ CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.lat!!, order.lng!!),
+ 16f
+ )
+ )
+ }
+ }
+ }
+ ) {
+ AsyncImage(
+ model = R.drawable.sv_rescuing,
+ contentDescription = "rescue",
+ modifier = Modifier.size(16.dp)
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = uiState.value.orderInfo?.address ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ // 目的地
+ if (!uiState.value.orderInfo?.distAddress.isNullOrBlank()) {
+ Row(
+ verticalAlignment = Alignment.Top,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ uiState.value.orderInfo?.let { order ->
+ if (order.distLat != null && order.distLat != 0.0 &&
+ order.distLng != null && order.distLng != 0.0
+ ) {
+ mapView.map.animateCamera(
+ CameraUpdateFactory.newLatLngZoom(
+ LatLng(order.distLat!!, order.distLng!!),
+ 16f
+ )
+ )
+ }
+ }
+ }
+ ) {
+ AsyncImage(
+ model = R.drawable.sv_dist,
+ contentDescription = "destination",
+ modifier = Modifier.size(16.dp)
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = uiState.value.orderInfo?.distAddress ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+
+ // 到达按钮 - 移除外层 Column,简化布局
+ Button(
+ onClick = {
+ vm.dispatch(GoToDestinationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .padding(horizontal = 16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = headBgColor
+ ),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Text(
+ text = "到达目的地",
+ color = Color.White,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ },
+ sheetPeekHeight = 180.dp,
+ sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
+ sheetContainerColor = Color.White,
+ sheetDragHandle = null,
+ sheetSwipeEnabled = true
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ AndroidView(
+ modifier = Modifier
+ .fillMaxSize(),
+ factory = {
+ AMapLocationClient.updatePrivacyShow(context, true, true)
+ AMapLocationClient.updatePrivacyAgree(context, true)
+ mapView.apply {
+ map.apply {
+ isTrafficEnabled = false
+ isMyLocationEnabled = false
+ uiSettings.isMyLocationButtonEnabled = false
+ uiSettings.setLogoBottomMargin(-100)
+ uiSettings.isZoomControlsEnabled = false
+
+ // 添加标记点点击事件
+ setOnMarkerClickListener { marker ->
+ marker.showInfoWindow()
+ Handler(Looper.getMainLooper()).postDelayed({
+ marker.hideInfoWindow()
+ }, 800)
+ true
+ }
+ }
+ }
+ },
+ update = {
+ // 清除旧标记和路线
+ mapView.map.clear()
+
+ // 先绘制路线
+ uiState.value.routePoints?.let { points ->
+ mapView.map.addPolyline(
+ PolylineOptions()
+ .addAll(points)
+ .width(15f)
+ .setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.icon_road_green_arrow))
+ .zIndex(1f)
+ )
+ }
+
+ // 再添加标记点,确保标记点在路线上层
+ // 添加当前位置标记
+ if (GlobalData.currentLocation != null) {
+ mapView.map.addMarker(
+ MarkerOptions()
+ .position(LatLng(
+ GlobalData.currentLocation?.latitude!!,
+ GlobalData.currentLocation?.longitude!!
+ ))
+ .title("当前位置")
+ .icon(ImageUtil.vectorToBitmap(context,R.drawable.ic_current_location))
+ .anchor(0.5f, 0.5f)
+ .visible(true)
+ )
+ }
+
+ // 添加救援地标记
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 &&
+ order.lng != null && order.lng != 0.0
+ ) {
+ mapView.map.addMarker(
+ MarkerOptions()
+ .position(LatLng(order.lat!!, order.lng!!))
+ .title("救援地点")
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_rescuing_map))
+ .anchor(0.5f, 0.5f)
+ )
+ }
+
+ // 添加目的地标记
+ if (order.distLat != null && order.distLat != 0.0 &&
+ order.distLng != null && order.distLng != 0.0
+ ) {
+ mapView.map.addMarker(
+ MarkerOptions()
+ .position(LatLng(order.distLat!!, order.distLng!!))
+ .title("目的地")
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_dist_map))
+ .anchor(0.5f, 0.5f)
+ )
+ }
+ }
+
+ // 调整地图显示范围
+ val bounds = LatLngBounds.Builder().apply {
+ // 添加当前位置
+ GlobalData.currentLocation?.let {
+ include(LatLng(it.latitude, it.longitude))
+ }
+
+ // 添加救援地点和目的地
+ uiState.value.orderInfo?.let { order ->
+ if (order.lat != null && order.lat != 0.0 &&
+ order.lng != null && order.lng != 0.0
+ ) {
+ include(LatLng(order.lat!!, order.lng!!))
+ }
+
+ if (order.distLat != null && order.distLat != 0.0 &&
+ order.distLng != null && order.distLng != 0.0
+ ) {
+ include(LatLng(order.distLat!!, order.distLng!!))
+ }
+ }
+ }.build()
+
+ try {
+ mapView.map.animateCamera(
+ CameraUpdateFactory.newLatLngBounds(bounds, 100)
+ )
+ } catch (e: Exception) {
+ // 如果计算边界失败,则使用默认缩放级别
+ GlobalData.currentLocation?.let {
+ mapView.map.animateCamera(
+ CameraUpdateFactory.newLatLngZoom(
+ LatLng(it.latitude, it.longitude),
+ 15f
+ )
+ )
+ }
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationVm.kt b/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationVm.kt
new file mode 100644
index 0000000..e1cfccd
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/go_to_destination/GoToDestinationVm.kt
@@ -0,0 +1,285 @@
+package com.za.ui.servicing.go_to_destination
+
+import com.amap.api.maps.model.BitmapDescriptorFactory
+import com.amap.api.maps.model.LatLng
+import com.amap.api.maps.model.MarkerOptions
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getNextStatus
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.service.location.ZdLocationManager
+import com.za.servicing.R
+import kotlinx.coroutines.flow.MutableStateFlow
+import androidx.lifecycle.viewModelScope
+import com.amap.api.maps.AMapUtils
+import com.amap.api.services.core.LatLonPoint
+import com.amap.api.services.route.BusRouteResult
+import com.amap.api.services.route.DriveRouteResult
+import com.amap.api.services.route.RideRouteResult
+import com.amap.api.services.route.RouteSearch
+import com.amap.api.services.route.WalkRouteResult
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+class GoToDestinationVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UploadOffline -> updateOfflineTask()
+ is Action.StartTimer -> startTimer()
+ }
+ }
+
+ private fun updateOfflineTask(taskRequest: UpdateTaskRequest? = null) {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val temp = taskRequest ?: UpdateTaskRequest(
+ type = "ARRIVE_DEST",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ flowType = GlobalData.currentOrder?.flowType,
+ currentState = GlobalData.currentOrder?.taskState,
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude)
+
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ type = temp.type,
+ taskId = temp.taskId,
+ flowType = temp.flowType,
+ userId = temp.userId,
+ vehicleId = temp.vehicleId,
+ currentState = temp.currentState,
+ offlineMode = 1,
+ operateTime = temp.operateTime,
+ updateTaskLat = temp.lat,
+ updateTaskLng = temp.lng,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ offlineTitle = "救援车发车,正在赶往现场",
+ offlineType = 1)
+ insertOfflineTask(offlineUpdateTaskBean)
+
+ updateOrder(getCurrentOrder()?.copy(taskState = getCurrentOrder()?.getNextStatus()))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(nextState = getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+
+ private fun updateTask() {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(
+ type = "ARRIVE_DEST",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ flowType = GlobalData.currentOrder?.flowType,
+ currentState = GlobalData.currentOrder?.taskState,
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude)
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun doUploadTask(request: UpdateTaskRequest) {
+ if (!getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateOfflineTask(taskRequest = request)
+ return
+ }
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = {
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ updateState(uiState = uiState.value.copy(orderInfo = getCurrentOrder()))
+ buildMarkers(getCurrentOrder())
+ searchDrivingRoute(getCurrentOrder())
+// dispatch(Action.StartTimer)
+ }
+
+ private fun searchDrivingRoute(orderInfo: OrderInfo?) {
+ // 如果没有当前位置,则不进行路径规划
+ if (GlobalData.currentLocation == null) return
+
+ val currentPoint = LatLonPoint(
+ GlobalData.currentLocation?.latitude ?: 0.0,
+ GlobalData.currentLocation?.longitude ?: 0.0
+ )
+
+ // 获取目的地坐标
+ val destPoint = if (orderInfo?.distLat != null && orderInfo.distLat != 0.0 &&
+ orderInfo.distLng != null && orderInfo.distLng != 0.0
+ ) {
+ LatLonPoint(orderInfo.distLat!!, orderInfo.distLng!!)
+ } else null
+
+ // 如果没有目的地,则不进行规划
+ if (destPoint == null) return
+
+ // 使用 Application Context 创建路由搜索对象
+ val routeSearch = RouteSearch(GlobalData.application)
+ routeSearch.setRouteSearchListener(object : RouteSearch.OnRouteSearchListener {
+ override fun onDriveRouteSearched(result: DriveRouteResult?, errorCode: Int) {
+ if (errorCode == 1000 && result != null && result.paths.isNotEmpty()) {
+ val path = result.paths[0]
+ val points = path.steps.flatMap { step ->
+ step.polyline.map { LatLng(it.latitude, it.longitude) }
+ }
+ val durationInSeconds = path.duration
+ val arrivalTime = SimpleDateFormat("HH:mm:ss",
+ Locale.getDefault()).format(System.currentTimeMillis() + durationInSeconds * 1000)
+
+ updateState(uiState.value.copy(routePoints = points, estimatedArrivalTime =arrivalTime, remainingDistance = path.distance ))
+ } else {
+ LogUtil.print("searchDrivingRoute", "路径规划失败: errorCode=$errorCode")
+ }
+ }
+
+ override fun onBusRouteSearched(p0: BusRouteResult?, p1: Int) {}
+ override fun onWalkRouteSearched(p0: WalkRouteResult?, p1: Int) {}
+ override fun onRideRouteSearched(p0: RideRouteResult?, p1: Int) {}
+ })
+
+ // 规划当前位置到目的地的路线
+ val query = RouteSearch.FromAndTo(currentPoint, destPoint)
+ val driveQuery = RouteSearch.DriveRouteQuery(query, RouteSearch.DrivingDefault, null, null, "")
+ routeSearch.calculateDriveRouteAsyn(driveQuery)
+ }
+
+ private fun calculateRemainingDistance(): Pair {
+ val currentLocation = GlobalData.currentLocation ?: return Pair(0f, "")
+ val orderInfo = _uiState.value.orderInfo ?: return Pair(0f, "")
+
+ // 计算到目的地的距离
+ val distance = if (orderInfo.distLat != null && orderInfo.distLng != null) {
+ AMapUtils.calculateLineDistance(
+ LatLng(currentLocation.latitude, currentLocation.longitude),
+ LatLng(orderInfo.distLat!!, orderInfo.distLng!!)
+ )
+ } else 0f
+
+ if (distance <= 0f) {
+ return Pair(0f, "")
+ }
+
+ // 计算预计到达时间(假设平均速度40km/h)
+ val timeInSeconds = ((distance / 1000.0 * 60.0 / 40.0) * 60).toInt()
+ val calendar = Calendar.getInstance()
+ calendar.add(Calendar.SECOND, timeInSeconds)
+ val arrivalTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(calendar.time)
+
+ return Pair(distance, arrivalTime)
+ }
+
+ private var timerJob: Job? = null
+
+ private fun startTimer() {
+ timerJob?.cancel()
+ timerJob = viewModelScope.launch {
+ while (isActive) {
+ val (distance, arrivalTime) = calculateRemainingDistance()
+ _uiState.update {
+ it.copy(
+ remainingDistance = distance,
+ estimatedArrivalTime = arrivalTime
+ )
+ }
+ delay(1000)
+ }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ timerJob?.cancel()
+ }
+
+ private fun buildMarkers(orderInfo: OrderInfo?) {
+ val markers = arrayListOf()
+ if (orderInfo?.lat != null && orderInfo.lat != 0.0 && orderInfo.lng != null && orderInfo.lng != 0.0) {
+ // 获取矢量图资源文件
+ val startMarkers = MarkerOptions()
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_rescuing_map))
+ .position(LatLng(orderInfo.lat!!, orderInfo.lng!!))
+ .infoWindowEnable(false)
+ .visible(true)
+ markers.add(startMarkers)
+ }
+
+ if (orderInfo?.distLat != null && orderInfo.distLat != 0.0 && orderInfo.distLng != null && orderInfo.distLng != 0.0) {
+ val startMarkers = MarkerOptions()
+ .icon(BitmapDescriptorFactory.fromResource(R.mipmap.sv_dist_map))
+ .position(LatLng(orderInfo.distLat!!, orderInfo.distLng!!))
+ .visible(true)
+ markers.add(startMarkers)
+ }
+ updateState(uiState.value.copy(markers = markers))
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object UploadOffline : Action()
+ data object StartTimer : Action()
+ }
+
+ data class UiState(
+ val orderInfo: OrderInfo? = null,
+ val showCallPhoneDialog: Boolean? = false,
+ val markers: ArrayList? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val showOfflineDialog: Boolean? = null,
+ val routePoints: List? = null,
+ val remainingDistance: Float = 0f,
+ val estimatedArrivalTime: String = ""
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailItemScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailItemScreen.kt
new file mode 100644
index 0000000..563513a
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailItemScreen.kt
@@ -0,0 +1,243 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Call
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import com.za.base.theme.bgColor
+import com.za.base.theme.black5
+import com.za.base.theme.headBgColor
+import com.za.base.view.ChoiceMapDialog
+import com.za.base.view.CommonDialog
+import com.za.bean.db.order.OrderInfo
+import com.za.ext.callPhone
+import com.za.ext.convertToFlowName
+import com.za.ext.copy
+import com.za.servicing.R
+
+@Composable
+fun OrderDetailItemScreen(orderInfo: OrderInfo?) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(state = rememberScrollState())
+ .background(color = bgColor)
+ .padding(5.dp)) {
+ OrderDetailBaseInformationView(orderInfo = orderInfo)
+ OrderDetailServiceInformationView(orderInfo = orderInfo)
+ }
+}
+
+@Composable
+private fun OrderDetailBaseInformationView(orderInfo: OrderInfo?) {
+ val context = LocalContext.current
+ val titleSize = 12.sp
+ val titleColor = Color(0xFF7A7A7A)
+ val contentColor = Color.Black
+
+ var showCallPhoneDialog by remember { mutableStateOf(false) }
+ if (showCallPhoneDialog) {
+ CommonDialog(title = "联系客户",
+ message = orderInfo?.customerName,
+ confirmText = "拨打电话",
+ cancelText = "取消", confirm = {
+ showCallPhoneDialog = false
+ context.callPhone(orderInfo?.customerPhone)
+ }, cancel = {
+ showCallPhoneDialog = false
+ }, dismiss = {
+ showCallPhoneDialog = false
+ })
+ }
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(color = Color.White, shape = RoundedCornerShape(4.dp))
+ .padding(10.dp), verticalArrangement = Arrangement.Top) {
+ Box(contentAlignment = Alignment.CenterStart) {
+ Text(text = "基本信息", color = Color.Black, fontWeight = FontWeight.Medium, fontSize = 16.sp)
+ }
+ HorizontalDivider(color = black5, modifier = Modifier.padding(vertical = 10.dp))
+
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "合同名称", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.orderSource}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "结算类型", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.settleTypeStr}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "工单编号", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.taskCode}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ Spacer(modifier = Modifier.width(5.dp))
+ AsyncImage(model = R.drawable.sv_copy, contentDescription = "", modifier = Modifier
+ .size(15.dp)
+ .clickable {
+ orderInfo?.taskCode?.copy(context = context)
+ })
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "客户姓名", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.customerName}",
+ color = headBgColor,
+ textDecoration = TextDecoration.Underline,
+ fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.clickable {
+ showCallPhoneDialog = true
+ })
+ Icon(imageVector = Icons.Default.Call, contentDescription = "", tint = headBgColor, modifier = Modifier.size(10.dp))
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "客户车牌号", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.carNo}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ Spacer(modifier = Modifier.width(5.dp))
+ AsyncImage(model = R.drawable.sv_copy, contentDescription = "", modifier = Modifier.size(15.dp))
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "车型", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.carModel}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+}
+
+
+@Composable
+private fun OrderDetailServiceInformationView(orderInfo: OrderInfo?) {
+ val titleSize = 12.sp
+ val titleColor = Color(0xFF7A7A7A)
+ val contentColor = Color.Black
+
+ // 1 事发地 2 目的地
+ var showChoiceMapDialog by remember { mutableStateOf(null) }
+ if (showChoiceMapDialog != null) {
+ ChoiceMapDialog(
+ dismiss = { showChoiceMapDialog = null },
+ lat = orderInfo?.lat.takeIf { showChoiceMapDialog == 1 } ?: orderInfo?.distLat,
+ lng = orderInfo?.lng.takeIf { showChoiceMapDialog == 1 } ?: orderInfo?.distLng,
+ address = orderInfo?.address.takeIf { showChoiceMapDialog == 1 }
+ ?: orderInfo?.distAddress,
+ )
+ }
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(color = Color.White, shape = RoundedCornerShape(4.dp))
+ .padding(10.dp), verticalArrangement = Arrangement.Top) {
+ Box(contentAlignment = Alignment.CenterStart) {
+ Text(text = "服务信息", color = Color.Black, fontWeight = FontWeight.Medium, fontSize = 16.sp)
+ }
+ HorizontalDivider(color = black5, modifier = Modifier.padding(vertical = 10.dp))
+
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "服务类型", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.serviceTypeName}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "车辆位于", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.addressProperty}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text(text = "事发地", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.address}",
+ color = headBgColor,
+ textDecoration = TextDecoration.Underline,
+ fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.clickable { showChoiceMapDialog = 1 })
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text(text = "事发地备注", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.addressRemark}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+
+ if (!orderInfo?.distAddress.isNullOrBlank()) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text(text = "目的地", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.distAddress}",
+ color = headBgColor,
+ textDecoration = TextDecoration.Underline,
+ fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.clickable { showChoiceMapDialog = 2 })
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
+ Text(text = "目的地备注", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.distAddressRemark}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "订单状态", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "${orderInfo?.convertToFlowName()}", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "备注", color = titleColor, fontSize = titleSize, fontWeight = FontWeight.Medium, modifier = Modifier.width(75.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = orderInfo?.importantTip
+ ?: "", color = contentColor, fontSize = titleSize, fontWeight = FontWeight.Medium)
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailScreen.kt
new file mode 100644
index 0000000..f2ee8ee
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderDetailScreen.kt
@@ -0,0 +1,96 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import com.za.base.theme.headBgColor
+import com.za.base.theme.white80
+import com.za.base.view.HeadView
+import com.za.bean.db.order.OrderInfo
+import com.za.ext.finish
+import kotlinx.coroutines.launch
+
+@Composable
+fun OrderDetailScreen(orderInfo: OrderInfo?) {
+ val context = LocalContext.current
+ val titleList = listOf("订单详情", "案件照片")
+ val pagerState = rememberPagerState(initialPage = 0, pageCount = { titleList.size })
+ val scope = rememberCoroutineScope()
+ Scaffold(topBar = {
+ HeadView(title = "案件信息", onBack = { context.finish() })
+ }) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(it)) {
+
+ Row(modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(state = rememberScrollState())
+ .height(30.dp)
+ .background(color = headBgColor), verticalAlignment = Alignment.CenterVertically) {
+ for (i in 0 until pagerState.pageCount) {
+ if (pagerState.currentPage == i) {
+ Column(modifier = Modifier
+ .weight(1f),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = titleList[i], color = Color.White)
+ HorizontalDivider(thickness = 0.dp,
+ modifier = Modifier
+ .width(25.dp)
+ .height(3.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(8.dp)))
+ }
+ } else {
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ scope.launch {
+ pagerState.animateScrollToPage(page = i)
+ }
+ }, contentAlignment = Alignment.Center) {
+ Text(text = titleList[i], color = white80)
+ }
+ }
+ }
+ }
+
+ HorizontalPager(state = pagerState, modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
+ when (pagerState.currentPage) {
+ 0 -> OrderDetailItemScreen(orderInfo = orderInfo)
+ 1 -> OrderPhotoScreen(orderInfo = orderInfo)
+ 2 -> OrderSettleScreen(orderInfo = orderInfo)
+ 3 -> OrderEleScreen(orderInfo = orderInfo)
+ 4 -> OrderTriceScreen(orderInfo = orderInfo)
+ }
+ }
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderEleScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderEleScreen.kt
new file mode 100644
index 0000000..e324a7c
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderEleScreen.kt
@@ -0,0 +1,17 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.za.bean.db.order.OrderInfo
+
+@Composable
+fun OrderEleScreen(orderInfo: OrderInfo?) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(text = "电子工单", color = Color.Black)
+ }
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderOnSitePhoto.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderOnSitePhoto.kt
new file mode 100644
index 0000000..7888be2
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderOnSitePhoto.kt
@@ -0,0 +1,77 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.lazy.staggeredgrid.items
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.za.base.view.CommonDialog
+import com.za.base.view.EmptyView
+import com.za.base.view.HeadView
+import com.za.bean.db.order.OrderInfo
+import com.za.ext.finish
+
+@Composable
+fun OrderOnSitePhotoScreen(orderInfo: OrderInfo?) {
+ val context = LocalContext.current
+ val showPreviewPhotoDialog = remember { mutableStateOf("") }
+ if (!showPreviewPhotoDialog.value.isNullOrBlank()) {
+ CommonDialog(title = "预览",
+ confirm = { showPreviewPhotoDialog.value = null },
+ cancelText = "取消",
+ confirmText = "确定",
+ cancelEnable = true,
+ cancel = {
+ showPreviewPhotoDialog.value = null
+ },
+ dismiss = {
+ showPreviewPhotoDialog.value = null
+ }, content = {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp), contentAlignment = Alignment.Center) {
+ AsyncImage(model = showPreviewPhotoDialog.value,
+ contentDescription = "",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillWidth)
+ }
+ })
+ }
+
+ Scaffold(topBar = { HeadView(title = "现场环境照片", onBack = { context.finish() }) }) { it ->
+ if (orderInfo?.customerReportImgs.isNullOrBlank() || orderInfo?.customerReportImgs?.split(",").isNullOrEmpty()) {
+ EmptyView()
+ } else {
+ LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2),
+ contentPadding = PaddingValues(5.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)) {
+ items(items = orderInfo?.customerReportImgs?.split(",") ?: arrayListOf()) {
+ AsyncImage(model = it, contentDescription = "", modifier = Modifier
+ .size(180.dp, 110.dp)
+ .clickable {
+ showPreviewPhotoDialog.value = it
+ }
+ .padding(3.dp), contentScale = ContentScale.FillHeight)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderPhotoScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderPhotoScreen.kt
new file mode 100644
index 0000000..d10149d
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderPhotoScreen.kt
@@ -0,0 +1,16 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.za.bean.db.order.OrderInfo
+
+@Composable
+fun OrderPhotoScreen(orderInfo: OrderInfo?) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(text = "案件照片", color = Color.Black)
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsActivity.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsActivity.kt
new file mode 100644
index 0000000..596a9c4
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsActivity.kt
@@ -0,0 +1,38 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.compose.runtime.Composable
+import com.za.base.BaseActivity
+import com.za.base.Const
+import com.za.bean.db.order.OrderInfo
+
+class OrderRequirementsActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ val orderInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra("orderInfo", OrderInfo::class.java)
+ } else {
+ intent.getParcelableExtra("orderInfo")
+ }
+
+ val type = intent.getIntExtra("type", 0)
+ when (type) {
+ Const.InServiceSettingType.ON_SITE_PHOTO -> OrderOnSitePhotoScreen(orderInfo)
+ Const.InServiceSettingType.ORDER_REQUIREMENTS -> OrderRequirementsScreen(orderInfo = orderInfo)
+ Const.InServiceSettingType.ORDER_DETAIL -> OrderDetailScreen(orderInfo = orderInfo)
+ Const.InServiceSettingType.ORDER_GIVE_UP -> OrderRequirementsScreen(orderInfo = orderInfo)
+ }
+ }
+
+ companion object {
+ // type 0 现场照片
+ fun goOrderRequirementsActivity(context: Context, orderInfo: OrderInfo?, type: Int) {
+ val intent = Intent(context, OrderRequirementsActivity::class.java)
+ intent.putExtra("type", type)
+ intent.putExtra("orderInfo", orderInfo)
+ context.startActivity(intent)
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsScreen.kt
new file mode 100644
index 0000000..0bd20b7
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderRequirementsScreen.kt
@@ -0,0 +1,135 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import com.za.base.theme.black5
+import com.za.base.theme.black65
+import com.za.base.view.HeadView
+import com.za.bean.db.order.OrderInfo
+import com.za.ext.finish
+import com.za.servicing.R
+
+@Composable
+fun OrderRequirementsScreen(orderInfo: OrderInfo?) {
+ val context = LocalContext.current
+ Scaffold(topBar = {
+ HeadView(title = "案件要求", onBack = {
+ context.finish()
+ })
+ }) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(state = rememberScrollState())
+ .padding(it)
+ .padding(horizontal = 16.dp)) {
+ Spacer(modifier = Modifier.height(12.dp))
+ OrderRequirementsItemView(title = "特殊提醒", content = orderInfo?.otherNotes)
+ OrderRequirementsItemView(title = "收费标准", content = orderInfo?.feeStandard)
+ if (!orderInfo?.carModel.isNullOrBlank()) {
+ CarModeView(orderInfo = orderInfo)
+ }
+ OrderRequirementsItemView(title = "救援要求", content = orderInfo?.taskNotes)
+ OrderRequirementsItemView(title = "客户要求", content = orderInfo?.customerNotes)
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun OrderRequirementsItemView(title: String?, content: String?) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ model = R.drawable.sv_warn_red,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = title ?: "",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = content?.replace("
", "\n\n") ?: "无",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Normal,
+ color = black65,
+ lineHeight = 20.sp,
+ modifier = Modifier.padding(start = 24.dp)
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ HorizontalDivider(
+ color = black5,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+}
+
+@Composable
+private fun CarModeView(orderInfo: OrderInfo?) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ model = R.drawable.sv_warn_red,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = "车型 ${orderInfo?.carModel}",
+ color = Color.Black,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.Medium
+ )
+ if (orderInfo?.linkToDocs == true) {
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ text = "点击查看相关资料",
+ color = Color(0xFFffa500),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(12.dp))
+ HorizontalDivider(
+ color = black5,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderSettleScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderSettleScreen.kt
new file mode 100644
index 0000000..eb9aca2
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderSettleScreen.kt
@@ -0,0 +1,16 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.za.bean.db.order.OrderInfo
+
+@Composable
+fun OrderSettleScreen(orderInfo: OrderInfo?) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(text = "结算单", color = Color.Black)
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderTriceScreen.kt b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderTriceScreen.kt
new file mode 100644
index 0000000..03ca3fc
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/in_servicing_setting/OrderTriceScreen.kt
@@ -0,0 +1,16 @@
+package com.za.ui.servicing.in_servicing_setting
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.za.bean.db.order.OrderInfo
+
+@Composable
+fun OrderTriceScreen(orderInfo: OrderInfo?) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(text = "订单轨迹", color = Color.Black)
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/operation/InOperationActivity.kt b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationActivity.kt
new file mode 100644
index 0000000..a940324
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationActivity.kt
@@ -0,0 +1,30 @@
+package com.za.ui.servicing.operation
+
+import androidx.compose.runtime.Composable
+import com.za.base.BaseActivity
+import com.za.common.GlobalData
+import com.za.ext.getEleState
+import com.za.ui.servicing.ele_check.EleSignCheckScreen
+import com.za.ui.servicing.ele_sign.EleSignScreen
+
+class InOperationActivity : BaseActivity() {
+
+ @Composable
+ override fun ContentView() {
+ //拖车流程
+ if (2 == GlobalData.currentOrder?.flowType || GlobalData.currentOrder?.serviceTypeName?.contains("困境") == true) {
+ if (GlobalData.currentOrder?.getEleState() == 1) {
+ EleSignScreen()
+ } else if (GlobalData.currentOrder?.getEleState() == 2) {
+ InOperationScreen()
+ } else {
+ EleSignCheckScreen()
+ }
+ } else {
+ InOperationScreen()
+ }
+ }
+
+}
+
+
diff --git a/servicing/src/main/java/com/za/ui/servicing/operation/InOperationScreen.kt b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationScreen.kt
new file mode 100644
index 0000000..45d97ca
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationScreen.kt
@@ -0,0 +1,98 @@
+package com.za.ui.servicing.operation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.ui.servicing.InServicingPhotoView
+import com.za.ui.servicing.view.InServicingHeadView
+
+@Composable
+fun InOperationScreen(vm: InOperationVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(InOperationVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = { vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false))) },
+ confirm = {
+ vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(InOperationVm.Action.UpdateTask)
+ })
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(
+ cancelText = "取消",
+ confirmText = "离线上传",
+ title = "任务提交失败,是否进行离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = { vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false))) },
+ confirm = {
+ vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(InOperationVm.Action.UpdateOffline)
+ })
+ }
+
+
+ Scaffold(topBar = {
+ InServicingHeadView(title = "准备拖车".takeIf { uiState.value.orderInfo?.flowType == 2 }
+ ?: "作业中",
+ orderInfo = uiState.value.orderInfo,
+ onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "作业完成,开始拖车".takeIf { uiState.value.orderInfo?.flowType == 2 }
+ ?: "作业完成") {
+ vm.dispatch(InOperationVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = true)))
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ contentPadding = PaddingValues(10.dp)) {
+ itemsIndexed(items = uiState.value.photoTemplateList
+ ?: arrayListOf(), key = { _, item -> item.hashCode() }) { index: Int, item: PhotoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = item, index = index + 1, success = {
+ vm.dispatch(InOperationVm.Action.UpdatePhotoTemplate(it))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/operation/InOperationVm.kt b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationVm.kt
new file mode 100644
index 0000000..1791401
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/operation/InOperationVm.kt
@@ -0,0 +1,263 @@
+package com.za.ui.servicing.operation
+
+import com.alibaba.fastjson.JSONObject
+import com.amap.api.location.AMapLocation
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.bean.request.UpdateTaskRequest
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getNextStatus
+import com.za.ext.toJson
+import com.za.net.CommonMethod
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.service.location.ZdLocationManager
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class InOperationVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateTask -> updateTask()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ is Action.UpdateOffline -> updateOffline()
+ }
+ }
+
+ private fun updateTemplate() {
+ getCurrentPhotoTemplate(success = {
+ updateState(uiState.value.copy(photoTemplateList = it))
+ })
+ }
+
+ private fun updateOffline() {
+ uiState.value.photoTemplateList?.forEachIndexed { index, photoTemplateInfo ->
+ if (photoTemplateInfo.photoType == 2) {
+ return@forEachIndexed
+ }
+
+ if (!photoTemplateInfo.photoLocalWaterMarkerPath.isNullOrBlank() && photoTemplateInfo.photoUploadPath.isNullOrBlank()) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ realTakePhotoTime = photoTemplateInfo.realTakePhotoTime,
+ photoSource = photoTemplateInfo.photoSource,
+ time = photoTemplateInfo.time,
+ imageLat = photoTemplateInfo.lat,
+ imageLng = photoTemplateInfo.lng,
+ imageAddress = photoTemplateInfo.address,
+ imageIndex = index,
+ needWater = photoTemplateInfo.needWaterMarker,
+ needPhoneBrand = photoTemplateInfo.needShowPhoneBrand,
+ imageLocalPath = photoTemplateInfo.photoLocalPath,
+ advanceTime = uiState.value.orderInfo?.advanceTime,
+ photoLocalWaterMarkerPath = photoTemplateInfo.photoLocalWaterMarkerPath,
+ offlineMode = 1,
+ taskId = uiState.value.orderInfo?.taskId,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ offlineTitle = "${photoTemplateInfo.imageTitle}",
+ offlineType = 2)
+ insertOfflineTask(offlineUpdateTaskBean)
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true,
+ success = {
+ LoadingManager.hideLoading()
+ doUploadOfflineTask(it, tempPhotoList = tempPhotoList)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ LogUtil.print("$tag updateOffline", "定位获取失败$it")
+ })
+ }
+
+ private fun doUploadOfflineTask(it: AMapLocation?, tempPhotoList: ArrayList) {
+ val taskRequest = UpdateTaskRequest(type = "OPERATION",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ flowType = GlobalData.currentOrder?.flowType,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "OPERATION",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it?.latitude,
+ lng = it?.longitude,
+ address = it?.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+
+ if (!getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ type = taskRequest.type,
+ taskId = taskRequest.taskId,
+ userId = taskRequest.userId,
+ vehicleId = taskRequest.vehicleId,
+ currentState = taskRequest.currentState,
+ offlineMode = 1,
+ operateTime = taskRequest.operateTime,
+ updateTaskLat = taskRequest.lat,
+ updateTaskLng = taskRequest.lng,
+ flowType = taskRequest.flowType,
+ templatePhotoInfoList = taskRequest.templatePhotoInfoList,
+ updateTaskAddress = taskRequest.address,
+ advanceTime = uiState.value.orderInfo?.advanceTime,
+ taskCode = uiState.value.orderInfo?.taskCode,
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ offlineTitle = "准备拖车",
+ offlineType = 1)
+ insertOfflineTask(offlineUpdateTaskBean)
+
+ updateOrder(getCurrentOrder()?.copy(taskState = getCurrentOrder()?.getNextStatus()))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(nextState = getCurrentOrder()?.taskState), orderInfo = getCurrentOrder()))
+ }
+ }
+
+ private fun updateTask() {
+ if (uiState.value.photoTemplateList.isNullOrEmpty()) {
+ ToastUtils.showShort("作业照片不能为空!")
+ return
+ }
+
+ //先检查照片是否是为空
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+ if (it.doHaveFilm == 1 && it.photoLocalWaterMarkerPath.isNullOrBlank()) {
+ ToastUtils.showLong("请上传 ${it.imageTitle}!!")
+ return
+ }
+ }
+
+ //先判断是否有离线任务
+ if (!getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateOffline()
+ return
+ }
+
+ uiState.value.photoTemplateList?.forEach {
+ if (it.photoType == 2) {
+ return@forEach
+ }
+
+ if (it.photoUploadStatus == 4) {
+ ToastUtils.showLong("${it.photoName}正在上传,请等待照片上传完成!")
+ return
+ }
+
+ if (!it.photoLocalWaterMarkerPath.isNullOrBlank() && it.photoUploadPath.isNullOrBlank()) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ return
+ }
+ }
+
+ val tempPhotoList = arrayListOf()
+ uiState.value.photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(isNeedAddress = true, success = {
+ LoadingManager.hideLoading()
+ val taskRequest = UpdateTaskRequest(type = "OPERATION",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ flowType = GlobalData.currentOrder?.flowType,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = "OPERATION",
+ offlineMode = 0,
+ operateTime = System.currentTimeMillis().toString(),
+ lat = it.latitude,
+ lng = it.longitude,
+ address = it.address,
+ templatePhotoInfoList = tempPhotoList.toList())
+ doUploadTask(request = taskRequest)
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun doUploadTask(request: UpdateTaskRequest) {
+ LoadingManager.showLoading()
+ CommonMethod.updateTask(request, success = {
+ LoadingManager.hideLoading()
+
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it, orderInfo = getCurrentOrder()))
+ }, failed = { msg, code ->
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("$tag doUploadTask", "状态更新失败==${request.toJson()} msg==$msg")
+ })
+ }
+
+ private fun init() {
+ getCurrentPhotoTemplate({
+ updateState(uiState.value.copy(photoTemplateList = it, orderInfo = getCurrentOrder()))
+ }, failure = {
+ ToastUtils.showShort(it)
+ })
+ }
+
+
+ sealed class Action {
+ data object Init : Action()
+ data object UpdateTask : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo: PhotoTemplateInfo) : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object UpdateOffline : Action()
+ }
+
+ data class UiState(val orderInfo: OrderInfo? = null,
+ val verifyValue: String? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val showCallPhoneDialog: Boolean? = null,
+ val showOfflineDialog: Boolean? = null,
+ val photoTemplateList: List? = null)
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryScreen.kt
new file mode 100644
index 0000000..ef94ad0
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryScreen.kt
@@ -0,0 +1,106 @@
+package com.za.ui.servicing.order_confirm
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.view.CommonButton
+import com.za.base.view.HeadView
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.ext.finish
+import com.za.ext.goStatusPage
+import com.za.ui.servicing.InServicingPhotoView
+
+@Composable
+fun ChangeBatteryScreen(vm: ChangeBatteryVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(ChangeBatteryVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ uiState.value.orderInfo?.goStatusPage(context)
+ context.finish()
+ }
+
+ Scaffold(topBar = {
+ HeadView(title = "更换电瓶", onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "提交照片") {
+ vm.dispatch(ChangeBatteryVm.Action.Upload)
+ }
+ }) { it ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ contentPadding = PaddingValues(10.dp)) {
+
+ item {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(4.dp))
+ .padding(10.dp)) {
+ Text(text = "是否更换电瓶:", color = Color.Black)
+ Spacer(modifier = Modifier.width(10.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "更换", color = Color.Black)
+ Checkbox(checked = uiState.value.eleWorkOrderBean?.changeBattery == true, onCheckedChange = {
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean
+ vm.dispatch(ChangeBatteryVm.Action.UpdateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean?.copy(changeBattery = true))))
+ }, colors = CheckboxDefaults.colors()
+ .copy(uncheckedBoxColor = Color.Gray,
+ checkedBorderColor = Color.Transparent,
+ uncheckedBorderColor = Color.Transparent,
+ checkedBoxColor = Color.Red))
+ }
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "不更换", color = Color.Black)
+ Checkbox(checked = uiState.value.eleWorkOrderBean?.changeBattery == false,
+ onCheckedChange = {
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean
+ vm.dispatch(ChangeBatteryVm.Action.UpdateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean?.copy(changeBattery = false))))
+ }, colors = CheckboxDefaults.colors()
+ .copy(uncheckedBoxColor = Color.Gray,
+ checkedBorderColor = Color.Transparent,
+ uncheckedBorderColor = Color.Transparent,
+ checkedBoxColor = Color.Red))
+ }
+ }
+ }
+
+ itemsIndexed(items = uiState.value.changeBatteryPhoto
+ ?: arrayListOf(), key = { _, item -> item.hashCode() }) { index: Int, item: PhotoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = item, index = index, success = {
+ vm.dispatch(ChangeBatteryVm.Action.UpdatePhotoTemplate(it))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryVm.kt
new file mode 100644
index 0000000..809915a
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ChangeBatteryVm.kt
@@ -0,0 +1,198 @@
+package com.za.ui.servicing.order_confirm
+
+
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.Const
+import com.za.base.view.LoadingManager
+import com.za.bean.BatteryCostQueryBean
+import com.za.bean.BatteryCostQueryRequest
+import com.za.bean.UploadChangeBatteryRequest
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import com.za.room.RoomHelper
+import com.za.servicing.R
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class ChangeBatteryVm : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.Upload -> upload()
+ is Action.IsChangeBattery -> updateIsChangeBattery(action.isChange)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ }
+ }
+
+ private fun updateTemplate() {
+ val list = RoomHelper.db?.changeBatteryDao()?.getAllChangeBatteryPhoto(GlobalData.currentOrder?.taskCode
+ ?: "")
+ updateState(uiState.value.copy(changeBatteryPhoto = list))
+ }
+
+ private fun updateIsChangeBattery(isChange: Boolean) {
+ updateState(uiState.value.copy(eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(changeBattery = isChange)))
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(changeBattery = isChange)
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ }
+ }
+
+ private fun upload() {
+ if (uiState.value.eleWorkOrderBean?.changeBattery == true) {
+ val list = uiState.value.changeBatteryPhoto
+ if (list.isNullOrEmpty()) {
+ ToastUtils.showLong("更换电瓶照片未拍!")
+ return
+ }
+ list.forEach {
+ if (it.photoUploadPath.isNullOrBlank()) {
+ ToastUtils.showLong("${it.photoName}未拍!")
+ return
+ }
+ }
+ }
+ batteryCostQuery(false)
+ }
+
+ private fun doUpload(isPayment: Boolean?) {
+ val list = uiState.value.changeBatteryPhoto
+ val uploadChangeBatteryRequest = UploadChangeBatteryRequest(
+ userOrderId = uiState.value.orderInfo?.userOrderId,
+ replaceBatteryImgList = list?.map { it.photoUploadPath ?: "" },
+ hasReplaceBattery = if (uiState.value.eleWorkOrderBean?.changeBattery == true) {
+ 1
+ } else {
+ 0
+ },
+ isPayment = isPayment)
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().saveReplaceBatteryPhoto(uploadChangeBatteryRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ LogUtil.print("changeBattery doUpload", it.toJson() ?: "")
+ batteryCostQuery(true)
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showLong(msg)
+ LogUtil.print("changeBattery doUpload", msg ?: "")
+ }
+ })
+ }
+
+ private fun init() {
+ getPhotos()
+ }
+
+ private fun batteryCostQuery(isAgainQuery: Boolean = false) {
+ LoadingManager.showLoading()
+ val batteryCostQueryRequest = BatteryCostQueryRequest(userOrderId = uiState.value.orderInfo?.userOrderId, uiState.value.orderInfo?.taskId)
+ RetrofitHelper.getDefaultService().batteryCostQuery(batteryCostQueryRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: BatteryCostQueryBean?) {
+ LoadingManager.hideLoading()
+ if (isAgainQuery) {
+ if (it?.isPayment == true && uiState.value.eleWorkOrderBean?.changeBattery == true) {
+// ChangeBatteryCostActivity.goChangeBatteryCostActivity(mContext, model.getOrderInfo().userOrderId, model.getOrderInfo().taskId)
+ } else {
+ RoomHelper.db?.changeBatteryDao()?.deleteAll()
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(uiState.value.orderInfo?.taskId
+ ?: 0)
+ eleWorkOrderBean?.copy(changeBattery = false)?.let { it1 -> RoomHelper.db?.eleWorkOrderDao()?.update(it1) }
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(uiState.value.orderInfo?.taskState)))
+ LogUtil.print("againQueryIsNeedReceiveMoney", batteryCostQueryRequest.toString())
+ return
+ }
+ }
+ doUpload(it?.isPayment)
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ LogUtil.print("queryIsNeedReceiveMoney", "code==$code request=$batteryCostQueryRequest")
+ }
+ })
+ }
+
+ private fun getPhotos() {
+ val titles = arrayOf("车牌、旧电瓶、新电瓶合照", "新电瓶安装完成照片", "新电瓶二维码照片", "新电瓶电子质保卡照片", "新电瓶安装完成之后仪表盘点亮的照片")
+ val tips = arrayOf("将新旧电瓶放置于车辆牌照前方,清晰拍摄车牌照、旧电瓶、新电瓶在一起的合照", "清晰拍摄新电瓶安装完成后的照片",
+ "清晰拍摄新电瓶上二维码的照片", "提供新电瓶电子质保卡的截图照片", "清晰拍摄新电瓶安装完成后的仪表盘点亮的照片")
+ val photoContracts = arrayOf(R.mipmap.img_old_new_battery,
+ R.mipmap.img_change_battery_complate,
+ R.mipmap.img_battery_qr,
+ R.mipmap.img_battery_quality, R.mipmap.dadianhou)
+
+ val changeBatteryPhotoList = RoomHelper.db?.changeBatteryDao()
+ ?.getAllChangeBatteryPhoto(GlobalData.currentOrder?.taskCode ?: "")
+ LogUtil.print("changeBatteryPhotoList ", " $changeBatteryPhotoList")
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(GlobalData.currentOrder?.taskId
+ ?: 0)
+ if (changeBatteryPhotoList.isNullOrEmpty()) {
+ val list = mutableListOf()
+ titles.forEachIndexed { i, s ->
+ val changeBattery = PhotoTemplateInfo(
+ photoName = s,
+ imageTitle = s,
+ doHaveFilm = 1,
+ needWaterMarker = true,
+ myCustomPhotoType = Const.PhotoType.ChangeBattery,
+ taskCode = GlobalData.currentOrder?.taskCode,
+ imageDescription = tips[i],
+ photoUrl = "${photoContracts[i]}")
+ list.add(changeBattery)
+ RoomHelper.db?.changeBatteryDao()?.insert(changeBattery)
+ }
+ val changeBatteryPhotoList1 = RoomHelper.db?.changeBatteryDao()
+ ?.getAllChangeBatteryPhoto(GlobalData.currentOrder?.taskCode ?: "")
+ LogUtil.print("changeBatteryPhotoList1 ", " $changeBatteryPhotoList1")
+ updateState(uiState.value.copy(changeBatteryPhoto = RoomHelper.db?.changeBatteryDao()
+ ?.getAllChangeBatteryPhoto(GlobalData.currentOrder?.taskCode ?: ""),
+ orderInfo = GlobalData.currentOrder,
+ eleWorkOrderBean = eleWorkOrderBean))
+ return
+ }
+ updateState(uiState.value.copy(changeBatteryPhoto = changeBatteryPhotoList, orderInfo = GlobalData.currentOrder, eleWorkOrderBean = eleWorkOrderBean))
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ data object Upload : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo: PhotoTemplateInfo) : Action()
+ data class IsChangeBattery(val isChange: Boolean) : Action()
+ }
+
+ data class UiState(
+ val orderInfo: OrderInfo? = null,
+ val eleWorkOrderBean: EleWorkOrderBean? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val changeBatteryPhoto: List? = null,
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleScreen.kt
new file mode 100644
index 0000000..9fb8445
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleScreen.kt
@@ -0,0 +1,673 @@
+package com.za.ui.servicing.order_confirm
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.blankj.utilcode.util.FileUtils
+import com.blankj.utilcode.util.TimeUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.theme.black65
+import com.za.base.theme.headBgColor
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.base.view.HeadView
+import com.za.bean.db.ele.EleCarDamagePhotoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.common.GlobalData
+import com.za.common.util.AppFileManager
+import com.za.common.util.ServicingSpeechManager
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.servicing.R
+import com.za.ui.view.SignatureView
+import java.io.File
+import kotlin.math.ceil
+
+@Composable
+fun ConfirmEleScreen(vm : ConfirmEleVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(ConfirmEleVm.Action.Init)
+ if (GlobalData.currentOrder?.flowType == Const.TUO_CHE) {
+ ServicingSpeechManager.playOrderAcceptSign(context)
+ } else {
+ ServicingSpeechManager.playCarOwnerSign(context)
+ }
+ }
+
+
+ if (uiState.value.goNextPage != null) {
+ goNextPage(uiState.value.goNextPage?.nextState, context)
+ context.finish()
+ }
+
+ if (uiState.value.isGoNextPageDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "前往下一步",
+ title = "是否前往下一步?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(isGoNextPageDialog = false)))
+ vm.dispatch(ConfirmEleVm.Action.Upload)
+ })
+ }
+
+ //离线操作框
+ if (uiState.value.showOfflineDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "离线上传",
+ title = "上传失败,是否进行离线提交?",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ },
+ confirm = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(showOfflineDialog = false)))
+ vm.dispatch(ConfirmEleVm.Action.UploadOffline)
+ })
+ }
+
+ if (uiState.value.showServicePeopleSignDialog == true) {
+ CommonDialog(cancelText = "取消",
+ confirmText = "保存",
+ title = "是否将签名保存,以后的工单中默认使用此签名",
+ cancelEnable = true,
+ cancel = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ showServicePeopleSignDialog = false)))
+ },
+ dismiss = {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ showServicePeopleSignDialog = false)))
+ },
+ confirm = {
+ if (uiState.value.eleWorkOrderBean?.localServicePeopleSignPath.isNullOrEmpty()) {
+ ToastUtils.showShort("请先进行签名")
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(showServicePeopleSignDialog = false)))
+ return@CommonDialog
+ }
+ File(uiState.value.eleWorkOrderBean?.localServicePeopleSignPath ?: "").copyTo(File(
+ AppFileManager.getDriverSignPath(context)), true)
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(showServicePeopleSignDialog = false)))
+ })
+ }
+
+
+ Scaffold(topBar = {
+ HeadView(title = "客户签字", onBack = { context.finish() })
+ }) { paddingValues ->
+ LazyColumn(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .background(Color(0xFFF5F5F5)), verticalArrangement = Arrangement.spacedBy(12.dp)) {
+
+ item {
+ EleWorkOrderDetailView(flowType = uiState.value.orderInfo?.flowType,
+ eleWorkOrderBean = uiState.value.eleWorkOrderBean,
+ damagePhoto = uiState.value.damagePhoto)
+ }
+
+ item {
+ if (uiState.value.orderInfo?.flowType != Const.TUO_CHE) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(horizontal = 16.dp, vertical = 10.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 8.dp)) {
+ Text(text = "*",
+ fontWeight = FontWeight.Bold,
+ color = Color.Red,
+ fontSize = 15.sp)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "服务是否成功",
+ fontWeight = FontWeight.Medium,
+ color = Color.Black,
+ fontSize = 15.sp)
+ }
+ Row(modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.UpdateServiceSuccessState(1))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.eleWorkOrderBean?.isSuccess == 1 }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("成功",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.eleWorkOrderBean?.isSuccess == 1 }
+ ?: Color.Black)
+ }
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.UpdateServiceSuccessState(2))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.eleWorkOrderBean?.isSuccess == 2 }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("失败",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.eleWorkOrderBean?.isSuccess == 2 }
+ ?: Color.Black)
+ }
+ }
+ }
+ }
+
+ //是否拥有更换电瓶的能力
+ if (uiState.value.orderInfo?.flowType == Const.SMALL_REPAIR && uiState.value.orderInfo?.hasReplaceBatteryCapable == 1) {
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(horizontal = 16.dp, vertical = 10.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 8.dp)) {
+ Text(text = "*",
+ fontWeight = FontWeight.Bold,
+ color = Color.Red,
+ fontSize = 15.sp)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "是否更换电瓶",
+ fontWeight = FontWeight.Medium,
+ color = Color.Black,
+ fontSize = 15.sp)
+ }
+ Row(modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.IsChangeBattery(true))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.eleWorkOrderBean?.changeBattery == true }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("更换",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.eleWorkOrderBean?.changeBattery == true }
+ ?: Color.Black)
+ }
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.IsChangeBattery(false))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.eleWorkOrderBean?.changeBattery == false }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("不更换",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.eleWorkOrderBean?.changeBattery == false }
+ ?: Color.Black)
+ }
+ }
+ }
+ }
+
+ if (uiState.value.orderInfo?.flowType == Const.TUO_CHE) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(horizontal = 16.dp, vertical = 10.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 8.dp)) {
+ Text(text = "*",
+ fontWeight = FontWeight.Bold,
+ color = Color.Red,
+ fontSize = 15.sp)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "是否加小轮",
+ fontWeight = FontWeight.Medium,
+ color = Color.Black,
+ fontSize = 15.sp)
+ }
+ Row(modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ isAddSmallWheel = true)))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.isAddSmallWheel == true }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("是",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.isAddSmallWheel == true }
+ ?: Color.Black)
+ }
+
+ Box(modifier = Modifier
+ .weight(1f)
+ .clickable {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ isAddSmallWheel = false)))
+ }
+ .border(width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(2.dp))
+ .background(color = headBgColor.takeIf { uiState.value.isAddSmallWheel == false }
+ ?: Color.White, shape = RoundedCornerShape(2.dp))
+ .padding(vertical = 5.dp), contentAlignment = Alignment.Center) {
+ Text("否",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White.takeIf { uiState.value.isAddSmallWheel == false }
+ ?: Color.Black)
+ }
+ }
+
+ Spacer(Modifier.height(5.dp))
+
+ AnimatedVisibility(uiState.value.isAddSmallWheel == true) {
+ OutlinedTextField(
+ value = "${uiState.value.wheelNum ?: ""}",
+ onValueChange = { wheel ->
+ if ((wheel.toIntOrNull() ?: 0) > 4) {
+ ToastUtils.showShort("小轮个数不能超过4个")
+ return@OutlinedTextField
+ }
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ wheelNum = wheel.toIntOrNull())))
+ },
+ label = { Text("小轮个数") },
+ modifier = Modifier.fillMaxWidth(),
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
+ textStyle = TextStyle.Default.copy(fontSize = 16.sp,
+ color = Color.Black),
+ shape = RoundedCornerShape(6.dp),
+ colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = headBgColor,
+ unfocusedBorderColor = Color.Gray.copy(alpha = 0.5f),
+ focusedLabelColor = headBgColor,
+ unfocusedLabelColor = Color.Gray),
+ )
+ }
+ }
+ }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(horizontal = 16.dp, vertical = 10.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()) {
+ AsyncImage(model = R.drawable.sv_star,
+ contentDescription = "",
+ modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "本人对服务完成无异议,同意救援服务商获取合理施救费用。",
+ maxLines = 1,
+ color = Color(0xFFD32F2F),
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ modifier = Modifier
+ .weight(1f)
+ .basicMarquee())
+ }
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(horizontal = 16.dp, vertical = 10.dp)) {
+ Row(modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround) {
+ Column(modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 8.dp)) {
+ Text(text = "接车人签字:".takeIf { uiState.value.orderInfo?.flowType == Const.TUO_CHE }
+ ?: "车主签字:",
+ color = Color.Black,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp)
+ Text(text = "(正楷)",
+ color = Color(0xFFD32F2F),
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ modifier = Modifier.padding(start = 4.dp))
+ }
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .border(width = 1.dp,
+ color = Color(0xFFE0E0E0),
+ shape = RoundedCornerShape(8.dp))
+ .padding(8.dp)) {
+ SignatureView(success = {
+ if (it.isNullOrBlank()) {
+ ToastUtils.showLong("签名合成失败")
+ return@SignatureView
+ }
+ vm.dispatch(ConfirmEleVm.Action.UpdateAcceptSignature(it))
+ },
+ serverPath = uiState.value.eleWorkOrderBean?.serverAcceptCarSignPath
+ ?: uiState.value.eleWorkOrderBean?.localAcceptCarSignPath)
+ }
+ }
+
+ Column(modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center) {
+ Row(verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(bottom = 8.dp)) {
+ Text(text = "服务人员签字:",
+ color = Color.Black,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp)
+ Text(text = "(正楷)",
+ color = Color(0xFFD32F2F),
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ modifier = Modifier.padding(start = 4.dp))
+ }
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .border(width = 1.dp,
+ color = Color(0xFFE0E0E0),
+ shape = RoundedCornerShape(8.dp))
+ .padding(8.dp)) {
+ SignatureView(success = {
+ if (it.isNullOrBlank()) {
+ ToastUtils.showLong("签名合成失败")
+ return@SignatureView
+ }
+ vm.dispatch(ConfirmEleVm.Action.UploadServiceSignature(it))
+
+ if (! FileUtils.isFileExists(File(AppFileManager.getDriverSignPath(
+ context)))
+ ) {
+ vm.updateState(uiState.value.copy(
+ showServicePeopleSignDialog = true))
+ }
+ },
+ serverPath = uiState.value.eleWorkOrderBean?.serverServicePeopleSignPath
+ ?: uiState.value.eleWorkOrderBean?.localServicePeopleSignPath)
+ }
+ }
+ }
+ }
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(20.dp))
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 20.dp)) {
+ CommonButton(
+ text = "上传签字,前往结算单",
+ ) {
+ vm.dispatch(ConfirmEleVm.Action.UpdateState(uiState.value.copy(
+ isGoNextPageDialog = true)))
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun EleWorkOrderDetailView(eleWorkOrderBean : EleWorkOrderBean?,
+ flowType : Int? = null,
+ damagePhoto : List? = null) {
+ val context = LocalContext.current
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ .background(color = Color(0xFFDFE2EA))
+ .padding(10.dp)) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Bottom,
+ horizontalArrangement = Arrangement.SpaceBetween) {
+ AsyncImage(model = R.mipmap.ic_company_brand,
+ contentDescription = "",
+ modifier = Modifier.size(86.dp, 25.dp),
+ contentScale = ContentScale.FillWidth)
+
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ Text(text = "车况检查表",
+ fontSize = 13.sp,
+ color = Color.Black,
+ fontWeight = FontWeight.Bold)
+ }
+ Text(text = "日期:${TimeUtils.millis2String(System.currentTimeMillis(), "yyyy-MM-dd")}",
+ color = Color.Black,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp)
+ }
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .border(0.5.dp, color = Color(0xFF989898))
+ .padding(10.dp)) {
+ Text(text = "一、车辆信息",
+ color = Color.Black,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp)
+
+ Spacer(modifier = Modifier.height(5.dp))
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+
+ Text(text = "车牌号:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = GlobalData.currentOrder?.carNo ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = "车架号后6位:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = eleWorkOrderBean?.carVin ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ }
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+
+ Text(text = "救援类型:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = eleWorkOrderBean?.orderType ?: "",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+
+ }
+
+ if (flowType != Const.SMALL_REPAIR) {
+ Spacer(modifier = Modifier.height(5.dp))
+ Row(modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "车辆损伤情况:", fontSize = 12.sp, color = black65)
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "有损伤".takeIf { true } ?: "无损伤",
+ textDecoration = TextDecoration.Underline,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ }
+
+ if (! damagePhoto.isNullOrEmpty()) {
+ val height = damagePhoto.size.div(3.0)
+ LazyVerticalGrid(modifier = Modifier
+ .fillMaxWidth()
+ .height(80.times(ceil(height)).dp), columns = GridCells.Fixed(3)) {
+ items(items = damagePhoto) { item ->
+ Box(modifier = Modifier
+ .size(110.dp, 76.dp)
+ .padding(5.dp),
+ contentAlignment = Alignment.BottomCenter) {
+ AsyncImage(model = item.serverPath.takeIf { item.path.isNullOrBlank() }
+ ?: item.path,
+ contentDescription = "",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillWidth)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .border(0.5.dp, color = Color(0xFF989898))
+ .padding(10.dp)) {
+ Row {
+ Text(text = "二、服务须知",
+ color = Color.Black,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp)
+ Text(text = "(请顾客认证阅读,并在理解且无异议后签字确认)",
+ color = Color.Black,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 10.sp)
+ }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .verticalScroll(rememberScrollState())) {
+ Text(text = eleWorkOrderBean?.serviceContent.takeIf { ! eleWorkOrderBean?.serviceContent.isNullOrBlank() }
+ ?: context.getString(R.string.service_content),
+ fontSize = 11.sp,
+ color = Color(0xE6252525))
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AsyncImage(model = R.drawable.sv_star_black,
+ contentDescription = "",
+ modifier = Modifier.size(15.dp))
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(text = "客户对上述内容知晓并接受,签字准许实施救援服务。",
+ color = Color.Black,
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp)
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(text = "困境救援案件,为了完成救援而导致的不可避免的车辆损伤,客户自行承担此类风险和损失。",
+ color = Color.Black,
+ fontWeight = FontWeight.Bold,
+ fontSize = 10.sp)
+
+ if (flowType != Const.SMALL_REPAIR) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "客户签名:",
+ fontSize = 10.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black)
+ Spacer(modifier = Modifier.width(5.dp))
+
+ AsyncImage(model = eleWorkOrderBean?.serverCustomSignPath
+ ?: eleWorkOrderBean?.localCustomSignPath,
+ contentDescription = "",
+ modifier = Modifier.size(142.dp, 52.dp),
+ contentScale = ContentScale.Fit)
+ }
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleVm.kt
new file mode 100644
index 0000000..44922ef
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmEleVm.kt
@@ -0,0 +1,316 @@
+package com.za.ui.servicing.order_confirm
+
+
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.FileUtils
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.ele.EleCarDamagePhotoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.SaveEleOrderRequest
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.common.util.AppFileManager
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.room.RoomHelper
+import com.za.service.location.ZdLocationManager
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.io.File
+
+class ConfirmEleVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.Upload -> upload()
+ is Action.UpdateServiceSuccessState -> updateIsServiceSuccess(action.state)
+ is Action.UpdateAcceptSignature -> uploadAcceptSignature(action.path)
+ is Action.UploadServiceSignature -> updateServiceSignature(action.path)
+ is Action.IsChangeBattery -> updateIsChangeBattery(action.isChange)
+ is Action.UploadOffline -> uploadOffline()
+ }
+ }
+
+
+ private fun updateIsChangeBattery(isChange : Boolean) {
+ updateState(uiState.value.copy(eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(
+ changeBattery = isChange)))
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(changeBattery = isChange)
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ }
+ }
+
+ private fun updateIsServiceSuccess(isSuccess : Int) {
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(isSuccess = isSuccess)
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean))
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ }
+ }
+
+ private fun updateServiceSignature(path : String) {
+ LoadingManager.showLoading()
+ val eleWorkOrderBean =
+ uiState.value.eleWorkOrderBean?.copy(localServicePeopleSignPath = path)
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean))
+ LogUtil.print("updateServiceSignature success", eleWorkOrderBean.toJson() ?: "")
+ }
+ CommonMethod.uploadImage(File(path), success = {
+ LoadingManager.hideLoading()
+ if (eleWorkOrderBean != null) {
+ val ele = eleWorkOrderBean.copy(serverServicePeopleSignPath = it)
+ RoomHelper.db?.eleWorkOrderDao()?.update(ele)
+ updateState(uiState.value.copy(eleWorkOrderBean = ele))
+ LogUtil.print("updateServiceSignature success", ele.toJson() ?: "")
+ }
+ }, failed = {
+ LoadingManager.hideLoading()
+ LogUtil.print("updateServiceSignature", "failed==$it")
+ })
+ }
+
+ private fun uploadAcceptSignature(path : String) {
+ LoadingManager.showLoading()
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean?.copy(localAcceptCarSignPath = path)
+ if (eleWorkOrderBean != null) {
+ RoomHelper.db?.eleWorkOrderDao()?.update(eleWorkOrderBean)
+ updateState(uiState.value.copy(eleWorkOrderBean = eleWorkOrderBean))
+ LogUtil.print("updateServiceSignature success", eleWorkOrderBean.toJson() ?: "")
+ }
+ CommonMethod.uploadImage(File(path), success = {
+ LoadingManager.hideLoading()
+ if (eleWorkOrderBean != null) {
+ val ele = eleWorkOrderBean.copy(serverAcceptCarSignPath = it)
+ RoomHelper.db?.eleWorkOrderDao()?.update(ele)
+ updateState(uiState.value.copy(eleWorkOrderBean = ele))
+ LogUtil.print("uploadAcceptSignature success", ele.toJson() ?: "")
+ }
+ }, failed = {
+ LoadingManager.hideLoading()
+ LogUtil.print("uploadAcceptSignature", "failed==$it")
+ })
+ }
+
+
+ private fun uploadOffline() {
+ val eleWorkOrderBean =
+ RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(getCurrentOrder()?.taskId ?: 0)
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+
+ if (uiState.value.eleWorkOrderBean?.serverAcceptCarSignPath.isNullOrBlank()) {
+ val signOfflineUpdateTaskBean =
+ OfflineUpdateTaskBean(imageLocalPath = uiState.value.eleWorkOrderBean?.localAcceptCarSignPath,
+ taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "电子工单-接车人签字",
+ offlineType = 6)
+ insertOfflineTask(signOfflineUpdateTaskBean)
+ }
+
+ if (uiState.value.eleWorkOrderBean?.serverServicePeopleSignPath.isNullOrBlank()) {
+ val signOfflineUpdateTaskBean =
+ OfflineUpdateTaskBean(imageLocalPath = uiState.value.eleWorkOrderBean?.localServicePeopleSignPath,
+ taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "电子工单-服务人员签字",
+ offlineType = 7)
+ insertOfflineTask(signOfflineUpdateTaskBean)
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ offlineTitle = "电子工单-客户签字",
+ offlineType = 3,
+ eleLat = it.latitude,
+ eleLng = it.longitude,
+ offlineMode = 1,
+ isFinish = true,
+ tyreNumber = if (uiState.value.isAddSmallWheel == true) {
+ uiState.value.wheelNum
+ } else null,
+ hasSuccess = eleWorkOrderBean.isSuccess,
+ recipientSignPath = eleWorkOrderBean.serverAcceptCarSignPath,
+ waitstaffSignPath = eleWorkOrderBean.serverServicePeopleSignPath,
+ eleState = 3,
+ userOrderId = getCurrentOrder()?.userOrderId)
+ insertOfflineTask(offlineUpdateTaskBean)
+ }, failed = {
+ LoadingManager.hideLoading()
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(taskId = getCurrentOrder()?.taskId,
+ taskCode = getCurrentOrder()?.taskCode,
+ customerSignPath = uiState.value.eleWorkOrderBean?.serverCustomSignPath,
+ offlineTitle = "电子工单-车况检查表",
+ offlineType = 3,
+ eleLat = GlobalData.currentLocation?.latitude,
+ eleLng = GlobalData.currentLocation?.longitude,
+ offlineMode = 1,
+ isFinish = true,
+ tyreNumber = if (uiState.value.isAddSmallWheel == true) {
+ uiState.value.wheelNum
+ } else null,
+ hasSuccess = eleWorkOrderBean.isSuccess,
+ recipientSignPath = eleWorkOrderBean.serverAcceptCarSignPath,
+ waitstaffSignPath = eleWorkOrderBean.serverServicePeopleSignPath,
+ eleState = 3,
+ userOrderId = getCurrentOrder()?.userOrderId)
+ insertOfflineTask(offlineUpdateTaskBean)
+ })
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(orderWorkStatus = 3))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(getCurrentOrder()?.taskState),
+ orderInfo = getCurrentOrder()))
+ }
+
+ private fun upload() {
+ val eleWorkOrderBean = uiState.value.eleWorkOrderBean
+ if (eleWorkOrderBean == null) {
+ ToastUtils.showLong("数据获取失败,请返回首页重试!")
+ return
+ }
+
+ if (getCurrentOrder()?.flowType != 2 && eleWorkOrderBean.isSuccess == null) {
+ showTipDialog("请选择服务是否成功!")
+ return
+ }
+
+ if (uiState.value.isAddSmallWheel == true && uiState.value.wheelNum == null || uiState.value.wheelNum == 0) {
+ showTipDialog("请输入辅助轮个数")
+ return
+ }
+
+ if (uiState.value.isAddSmallWheel == true && uiState.value.wheelNum != null && uiState.value.wheelNum !! > 4) {
+ showTipDialog("辅助轮个数不能超过4个")
+ return
+ }
+
+ if (eleWorkOrderBean.localAcceptCarSignPath.isNullOrBlank() || eleWorkOrderBean.localServicePeopleSignPath.isNullOrBlank()) {
+ showTipDialog("请先上传签名!")
+ return
+ }
+
+ if (eleWorkOrderBean.serverAcceptCarSignPath.isNullOrBlank() || eleWorkOrderBean.serverServicePeopleSignPath.isNullOrBlank()) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ return
+ }
+
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ uploadOffline()
+ return
+ }
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = {
+ LoadingManager.hideLoading()
+ val saveEleOrderRequest = SaveEleOrderRequest(state = 3,
+ userOrderId = GlobalData.currentOrder?.userOrderId,
+ taskOrderId = GlobalData.currentOrder?.taskId,
+ lat = it.latitude,
+ lng = it.longitude,
+ offlineMode = 0,
+ isFinish = true,
+ tyreNumber = if (uiState.value.isAddSmallWheel == true) {
+ uiState.value.wheelNum
+ } else null,
+ hasSuccess = eleWorkOrderBean.isSuccess,
+ recipientSignPath = eleWorkOrderBean.serverAcceptCarSignPath,
+ waitstaffSignPath = eleWorkOrderBean.serverServicePeopleSignPath)
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().saveElectronOrder(saveEleOrderRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : String?) {
+ LoadingManager.hideLoading()
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(orderWorkStatus = 3))
+ updateState(uiState.value.copy(goNextPage = UpdateTaskBean(getCurrentOrder()?.taskState),
+ orderInfo = getCurrentOrder()))
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ LoadingManager.hideLoading()
+ if (code == Const.NetWorkException) {
+ updateState(uiState.value.copy(showOfflineDialog = true))
+ }
+ LogUtil.print("eleSign upload failed", msg ?: "")
+ }
+ })
+
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun init() {
+ LoadingManager.showLoading()
+ CommonMethod.queryElectronOrder(getCurrentOrder(), success = {
+ LoadingManager.hideLoading()
+ val photoList = RoomHelper.db?.eleCarDamagePhotoDao()
+ ?.getEleCarDamagePhotos(getCurrentOrder()?.taskId ?: 0)
+ updateState(uiState.value.copy(eleWorkOrderBean = it,
+ damagePhoto = photoList,
+ orderInfo = getCurrentOrder()))
+
+ LogUtil.print("电子表单更新车辆损伤照片", "eleWorkOrderBean==${photoList.toJson()}")
+
+ if (! it.localServicePeopleSignPath.isNullOrBlank() || ! it.serverServicePeopleSignPath.isNullOrBlank()) {
+ return@queryElectronOrder
+ }
+
+ if (FileUtils.isFileExists(File(AppFileManager.getDriverSignPath(context = ActivityUtils.getTopActivity())))) {
+ updateServiceSignature(AppFileManager.getDriverSignPath(context = ActivityUtils.getTopActivity()))
+ }
+
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort("数据加载异常,请返回重试!")
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ data object Upload : Action()
+ data object UploadOffline : Action()
+ data class UpdateServiceSuccessState(val state : Int) : Action()
+ data class IsChangeBattery(val isChange : Boolean) : Action()
+ data class UpdateAcceptSignature(val path : String) : Action()
+ data class UploadServiceSignature(val path : String) : Action()
+ }
+
+ data class UiState(
+ val orderInfo : OrderInfo? = null,
+ val eleWorkOrderBean : EleWorkOrderBean? = null,
+ val goNextPage : UpdateTaskBean? = null,
+ val isGoNextPageDialog : Boolean? = null,
+ val showCallPhoneDialog : Boolean? = null,
+ val damagePhoto : List? = null,
+ val isAddSmallWheel : Boolean? = null,
+ val wheelNum : Int? = null,
+ val showOfflineDialog : Boolean? = null,
+ val showServicePeopleSignDialog : Boolean? = null, //服务人员签名弹窗
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessScreen.kt
new file mode 100644
index 0000000..5235393
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessScreen.kt
@@ -0,0 +1,264 @@
+package com.za.ui.servicing.order_confirm
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Bitmap
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import coil.decode.GifDecoder
+import coil.request.ImageRequest
+import com.blankj.utilcode.util.ToastUtils
+import com.tencent.smtt.sdk.WebChromeClient
+import com.tencent.smtt.sdk.WebSettings
+import com.tencent.smtt.sdk.WebView
+import com.tencent.smtt.sdk.WebViewClient
+import com.za.base.theme.black65
+import com.za.base.view.CommonButton
+import com.za.base.view.CommonDialog
+import com.za.base.view.HeadView
+import com.za.base.view.LoadingState
+import com.za.common.log.LogUtil
+import com.za.ext.finish
+import com.za.ext.getEleOrderH5Url
+import com.za.ext.goStatusPage
+import com.za.servicing.R
+import com.za.signature.GridPaintActivity
+import com.za.signature.config.PenConfig
+
+@Composable
+fun ConfirmH5SuccessScreen(vm : ConfirmH5SuccessVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val webView = WebView(context)
+
+ //客户签名
+ val customerSignatureLauncher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra(PenConfig.SAVE_PATH)
+ LogUtil.print("pen save Path", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ vm.dispatch(ConfirmH5SuccessVm.Action.UpdateCurrentEleWorkOrder(type = 1,
+ path = value))
+ }
+ }
+
+ //接车人签字
+ val receivePeopleSignatureLauncher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra(PenConfig.SAVE_PATH)
+ LogUtil.print("pen save Path", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ vm.dispatch(ConfirmH5SuccessVm.Action.UpdateCurrentEleWorkOrder(type = 2,
+ path = value))
+ }
+ }
+
+
+ //服务人员签字
+ val servicePeopleSignatureLauncher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ val value = it.data?.getStringExtra(PenConfig.SAVE_PATH)
+ LogUtil.print("pen save Path", "path==$value")
+ if (value.isNullOrBlank()) {
+ ToastUtils.showLong("照片路径为空,请重新拍摄!")
+ return@rememberLauncherForActivityResult
+ }
+ vm.dispatch(ConfirmH5SuccessVm.Action.UpdateCurrentEleWorkOrder(type = 3,
+ path = value))
+ }
+ }
+
+
+ BackHandler(enabled = true) {}
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(ConfirmH5SuccessVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage == true) {
+ uiState.value.orderInfo?.goStatusPage(context)
+ context.finish()
+ }
+
+ if (uiState.value.showSignDialog != null) {
+ val title = when (uiState.value.showSignDialog) {
+ 1 -> "客户签字缺失,请补充!"
+ 2 -> "接车人签字缺失,请补充!"
+ 3 -> "服务人员签字缺失,请补充!"
+ else -> "签字缺失,请补充!"
+ }
+ CommonDialog(title = title, confirmText = "去签字", cancel = {
+ vm.updateState(uiState.value.copy(showSignDialog = null))
+ }, cancelEnable = false, confirm = {
+ vm.updateState(uiState.value.copy(showSignDialog = null))
+ val intent = Intent(context, GridPaintActivity::class.java)
+ intent.putExtra("background", android.graphics.Color.WHITE)
+ intent.putExtra("crop", true)
+ intent.putExtra("fontSize", 50) //手写字体大小
+ intent.putExtra("format", PenConfig.FORMAT_PNG)
+ intent.putExtra("lineLength", 10) //每行显示字数(超出屏幕支持横向滚动)
+ when (uiState.value.showSignDialog) {
+ 1 -> {
+ customerSignatureLauncher.launch(intent)
+ }
+
+ 2 -> {
+ receivePeopleSignatureLauncher.launch(intent)
+ }
+
+ 3 -> {
+ servicePeopleSignatureLauncher.launch(intent)
+ }
+ }
+ }, cancelText = "取消", dismiss = {
+ vm.updateState(uiState.value.copy(showSignDialog = null))
+ })
+ }
+
+ if (uiState.value.showOfflineTaskDialog == true) {
+ CommonDialog(title = "提示", content = {
+ Box {
+ Text("有正在进行的离线任务,是否仍要提交?")
+ }
+ }, confirmText = "等待离线任务上传", cancelText = "清除离线任务,后续补传", confirm = {
+ vm.updateState(uiState.value.copy(showOfflineTaskDialog = false))
+ }, cancelEnable = true, cancel = {
+ vm.dispatch(ConfirmH5SuccessVm.Action.ClearOfflineTask)
+ vm.updateState(uiState.value.copy(showOfflineTaskDialog = false))
+ }, dismiss = {
+ vm.updateState(uiState.value.copy(showOfflineTaskDialog = false))
+ })
+ }
+
+ Scaffold(topBar = {
+ HeadView(title = "电子工单确认", isCanBack = false)
+ }) {
+ when (uiState.value.loadingState) {
+ is LoadingState.Loading -> {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .background(color = Color.White),
+ contentAlignment = Alignment.Center) {
+ AsyncImage(model = ImageRequest.Builder(LocalContext.current)
+ .data(R.drawable.gif_loading).decoderFactory(GifDecoder.Factory()).build(),
+ contentDescription = "",
+ modifier = Modifier.size(80.dp))
+ }
+ }
+
+ is LoadingState.LoadingSuccess -> {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .background(color = Color.White)
+ .verticalScroll(rememberScrollState())) {
+ AndroidView(modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(), factory = {
+ webView.webChromeClient = WebChromeClient()
+ val webSetting = webView.settings
+ webSetting.allowFileAccess = true
+ webSetting.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
+ webSetting.setSupportZoom(true)
+ webSetting.builtInZoomControls = true
+ webSetting.useWideViewPort = true
+ webSetting.loadWithOverviewMode = true
+ webSetting.setSupportMultipleWindows(false)
+ webSetting.setAppCacheEnabled(true)
+ webSetting.domStorageEnabled = true
+ webSetting.javaScriptEnabled = true
+ webSetting.setGeolocationEnabled(true)
+ webSetting.setAppCacheMaxSize(Long.MAX_VALUE)
+ webSetting.blockNetworkImage = false // 解决图片不显示
+ webSetting.mixedContentMode = 0
+ webSetting.setAppCachePath(context.getDir("appcache", 0).path)
+ webSetting.databasePath = context.getDir("databases", 0).path
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageStarted(p0 : WebView?, p1 : String?, p2 : Bitmap?) {
+ super.onPageStarted(p0, p1, p2)
+ LogUtil.print("onPageStarted", "$p1---")
+ }
+
+ override fun onReceivedError(p0 : WebView?,
+ p1 : Int,
+ p2 : String?,
+ p3 : String?) {
+ super.onReceivedError(p0, p1, p2, p3)
+ LogUtil.print("onReceivedError1", "$p2---$p3")
+ }
+
+ override fun onPageFinished(p0 : WebView?, p1 : String?) {
+ super.onPageFinished(p0, p1)
+ LogUtil.print("onPageFinished", "$p1---$p1")
+ }
+ }
+ webView
+ }, update = { webView.loadUrl(uiState.value.orderInfo?.getEleOrderH5Url()) })
+
+ CommonButton(text = "订单完成,去结算单页") {
+ vm.dispatch(ConfirmH5SuccessVm.Action.TaskFinish)
+ }
+ }
+ }
+
+ is LoadingState.LoadingFailed -> {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .clickable { vm.dispatch(ConfirmH5SuccessVm.Action.Init) }
+ .padding(it),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally) {
+ AsyncImage(model = R.drawable.sv_load_error,
+ contentDescription = "",
+ modifier = Modifier.size(120.dp))
+ Spacer(modifier = Modifier.height(10.dp))
+ Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Text(text = "加载失败,点击重试",
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ color = black65)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessVm.kt
new file mode 100644
index 0000000..42753e3
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/ConfirmH5SuccessVm.kt
@@ -0,0 +1,242 @@
+package com.za.ui.servicing.order_confirm
+
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.base.view.LoadingState
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.ElectronOrderResponse
+import com.za.bean.request.QueryEleOrderRequest
+import com.za.bean.request.SaveEleOrderRequest
+import com.za.bean.request.TaskFinishRequest
+import com.za.bean.request.TaskFinishResponse
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.CommonMethod
+import com.za.net.RetrofitHelper
+import com.za.offline.OfflineUpdateTaskBean
+import com.za.room.RoomHelper
+import com.za.service.location.ZdLocationManager
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.io.File
+
+class ConfirmH5SuccessVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.TaskFinish -> taskFinish()
+ is Action.ClearOfflineTask -> clearCurrentOrderOfflineTask()
+ is Action.UpdateCurrentEleWorkOrder->updateCurrentEleWorkOrder(action.type, action.path)
+ }
+ }
+
+ private fun init() {
+ updateState(uiState.value.copy(loadingState = LoadingState.Loading))
+ updateState(uiState.value.copy(loadingState = LoadingState.LoadingSuccess,
+ orderInfo = getCurrentOrder()))
+ }
+
+ private fun taskFinishOffline(taskFinishRequest : TaskFinishRequest) {
+ val offlineUpdateTaskBean = OfflineUpdateTaskBean(
+ taskCode = getCurrentOrder()?.taskCode,
+ taskId = getCurrentOrder()?.taskId,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ offlineMode = 1,
+ offlineType = 8,
+ offlineTitle = "任务完成",
+ updateTaskLat = taskFinishRequest.lat,
+ updateTaskLng = taskFinishRequest.lng,
+ operateTime = "${System.currentTimeMillis()}",
+ )
+ insertOfflineTask(offlineUpdateTaskBean)
+
+ val eleWorkOrderBean =
+ RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(uiState.value.orderInfo?.taskId ?: 0)
+ eleWorkOrderBean?.copy(hasCreatedEleWorkOrderPhoto = true)
+ ?.let { it1 -> updateCurrentEleWorkOrder(it1) }
+ updateState(uiState.value.copy(goNextPage = true))
+ }
+
+ private fun queryEleWorkOrder(success : () -> Unit, failed : (String?) -> Unit) {
+ LoadingManager.showLoading()
+ val queryEleOrderRequest = QueryEleOrderRequest(taskOrderId = getCurrentOrder()?.taskId,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ userOrderCode = getCurrentOrder()?.taskCode)
+ RetrofitHelper.getDefaultService().queryElectronOrder(queryEleOrderRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : ElectronOrderResponse?) {
+ LoadingManager.hideLoading()
+ if (it == null) {
+ failed("查询失败")
+ LogUtil.print("queryEleWorkOrder", "查询失败")
+ return
+ }
+
+ if (getCurrentOrder()?.flowType == Const.TUO_CHE) {
+ if (it.customerSignPath.isNullOrEmpty()) {
+ updateState(uiState.value.copy(showSignDialog = 1))
+ return
+ }
+ }
+
+ //接车人签字
+ if (it.recipientSignPath.isNullOrEmpty()) {
+ updateState(uiState.value.copy(showSignDialog = 2))
+ return
+ }
+ if (it.waitstaffSignPath.isNullOrEmpty()) {
+ updateState(uiState.value.copy(showSignDialog = 3))
+ return
+ }
+ success()
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ LoadingManager.hideLoading()
+ failed(msg)
+ }
+ })
+ }
+
+ private fun updateCurrentEleWorkOrder(type : Int, path : String) {
+ CommonMethod.uploadImage(File(path), success = {
+ var saveEleOrderRequest = SaveEleOrderRequest(taskOrderId = getCurrentOrder()?.taskId,
+ userOrderId = getCurrentOrder()?.userOrderId,
+ state = 3)
+ when (type) {
+ 1 -> {
+ saveEleOrderRequest = saveEleOrderRequest.copy(customerSignPath = it)
+ }
+
+ 2 -> {
+ saveEleOrderRequest = saveEleOrderRequest.copy(recipientSignPath = it)
+ }
+
+ 3 -> {
+ saveEleOrderRequest = saveEleOrderRequest.copy(waitstaffSignPath = it)
+ }
+ }
+ doUpdateCurrentEleWorkOrder(type, saveEleOrderRequest)
+ }, failed = {
+ ToastUtils.showLong(it)
+ })
+ }
+
+ private fun doUpdateCurrentEleWorkOrder(type : Int, saveEleOrderRequest : SaveEleOrderRequest) {
+ RetrofitHelper.getDefaultService().saveElectronOrder(saveEleOrderRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : String?) {
+ val eleWorkOrderBean = getCurrentOrderEleWorkOrder() ?: return
+ when (type) {
+ 1 -> {
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(serverCustomSignPath = saveEleOrderRequest.customerSignPath,
+ orderWorkStatus = 3))
+ }
+
+ 2 -> {
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(serverAcceptCarSignPath = saveEleOrderRequest.recipientSignPath,
+ orderWorkStatus = 3))
+ }
+
+ 3 -> {
+ updateCurrentEleWorkOrder(eleWorkOrderBean.copy(
+ serverServicePeopleSignPath = saveEleOrderRequest.waitstaffSignPath,
+ orderWorkStatus = 3))
+ }
+ }
+ LogUtil.print("doUpdateCurrentEleWorkOrder success",
+ "saveEleOrderRequest==${saveEleOrderRequest.toJson()}")
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ ToastUtils.showLong(msg)
+ LogUtil.print("doUpdateCurrentEleWorkOrder", "code==$code msg==$msg")
+ }
+ })
+ }
+
+ private fun taskFinish() { //如果有离线任务,先提示用户离线任务信息
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ updateState(uiState.value.copy(showOfflineTaskDialog = true))
+ return
+ }
+
+ queryEleWorkOrder(success = {
+ doTaskFinish()
+ }, failed = {
+ ToastUtils.showLong(it)
+ })
+ }
+
+ private fun doTaskFinish() {
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = { it ->
+ LoadingManager.hideLoading()
+ val taskFinishRequest = TaskFinishRequest(uiState.value.orderInfo?.userOrderId,
+ it.latitude,
+ it.longitude,
+ System.currentTimeMillis())
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().taskFinish(taskFinishRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : TaskFinishResponse?) {
+ LoadingManager.hideLoading()
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()
+ ?.getEleWorkOrder(uiState.value.orderInfo?.taskId ?: 0)
+ eleWorkOrderBean?.copy(hasCreatedEleWorkOrderPhoto = true)
+ ?.let { it1 -> updateCurrentEleWorkOrder(it1) }
+ updateState(uiState.value.copy(goNextPage = true))
+ LogUtil.print("任务完成,进入历史", "taskFinishRequest==$taskFinishRequest")
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ LoadingManager.hideLoading()
+ if (msg?.contains("进入历史") == true) {
+ RoomHelper.db?.eleWorkOrderDao()
+ ?.getEleWorkOrder(getCurrentOrder()?.taskId ?: 0)?.let {
+ RoomHelper.db?.eleWorkOrderDao()
+ ?.update(it.copy(hasCreatedEleWorkOrderPhoto = true))
+ }
+ updateState(uiState.value.copy(goNextPage = true))
+ return
+ }
+ updateState(uiState.value.copy(loadingState = LoadingState.LoadingFailed))
+ LogUtil.print("任务失败", "code==$code msg==$msg")
+ }
+ })
+ }, failed = {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(loadingState = LoadingState.LoadingFailed))
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ data object TaskFinish : Action()
+ data object ClearOfflineTask : Action()
+ data class UpdateCurrentEleWorkOrder(val type : Int, val path : String) : Action()
+ }
+
+ data class UiState(
+ val loadingState : LoadingState = LoadingState.Loading,
+ val orderInfo : OrderInfo? = null,
+ val goNextPage : Boolean? = null,
+ val showOfflineTaskDialog : Boolean? = null,
+ val showSignDialog : Int? = null, //1 客户签名 2 接车人签名 3 服务人员签名
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/EleSuccessScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/EleSuccessScreen.kt
new file mode 100644
index 0000000..55d7985
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/EleSuccessScreen.kt
@@ -0,0 +1,9 @@
+package com.za.ui.servicing.order_confirm
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun EleSuccessScreen(modifier: Modifier = Modifier) {
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmActivity.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmActivity.kt
new file mode 100644
index 0000000..c1dcdd7
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmActivity.kt
@@ -0,0 +1,76 @@
+package com.za.ui.servicing.order_confirm
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.BaseActivity
+import com.za.base.view.CommonDialog
+import com.za.base.view.LoadingManager
+import com.za.ui.servicing.order_confirm.input_money.InputMoneyActivity
+import com.za.ui.servicing.order_confirm.real_order_confirm.RealOrderConfirmActivity
+import com.za.ui.servicing.order_confirm.receive_money.ReceiveMoneyActivity
+
+class OrderConfirmActivity : BaseActivity() {
+
+ @Composable
+ override fun ContentView() {
+ OrderConfirmInitScreen()
+ }
+}
+
+@Composable
+fun OrderConfirmInitScreen(vm: OrderConfirmInitVm = viewModel()) {
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(OrderConfirmInitVm.Action.Init)
+ }
+
+ if (uiState.value.showNoNeedPayDialog == true) {
+ CommonDialog(title = "是否需要收款?", confirmText = "去收款", cancelText = "无需收款",
+ confirm = {
+ vm.updateState(uiState.value.copy(showNoNeedPayDialog = false))
+ InputMoneyActivity.goInputMoney(context, userOrderId = uiState.value.orderInfo?.userOrderId
+ ?: 0, taskId = uiState.value.orderInfo?.taskId ?: 0)
+ },
+ cancel = {
+ vm.updateState(uiState.value.copy(showNoNeedPayDialog = false, orderConfirmState = OrderConfirmInitVm.OrderConfirmState.OrderConfirm))
+ },
+ dismiss = {
+ vm.updateState(uiState.value.copy(showNoNeedPayDialog = false, orderConfirmState = OrderConfirmInitVm.OrderConfirmState.OrderConfirm))
+ })
+ }
+
+ when (uiState.value.orderConfirmState) {
+ is OrderConfirmInitVm.OrderConfirmState.ChangeBattery -> ChangeBatteryScreen()
+ is OrderConfirmInitVm.OrderConfirmState.ConfirmEle -> ConfirmEleScreen()
+ is OrderConfirmInitVm.OrderConfirmState.ConfirmH5Success -> ConfirmH5SuccessScreen()
+ is OrderConfirmInitVm.OrderConfirmState.Init -> {
+ Box {
+ LoadingManager.showLoading()
+ }
+ }
+
+ is OrderConfirmInitVm.OrderConfirmState.Failed -> {
+ Box(modifier = Modifier.clickable {
+ vm.dispatch(OrderConfirmInitVm.Action.Init)
+ }) {
+ Text("点击重试")
+ }
+ }
+
+ is OrderConfirmInitVm.OrderConfirmState.PaymentInfo -> {
+ ReceiveMoneyActivity.goReceiveMoney(context, userOrderId = uiState.value.orderInfo?.userOrderId
+ ?: 0, taskId = uiState.value.orderInfo?.taskId ?: 0)
+ }
+
+ else -> RealOrderConfirmActivity.goRealOrderConfirm(context)
+ }
+
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmInitVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmInitVm.kt
new file mode 100644
index 0000000..ffa2b61
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/OrderConfirmInitVm.kt
@@ -0,0 +1,126 @@
+package com.za.ui.servicing.order_confirm
+
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.PaymentInfoBean
+import com.za.bean.db.ele.EleWorkOrderBean
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.request.PaymentInfoRequest
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.log.LogUtil
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import com.za.room.RoomHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class OrderConfirmInitVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.UpdateState -> updateState(action.uiState)
+ }
+ }
+
+ private fun init() {
+ updateState(uiState.value.copy(orderInfo = getCurrentOrder()))
+ queryPaymentInfo(uiState.value.orderInfo)
+ }
+
+ private fun queryPaymentInfo(orderInfo: OrderInfo?) {
+ LoadingManager.showLoading()
+ val paymentInfoRequest = PaymentInfoRequest(orderInfo?.userOrderId, orderInfo?.taskId)
+ val eleWorkOrderBean = RoomHelper.db?.eleWorkOrderDao()?.getEleWorkOrder(taskId = orderInfo?.taskId
+ ?: 0)
+ RetrofitHelper.getDefaultService().paymentInfoQuery(paymentInfoRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: PaymentInfoBean?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(paymentInfoBean = it, orderInfo = orderInfo, eleWorkOrderBean = eleWorkOrderBean))
+ if (it?.isPayment == true && it.tradeState != 2) {
+ if (it.askPayment == true) {
+ updateState(uiState.value.copy(showNoNeedPayDialog = true))
+ return
+ }
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.PaymentInfo))
+ return
+ }
+
+ if (orderInfo?.electronOrderState == 0 || orderInfo?.electronOrderState == 1
+ || orderInfo?.electronOrderState == 2
+ ) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ConfirmEle))
+ return
+ }
+
+ if (eleWorkOrderBean?.changeBattery == true) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ChangeBattery))
+ return
+ }
+ if (eleWorkOrderBean?.hasCreatedEleWorkOrderPhoto == null || eleWorkOrderBean.hasCreatedEleWorkOrderPhoto != true) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ConfirmH5Success))
+ return
+ }
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.OrderConfirm))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ LogUtil.print("eleworkOrder", eleWorkOrderBean.toJson() ?: "")
+ updateState(uiState.value.copy(orderInfo = orderInfo, eleWorkOrderBean = eleWorkOrderBean))
+ if (orderInfo?.electronOrderState == 0 || orderInfo?.electronOrderState == 1 || orderInfo?.electronOrderState == 2) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ConfirmEle))
+ return
+ }
+
+ if (eleWorkOrderBean?.changeBattery == true) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ChangeBattery))
+ return
+ }
+
+ if (eleWorkOrderBean?.hasCreatedEleWorkOrderPhoto == null || eleWorkOrderBean.hasCreatedEleWorkOrderPhoto != true) {
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.ConfirmH5Success))
+ return
+ }
+
+ updateState(uiState.value.copy(orderConfirmState = OrderConfirmState.OrderConfirm))
+ LogUtil.print("queryPaymentInfo", "failed=$msg request=${paymentInfoRequest.toJson()}")
+ }
+ })
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data class UpdateState(val uiState: UiState) : Action()
+ }
+
+ data class UiState(
+ val orderConfirmState: OrderConfirmState = OrderConfirmState.Init,
+ val orderInfo: OrderInfo? = null,
+ val eleWorkOrderBean: EleWorkOrderBean? = null,
+ val goNextPage: UpdateTaskBean? = null,
+ val isGoNextPageDialog: Boolean? = null,
+ val paymentInfoBean: PaymentInfoBean? = null,
+ val showNoNeedPayDialog: Boolean? = null,
+ )
+
+ sealed class OrderConfirmState {
+ data object Init : OrderConfirmState()
+ data object Failed : OrderConfirmState()
+ data object PaymentInfo : OrderConfirmState()
+ data object ConfirmEle : OrderConfirmState()
+ data object ChangeBattery : OrderConfirmState()
+ data object ConfirmH5Success : OrderConfirmState()
+ data object OrderConfirm : OrderConfirmState()
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyActivity.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyActivity.kt
new file mode 100644
index 0000000..f52a4df
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyActivity.kt
@@ -0,0 +1,482 @@
+package com.za.ui.servicing.order_confirm.input_money
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.rounded.Phone
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseActivity
+import com.za.base.theme.black90
+import com.za.base.view.CommonDialog
+import com.za.base.view.HeadView
+import com.za.base.view.LoadError
+import com.za.common.GlobalData
+import com.za.ext.finish
+import com.za.ext.goNextPage
+import com.za.ext.noDoubleClick
+import com.za.ui.servicing.order_confirm.receive_money.backgroundColor
+import kotlinx.coroutines.delay
+
+val primaryColor = Color(0xFF3B82F6)
+val gradientColors = listOf(Color(0xFF3B82F6), Color(0xFF60A5FA))
+
+class InputMoneyActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ InputMoneyScreen(userOrderId = intent.getIntExtra("userOrderId", 0), taskId = intent.getIntExtra("taskId", 0))
+ }
+
+ companion object {
+ fun goInputMoney(context: Context, userOrderId: Int, taskId: Int) {
+ context.startActivity(Intent(context, InputMoneyActivity::class.java).apply {
+ putExtra("userOrderId", userOrderId)
+ putExtra("taskId", taskId)
+ })
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewInputMoneyScreen() {
+ InputMoneyScreen(userOrderId = 1, taskId = 1)
+}
+
+@Composable
+fun InputMoneyScreen(userOrderId: Int, taskId: Int,
+ vm: InputMoneyVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ var showSuccessDialog by remember { mutableStateOf(false) }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ vm.dispatch(action = InputMoneyVm.Action.Init(userOrderId, taskId))
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ if (showSuccessDialog) {
+ SuccessDialog(
+ amount = "${uiState.value.paymentInfoBean?.amount ?: 0.00}",
+ onDismiss = {
+ showSuccessDialog = false
+ context.finish()
+ }
+ )
+ }
+
+ if (uiState.value.payState == 3) {
+ CommonDialog(title = "收款成功", message = "收款成功", cancelEnable = false,
+ confirm = {
+ goNextPage(GlobalData.currentOrder?.taskState, context)
+ context.finish()
+ }, dismiss = {
+ goNextPage(GlobalData.currentOrder?.taskState, context)
+ context.finish()
+ })
+ }
+
+ Scaffold(
+ topBar = { HeadView(title = "客户收款", onBack = { context.finish() }) },
+ bottomBar = {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ tonalElevation = 8.dp,
+ shadowElevation = 8.dp,
+ color = backgroundColor,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Button(
+ onClick = {
+ goNextPage(GlobalData.currentOrder?.taskState, context)
+ context.finish()
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp)
+ .animateContentSize(),
+ shape = RoundedCornerShape(24.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF64748B),
+ contentColor = Color.White
+ ),
+ elevation = ButtonDefaults.buttonElevation(4.dp),
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ Text("无须收款", fontSize = 12.sp, fontWeight = FontWeight.Medium)
+ }
+
+ Button(
+ onClick = {
+ if (uiState.value.receiveMoney == null || uiState.value.receiveMoney == 0f) {
+ ToastUtils.showLong("请输入收款金额")
+ return@Button
+ }
+ if (uiState.value.isOnSite == 2 && uiState.value.userPhone.isNullOrEmpty()) {
+ ToastUtils.showLong("请输入客户手机号")
+ return@Button
+ }
+ vm.dispatch(InputMoneyVm.Action.Save)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ .weight(1f),
+ shape = RoundedCornerShape(24.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = primaryColor
+ ),
+ elevation = ButtonDefaults.buttonElevation(4.dp)
+ ) {
+ Text("发送短信链接".takeIf { uiState.value.isOnSite != 1 }
+ ?: "生成收款二维码", fontSize = 12.sp, fontWeight = FontWeight.Medium)
+ }
+ }
+ }
+ },
+ containerColor = backgroundColor
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp)
+ ) {
+ Column(modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(12.dp))
+ Text("收款金额",
+ color = Color.Gray,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+
+ OutlinedTextField(
+ value = uiState.value.receiveMoneyText ?: "",
+ onValueChange = { newValue ->
+ vm.updateState(uiState.value.copy(receiveMoney = newValue.toFloatOrNull(), receiveMoneyText = newValue))
+ },
+ modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
+ prefix = { Text("¥") },
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = primaryColor,
+ unfocusedBorderColor = Color.Gray.copy(alpha = 0.5f),
+ focusedLabelColor = primaryColor,
+ unfocusedLabelColor = Color.Gray
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.height(15.dp))
+ // Action Buttons
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = if (uiState.value.isOnSite == 1) {
+ Modifier
+ .weight(1f)
+ .background(
+ brush = Brush.linearGradient(gradientColors),
+ shape = RoundedCornerShape(12.dp)
+ )
+ .padding(vertical = 16.dp)
+ } else {
+ Modifier
+ .weight(1f)
+ .noDoubleClick { vm.dispatch(InputMoneyVm.Action.ChangeOnSite(1)) }
+ .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(12.dp))
+ .padding(vertical = 16.dp)
+ },
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (uiState.value.isOnSite == 1) {
+ Icon(imageVector = Icons.Default.LocationOn, contentDescription = null, tint = Color.White)
+ Text(text = "在现场", color = Color.White)
+ } else {
+ Icon(imageVector = Icons.Default.LocationOn, contentDescription = null, tint = Color.Gray)
+ Text(text = "在现场", color = Color.Gray)
+ }
+ }
+
+ Row(
+ modifier = if (uiState.value.isOnSite == 2) {
+ Modifier
+ .weight(1f)
+ .background(color = primaryColor, shape = RoundedCornerShape(3.dp))
+ .padding(vertical = 15.dp)
+ } else {
+ Modifier
+ .weight(1f)
+ .noDoubleClick {
+ vm.dispatch(InputMoneyVm.Action.ChangeOnSite(2))
+ }
+ .border(1.dp, color = Color.Gray, shape = RoundedCornerShape(3.dp))
+ .padding(vertical = 15.dp)
+ },
+ horizontalArrangement = Arrangement.Center
+ ) {
+ if (uiState.value.isOnSite == 2) {
+ Icon(imageVector = Icons.Default.Notifications, contentDescription = null, tint = Color.White)
+ Text(text = "不在现场", color = Color.White)
+ } else {
+ Icon(imageVector = Icons.Default.Notifications, contentDescription = null, tint = Color.Gray)
+ Text(text = "不在现场", color = Color.Gray)
+ }
+ }
+ }
+
+ AnimatedVisibility(visible = uiState.value.isOnSite == 1) {
+ QRCodeSection(uiState.value.qrCode
+ ?: "", createPayment = { vm.dispatch(InputMoneyVm.Action.CreatePaymentInfo) })
+ }
+
+ AnimatedVisibility(visible = uiState.value.isOnSite == 2) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp, vertical = 15.dp)) {
+ OutlinedTextField(
+ value = uiState.value.userPhone ?: "",
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone),
+ onValueChange = { vm.updateState(uiState.value.copy(userPhone = it)) },
+ label = { Text("请输入客户手机号") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Phone,
+ contentDescription = null,
+ tint = primaryColor
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = primaryColor,
+ unfocusedBorderColor = Color.Gray,
+ focusedLabelColor = primaryColor,
+ unfocusedLabelColor = Color.Gray),
+ singleLine = true
+ )
+ }
+ }
+
+ if (uiState.value.payState == 1) {
+ Column {
+ Spacer(modifier = Modifier.height(15.dp))
+ CountdownRing { vm.updateState(uiState.value.copy(payState = 4, qrCode = null, qrCodeOutTime = true)) }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun QRCodeSection(qrCode: String? = null, isOutTime: Boolean? = null, createPayment: () -> Unit = {}) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (!qrCode.isNullOrEmpty()) {
+ Surface(modifier = Modifier
+ .size(200.dp)
+ .border(2.dp, primaryColor.copy(alpha = 0.1f), RoundedCornerShape(16.dp)),
+ color = backgroundColor,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ AsyncImage(
+ model = qrCode,
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(0.8f)
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ "请顾客扫码支付",
+ color = Color.Gray,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium
+ )
+ }
+
+ if (isOutTime == true) {
+ LoadError(message = "收款码失效", onRetry = { createPayment() })
+ }
+
+ }
+}
+
+@Composable
+private fun CountdownRing(outTime: () -> Unit) {
+ var countdown by remember { mutableIntStateOf(5 * 60) }
+ val progress by animateFloatAsState(
+ targetValue = (countdown / 5 * 60) * 360f,
+ animationSpec = remember { androidx.compose.animation.core.tween(1000) }
+ )
+
+ LaunchedEffect(Unit) {
+ while (countdown > 0) {
+ delay(1000)
+ countdown--
+ if (countdown == 0) {
+ outTime()
+ }
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier.size(50.dp), contentAlignment = Alignment.Center) {
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ drawArc(
+ brush = Brush.sweepGradient(
+ 0f to primaryColor,
+ 1f to Color.LightGray
+ ),
+ startAngle = -90f,
+ sweepAngle = progress,
+ useCenter = false,
+ style = Stroke(width = 8.dp.toPx(), cap = StrokeCap.Round)
+ )
+ }
+ Surface(
+ color = Color.White,
+ shape = CircleShape,
+ modifier = Modifier.size(50.dp)) {}
+ Text(
+ text = countdown.toString(),
+ fontSize = 20.sp,
+ color = black90,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+}
+
+@Composable
+private fun SuccessDialog(amount: String, onDismiss: () -> Unit) {
+ Dialog(onDismissRequest = onDismiss) {
+ Surface(
+ shape = RoundedCornerShape(16.dp),
+ color = Color.White
+ ) {
+ Column(
+ modifier = Modifier.padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(64.dp)
+ .background(primaryColor.copy(alpha = 0.1f), CircleShape)
+ .clip(CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = null,
+ tint = primaryColor,
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Text("收款成功", fontWeight = FontWeight.Medium, fontSize = 18.sp)
+ Text("已收款 ¥$amount", color = Color.Gray, fontSize = 14.sp)
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(
+ onClick = onDismiss,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("完成")
+ }
+ }
+ }
+ }
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyVm.kt
new file mode 100644
index 0000000..5375473
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/input_money/InputMoneyVm.kt
@@ -0,0 +1,222 @@
+package com.za.ui.servicing.order_confirm.input_money
+
+import androidx.lifecycle.viewModelScope
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.PaymentInfoBean
+import com.za.bean.request.CustomerPaymentCreateBean
+import com.za.bean.request.CustomerPaymentCreateRequest
+import com.za.bean.request.PaymentInfoRequest
+import com.za.bean.request.PaymentUpdateRequest
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+class InputMoneyVm : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState.asStateFlow()
+ private var timerJob: Job? = null
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init(action.userOrderId, action.taskId)
+ is Action.ChangeOnSite -> changeOnSite(action.onSite)
+ is Action.CreatePaymentInfo -> createPaymentInfo()
+ is Action.Save -> save()
+ is Action.NoNeedReceiveMoney -> noNeedReceiveMoney()
+ }
+ }
+
+ private fun init(userOrderId: Int, taskId: Int) {
+ updateState(uiState.value.copy(userOrderId = userOrderId, taskId = taskId))
+ queryPaymentInfo { startTimer() }
+ }
+
+ private fun createPaymentInfo() {
+ LoadingManager.showLoading()
+ val customerPaymentCreateRequest = if (uiState.value.isOnSite == 1) {
+ CustomerPaymentCreateRequest(userOrderId = uiState.value.userOrderId,
+ taskOrderId = uiState.value.taskId,
+ onSite = uiState.value.isOnSite,
+ orderPayDetailId = uiState.value.paymentInfoBean?.orderPayDetailId)
+ } else {
+ CustomerPaymentCreateRequest(userOrderId = uiState.value.userOrderId,
+ taskOrderId = uiState.value.taskId,
+ onSite = uiState.value.isOnSite,
+ userPhone = uiState.value.userPhone,
+ orderPayDetailId = uiState.value.paymentInfoBean?.orderPayDetailId)
+ }
+ RetrofitHelper.getDefaultService().customerPaymentCreate(customerPaymentCreateRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: CustomerPaymentCreateBean?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(qrCode = it?.payData, qrCodeOutTime = null, payState = 1))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ }
+ })
+ }
+
+ private fun changeOnSite(onSite: Int) {
+ updateState(uiState.value.copy(isOnSite = onSite, payState = null, userPhone = null, qrCode = null))
+ }
+
+ private fun queryPaymentInfo(success: (PaymentInfoBean?) -> Unit = {}) {
+ LoadingManager.showLoading()
+ val paymentInfoRequest = PaymentInfoRequest(uiState.value.userOrderId, uiState.value.taskId)
+ RetrofitHelper.getDefaultService()
+ .paymentInfoQuery(paymentInfoRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: PaymentInfoBean?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(paymentInfoBean = it, userPhone = it?.userPhone))
+ success(it)
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ }
+ })
+ }
+
+ private fun queryPaymentResult() {
+ val paymentInfoRequest = PaymentInfoRequest(uiState.value.userOrderId, uiState.value.taskId)
+ RetrofitHelper.getDefaultService()
+ .paymentInfoQuery(paymentInfoRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: PaymentInfoBean?) {
+ if (it?.tradeState == 2) {
+ stopTimer()
+ updateState(uiState.value.copy(payState = 3))
+ return
+ } else {
+ updateState(uiState.value.copy(paymentInfoBean = it))
+ }
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+
+ }
+
+ })
+ }
+
+ private fun save() {
+ val paymentUpdateRequest = PaymentUpdateRequest(
+ userOrderId = uiState.value.userOrderId, taskOrderId = uiState.value.taskId,
+ payAmount = uiState.value.receiveMoney,
+ unitPrice = null,
+ mileage = null,
+ orderPayDetailId = uiState.value.paymentInfoBean?.orderPayDetailId,
+ calculateAmount = uiState.value.receiveMoney,
+ adjustAmount = null,
+ updateRemark = null,
+ askPayment = uiState.value.paymentInfoBean?.askPayment)
+
+ LoadingManager.showLoading()
+
+ RetrofitHelper.getDefaultService().paymentAmountUpdate(paymentUpdateRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ createPaymentInfo()
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ }
+ })
+ }
+
+
+ private fun noNeedReceiveMoney() {
+ val paymentUpdateRequest = PaymentUpdateRequest(
+ userOrderId = uiState.value.userOrderId, taskOrderId = uiState.value.taskId,
+ payAmount = 0f,
+ unitPrice = null,
+ mileage = null,
+ orderPayDetailId = uiState.value.paymentInfoBean?.orderPayDetailId,
+ calculateAmount = 0f,
+ adjustAmount = null,
+ updateRemark = null,
+ askPayment = uiState.value.paymentInfoBean?.askPayment)
+
+ LoadingManager.showLoading()
+
+ RetrofitHelper.getDefaultService().paymentAmountUpdate(paymentUpdateRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(saveSuccess = true))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ }
+ })
+ }
+
+ private fun startTimer() {
+ stopTimer()
+ timerJob = viewModelScope.launch {
+ while (isActive) {
+ delay(5000)
+ queryPaymentResult()
+ }
+ }
+ timerJob?.start()
+ }
+
+ private fun stopTimer() {
+ timerJob?.cancel()
+ timerJob = null
+ }
+
+ sealed class Action {
+ data class Init(val userOrderId: Int, val taskId: Int) : Action()
+ data class ChangeOnSite(val onSite: Int) : Action()
+ data object CreatePaymentInfo : Action()
+ data object Save : Action()
+ data object NoNeedReceiveMoney : Action()
+ }
+
+ data class UiState(
+ val paymentInfoBean: PaymentInfoBean? = null,
+ val userOrderId: Int = 0,
+ val taskId: Int = 0,
+ val userPhone: String? = null,//用户手机号
+ val isOnSite: Int = 1,//是否在现场 1是 2否
+ val qrCode: String? = null,//收款二维码
+ val qrCodeOutTime: Boolean? = null,
+ val payState: Int? = null,//支付状态 1待支付 2支付成功 3 支付成功 4 二维码超时
+ val receiveMoney: Float? = null,
+ val receiveMoneyText: String? = null,
+ val saveSuccess: Boolean? = null
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyActivity.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyActivity.kt
new file mode 100644
index 0000000..9d14950
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyActivity.kt
@@ -0,0 +1,23 @@
+package com.za.ui.servicing.order_confirm.modify_money
+
+import ModifyMoneyScreen
+import android.content.Context
+import android.content.Intent
+import androidx.compose.runtime.Composable
+import com.za.base.BaseActivity
+
+class ModifyMoneyActivity : BaseActivity() {
+ @Composable
+ override fun ContentView() {
+ ModifyMoneyScreen(userOrderId = intent.getIntExtra("userOrderId", 0), taskId = intent.getIntExtra("taskId", 0))
+ }
+
+ companion object {
+ fun goModifyMoney(context: Context, userOrderId: Int, taskId: Int) {
+ context.startActivity(Intent(context, ModifyMoneyActivity::class.java).apply {
+ putExtra("userOrderId", userOrderId)
+ putExtra("taskId", taskId)
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyScreen.kt
new file mode 100644
index 0000000..1433e1d
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyScreen.kt
@@ -0,0 +1,208 @@
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.theme.headBgColor
+import com.za.base.view.HeadView
+import com.za.ext.finish
+import com.za.ui.servicing.order_confirm.modify_money.ModifyMoneyViewModel
+import com.za.ui.servicing.order_confirm.receive_money.backgroundColor
+
+
+@Composable
+fun ModifyMoneyScreen(userOrderId: Int, taskId: Int, vm: ModifyMoneyViewModel = viewModel()) {
+
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ LaunchedEffect(Unit) {
+ vm.dispatch(ModifyMoneyViewModel.Action.Init(userOrderId, taskId))
+ }
+
+ if (uiState.value.saveSuccess == true) {
+ context.finish()
+ }
+
+ Scaffold(topBar = {
+ HeadView(title = "修改金额", onBack = { context.finish() })
+ }) {
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ .padding(20.dp)) {
+ Spacer(modifier = Modifier.height(12.dp))
+ if (uiState.value.paymentInfoBean?.payItem == 1) {
+ MoneyInputField(value = "${uiState.value.unitPrice ?: "0"}",
+ onValueChange = { vm.updateState(uiState.value.copy(unitPrice = it.toFloat())) },
+ label = "单价",
+ suffix = "元/公里",
+ hint = uiState.value.paymentInfoBean?.unitPrice?.toString() ?: "0",
+ enabled = false)
+ } else {
+ MoneyInputField(value = "${uiState.value.unitPrice ?: "0"}",
+ onValueChange = { vm.updateState(uiState.value.copy(unitPrice = it.toFloat())) },
+ label = "超限单价",
+ suffix = "元/公里",
+ hint = uiState.value.paymentInfoBean?.unitPrice?.toString() ?: "0",
+ enabled = false
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ MoneyInputField(
+ value = uiState.value.mileageText ?: "",
+ onValueChange = {
+ vm.dispatch(ModifyMoneyViewModel.Action.ChangeMileage((it)))
+ },
+ label = "公里数",
+ hint = uiState.value.paymentInfoBean?.mileage?.toString() ?: "0",
+ suffix = "公里"
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ MoneyInputField(
+ value = "${uiState.value.calculateAmount ?: 0}",
+ onValueChange = { },
+ label = "计算金额",
+ suffix = "元",
+ hint = uiState.value.paymentInfoBean?.calculateAmount?.toString() ?: "0",
+ enabled = false
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ MoneyInputField(
+ value = uiState.value.adjustAmountText ?: "",
+ onValueChange = {
+ vm.dispatch(ModifyMoneyViewModel.Action.ChangeAdjustAmount(it))
+ },
+ label = "调整金额",
+ hint = uiState.value.paymentInfoBean?.adjustAmount?.toString() ?: "0",
+ suffix = "元"
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ OutlinedTextField(
+ value = uiState.value.adjustRemark,
+ onValueChange = { vm.updateState(uiState.value.copy(adjustRemark = it)) },
+ label = { Text("调整原因") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ // 替换原来的 Row 为新的底部布局
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ tonalElevation = 8.dp,
+ shadowElevation = 8.dp,
+ color = backgroundColor,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = "总金额",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Text(
+ text = "¥ ${uiState.value.totalMoney ?: 0}",
+ style = MaterialTheme.typography.headlineMedium,
+ color = headBgColor,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ Button(
+ onClick = { vm.dispatch(ModifyMoneyViewModel.Action.Save) },
+ modifier = Modifier
+ .height(48.dp)
+ .width(120.dp),
+ shape = RoundedCornerShape(24.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = headBgColor
+ )
+ ) {
+ Text(
+ "保存",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun MoneyInputField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ label: String,
+ suffix: String,
+ hint: String,
+ keyboardType: KeyboardType = KeyboardType.Decimal,
+ enabled: Boolean = true
+) {
+ val secondaryTextColor=Color(0xFF666666)
+ OutlinedTextField(
+ value = value,
+ placeholder = { Text(hint, color = secondaryTextColor) },
+ onValueChange = onValueChange,
+ label = { Text(label, color = secondaryTextColor) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ enabled = enabled,
+ keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
+ trailingIcon = { Text(suffix, modifier = Modifier.padding(end = 12.dp), color = secondaryTextColor) },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = headBgColor,
+ unfocusedBorderColor = Color(0xFFE0E0E0),
+ focusedLabelColor = headBgColor,
+ unfocusedLabelColor = secondaryTextColor,
+ cursorColor = headBgColor
+ ),
+ shape = RoundedCornerShape(8.dp)
+ )
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyViewModel.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyViewModel.kt
new file mode 100644
index 0000000..efbca94
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/modify_money/ModifyMoneyViewModel.kt
@@ -0,0 +1,153 @@
+package com.za.ui.servicing.order_confirm.modify_money
+
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.BaseVm
+import com.za.base.view.LoadingManager
+import com.za.bean.PaymentInfoBean
+import com.za.bean.request.PaymentInfoRequest
+import com.za.bean.request.PaymentUpdateRequest
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import com.za.ui.servicing.order_confirm.modify_money.ModifyMoneyViewModel.Action
+import com.za.ui.servicing.order_confirm.modify_money.ModifyMoneyViewModel.UiState
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class ModifyMoneyViewModel : BaseVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState.asStateFlow()
+
+ override fun updateState(uiState: UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action: Action) {
+ when (action) {
+ is Action.Init -> init(action.userOrderId, action.taskId)
+ is Action.ChangeMileage -> changeMileage(action.mileage)
+ is Action.ChangeAdjustAmount -> changeAdjustAmount(action.adjustAmount)
+ is Action.Save -> save()
+ }
+ }
+
+
+ private fun init(userOrderId: Int, taskId: Int) {
+ LoadingManager.showLoading()
+ val paymentInfoRequest = PaymentInfoRequest(userOrderId, taskId)
+ RetrofitHelper.getDefaultService()
+ .paymentInfoQuery(paymentInfoRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: PaymentInfoBean?) {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(paymentInfoBean = it,
+ userOrderId = userOrderId,
+ taskId = taskId,
+ unitPrice = it?.unitPrice,
+ mileage = it?.amount,
+ mileageText = "${it?.mileage ?: ""}",
+ calculateAmount = it?.calculateAmount,
+ adjustAmount = it?.adjustAmount?.toFloat(),
+ adjustAmountText = "${it?.adjustAmount ?: ""}",
+ amount = it?.amount,
+ totalMoney = it?.amount))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ }
+ })
+ }
+
+ private fun changeMileage(value: String?) {
+ val mileage = value?.toFloatOrNull() ?: 0f
+ if (uiState.value.paymentInfoBean?.payItem == 1) {
+ val calculateAmount = if (mileage <= (uiState.value.paymentInfoBean?.limitedMileage
+ ?: 0)
+ ) {
+ uiState.value.paymentInfoBean?.startPrice ?: 0f
+ } else {
+ (mileage - (uiState.value.paymentInfoBean?.limitedMileage ?: 0))
+ .times(uiState.value.unitPrice ?: 0f)
+ .plus(uiState.value.paymentInfoBean?.startPrice ?: 0)
+ }
+ updateState(uiState.value.copy(mileage = mileage, mileageText = value, calculateAmount = calculateAmount.toFloat(),
+ totalMoney = calculateAmount.toFloat().plus(uiState.value.adjustAmount ?: 0f)))
+ return
+ }
+ val calculateAmount = mileage.times(uiState.value.unitPrice ?: 0f)
+ val totalMoney = calculateAmount + (uiState.value.paymentInfoBean?.adjustAmount ?: 0)
+ updateState(uiState.value.copy(mileage = mileage, mileageText = value, calculateAmount = calculateAmount, totalMoney = totalMoney))
+ }
+
+ private fun changeAdjustAmount(value: String?) {
+ val adjustAmount = value?.toFloatOrNull() ?: 0f
+ updateState(uiState.value.copy(adjustAmountText = value, adjustAmount = adjustAmount,
+ totalMoney = uiState.value.calculateAmount?.plus(adjustAmount)))
+ }
+
+
+ private fun save() {
+ if (uiState.value.adjustRemark.isEmpty()) {
+ ToastUtils.showShort("请输入调整原因")
+ return
+ }
+
+ val paymentUpdateRequest = PaymentUpdateRequest(
+ userOrderId = uiState.value.userOrderId, taskOrderId = uiState.value.taskId,
+ payAmount = uiState.value.totalMoney,
+ unitPrice = uiState.value.unitPrice,
+ mileage = uiState.value.mileage,
+ orderPayDetailId = uiState.value.paymentInfoBean?.orderPayDetailId,
+ calculateAmount = uiState.value.calculateAmount,
+ adjustAmount = uiState.value.adjustAmount,
+ updateRemark = uiState.value.adjustRemark,
+ askPayment = uiState.value.paymentInfoBean?.askPayment)
+
+ LoadingManager.showLoading()
+
+ RetrofitHelper.getDefaultService().paymentAmountUpdate(paymentUpdateRequest)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort("修改成功")
+ updateState(uiState.value.copy(saveSuccess = true))
+ }
+
+ override fun doFailure(code: Int, msg: String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ }
+ })
+
+ }
+
+ sealed class Action {
+ data class Init(val userOrderId: Int, val taskId: Int) : Action()
+ data class ChangeMileage(val mileage: String?) : Action()
+ data class ChangeAdjustAmount(val adjustAmount: String?) : Action()
+ data object Save : Action()
+ }
+
+ data class UiState(
+ val userOrderId: Int = 0,
+ val taskId: Int = 0,
+ val paymentInfoBean: PaymentInfoBean? = null,
+ val unitPrice: Float? = null,//超限单价
+ val mileage: Float? = null,//公里数
+ val calculateAmount: Float? = null,//计算金额
+ val adjustAmount: Float? = null, //调整金额
+ val amount: Float? = null,
+ val adjustRemark: String = "",//调整原因
+ val totalMoney: Float? = null,
+
+ val mileageText: String? = null,//输入框文本展示
+ val adjustAmountText: String? = null,
+ val saveSuccess: Boolean? = null
+ )
+}
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmScreen.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmScreen.kt
new file mode 100644
index 0000000..f48d0f7
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmScreen.kt
@@ -0,0 +1,347 @@
+package com.za.ui.servicing.order_confirm.real_order_confirm
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.za.base.Const
+import com.za.base.theme.black5
+import com.za.base.theme.black65
+import com.za.base.view.CommonButton
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.common.GlobalData
+import com.za.common.util.ServicingSpeechManager
+import com.za.ext.finish
+import com.za.ui.servicing.InServicingPhotoItemView
+import com.za.ui.servicing.InServicingPhotoView
+import com.za.ui.servicing.InServicingPhotoWithoutTitleView
+import com.za.ui.servicing.view.InServicingHeadView
+
+@Composable
+fun OrderConfirmScreen(vm : OrderConfirmVm = viewModel()) {
+ val context = LocalContext.current
+ val uiState = vm.uiState.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = Unit) {
+ vm.dispatch(OrderConfirmVm.Action.Init)
+ }
+
+ if (uiState.value.goNextPage != null) {
+ context.finish()
+ }
+ Scaffold(topBar = {
+ InServicingHeadView(title = "结算信息",
+ orderInfo = uiState.value.orderInfo,
+ onBack = { context.finish() })
+ }, bottomBar = {
+ CommonButton(text = "提交结算信息") {
+ vm.dispatch(OrderConfirmVm.Action.Submit)
+ }
+ }) { it ->
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(it)) {
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp),
+ contentAlignment = Alignment.CenterStart) {
+ Text(text = "结算类型: ${uiState.value.orderInfo?.settleTypeStr ?: ""}",
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ }
+ BaseFeeView(flowType = uiState.value.orderInfo?.flowType ?: 0,
+ startPrice = uiState.value.startPrice,
+ unitPrice = uiState.value.unitKmPrice,
+ overKm = uiState.value.overKm,
+ abKm = uiState.value.abKm,
+ bcKm = uiState.value.bcKm,
+ dispatch = { vm.dispatch(it) })
+ Spacer(modifier = Modifier.height(10.dp))
+ AuxiliaryFeeView(dilemmaFee = uiState.value.dilemmaFee,
+ basementFee = uiState.value.basementFee,
+ abroadFee = uiState.value.abRoadFee,
+ bcRoadFee = uiState.value.bcRoadFee,
+ photoList = uiState.value.aBAndBCPhotos,
+ updatePhotoTemplate = { item ->
+ vm.dispatch(OrderConfirmVm.Action.UpdatePhotoTemplate(item))
+ },
+ dispatch = { vm.dispatch(it) })
+ Spacer(modifier = Modifier.height(10.dp))
+ uiState.value.photoTemplateList?.forEachIndexed { index, photoTemplateInfo ->
+ InServicingPhotoView(photoTemplateInfo = photoTemplateInfo,
+ index = index + 1,
+ success = { item ->
+ vm.dispatch(OrderConfirmVm.Action.UpdatePhotoTemplate(item))
+ })
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+ }
+ }
+}
+
+@Composable
+fun BaseFeeView(flowType : Int,
+ startPrice : Int?,
+ unitPrice : Int?,
+ overKm : Int?,
+ abKm : Int?,
+ bcKm : Int?,
+ dispatch : (OrderConfirmVm.Action) -> Unit) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(6.dp))
+ .padding(10.dp)) {
+ Text(text = "基本费用",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black)
+ HorizontalDivider(modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ color = black5)
+ if (GlobalData.driverInfo?.supplierType == Const.CHILD_COMPANY) {
+ Spacer(modifier = Modifier.height(5.dp))
+ OrderConfirmEditView(title = "起步价", value = "${startPrice ?: ""}", onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateStartPrice(it.toIntOrNull() ?: 0))
+ }, unit = "元")
+ Spacer(modifier = Modifier.height(5.dp))
+ OrderConfirmEditView(title = "超出公里数", value = "${overKm ?: ""}", onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateOverKm(it.toIntOrNull() ?: 0))
+ }, unit = "元")
+ Spacer(modifier = Modifier.height(5.dp))
+ OrderConfirmEditView(title = "每公里价格",
+ value = "${unitPrice ?: ""}",
+ onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateUnitKmPrice(it.toIntOrNull() ?: 0))
+ },
+ unit = "元")
+ } else {
+ Spacer(modifier = Modifier.height(5.dp))
+ OrderConfirmEditView(title = "出发地-事发地",
+ isMustInput = true,
+ value = "${abKm ?: ""}",
+ onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateAbKm(it.toIntOrNull() ?: 0))
+ },
+ unit = "公里")
+ if (flowType != Const.SMALL_REPAIR) {
+ Spacer(modifier = Modifier.height(5.dp))
+ OrderConfirmEditView(title = "事发-目的地",
+ isMustInput = true,
+ value = "${bcKm ?: ""}",
+ onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateBcKm(it.toIntOrNull() ?: 0))
+ },
+ unit = "公里")
+ }
+ }
+ }
+}
+
+@Composable
+fun AuxiliaryFeeView(dilemmaFee : Int?,
+ basementFee : Int?,
+ abroadFee : Int?,
+ bcRoadFee : Int?,
+ photoList : List?,
+ updatePhotoTemplate : (PhotoTemplateInfo) -> Unit,
+ dispatch : (OrderConfirmVm.Action) -> Unit) {
+ val context = LocalContext.current
+ LaunchedEffect(Unit) {
+ photoList?.find { it.photoName?.contains("好评") == true }?.let {
+ ServicingSpeechManager.playOrderGoodService(context)
+ return@LaunchedEffect
+ }
+ }
+
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(12.dp))
+ .padding(horizontal = 16.dp, vertical = 12.dp)) {
+ Text(text = "附加费用",
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color.Black,
+ modifier = Modifier.padding(bottom = 8.dp))
+ HorizontalDivider(modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ color = black5,
+ thickness = 0.5.dp)
+
+ if (GlobalData.driverInfo?.supplierType == Const.CHILD_COMPANY) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ photoList?.forEachIndexed { index, photo ->
+ Column(modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally) {
+ OutlinedTextField(value = if (photo.numbering == "P.1") "${abroadFee ?: ""}" else "${bcRoadFee ?: ""}",
+ onValueChange = { value ->
+ if (photo.numbering == "P.1") {
+ dispatch(OrderConfirmVm.Action.UpdateAbRoadFee(value.toIntOrNull()
+ ?: 0))
+ } else {
+ dispatch(OrderConfirmVm.Action.UpdateBcRoadFee(value.toIntOrNull()
+ ?: 0))
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 52.dp),
+ textStyle = TextStyle(fontSize = 15.sp,
+ color = Color.Black,
+ textAlign = TextAlign.End,
+ fontWeight = FontWeight.Normal),
+ placeholder = {
+ Text(text = if (photo.numbering == "P.1") "AB段路桥费" else "BC段路桥费",
+ fontSize = 14.sp,
+ color = black65)
+ },
+ trailingIcon = {
+ Text(text = "元",
+ fontSize = 14.sp,
+ color = black65,
+ modifier = Modifier.padding(end = 8.dp))
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ shape = RoundedCornerShape(8.dp),
+ colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = Color(
+ 0xFFE5E5E5),
+ focusedBorderColor = Color(0xFF4C81F5),
+ unfocusedContainerColor = Color(0xFFF8F8F8),
+ focusedContainerColor = Color(0xFFF8F8F8)),
+ singleLine = true)
+ Spacer(modifier = Modifier.height(8.dp))
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(color = Color(0xFFF8F8F8), shape = RoundedCornerShape(8.dp))) {
+ InServicingPhotoItemView(photoTemplateInfo = photo,
+ success = { updatePhotoTemplate(it) })
+ }
+ }
+ }
+ }
+ }
+
+ HorizontalDivider(modifier = Modifier
+ .padding(vertical = 10.dp)
+ .alpha(0.3f))
+
+ OrderConfirmEditView(title = "困境费", value = "${dilemmaFee ?: ""}", onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateDilemmaFee(it.toIntOrNull() ?: 0))
+ })
+ Spacer(modifier = Modifier.height(12.dp))
+ OrderConfirmEditView(title = "地库费用", value = "${basementFee ?: ""}", onValueChanged = {
+ dispatch(OrderConfirmVm.Action.UpdateBasementFee(it.toIntOrNull() ?: 0))
+ })
+ }
+}
+
+@Composable
+private fun OrderConfirmEditView(value : String? = null,
+ onValueChanged : (String) -> Unit,
+ title : String,
+ isMustInput : Boolean = false,
+ unit : String = "元") {
+ OutlinedTextField(value = value ?: "",
+ onValueChange = onValueChanged,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 52.dp),
+ textStyle = TextStyle(fontSize = 15.sp,
+ color = Color.Black,
+ textAlign = TextAlign.End,
+ fontWeight = FontWeight.Normal),
+ placeholder = {
+ Text(text = title, fontSize = 14.sp, color = black65)
+ },
+ trailingIcon = {
+ Text(text = unit,
+ fontSize = 14.sp,
+ color = black65,
+ modifier = Modifier.padding(end = 8.dp))
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ shape = RoundedCornerShape(8.dp),
+ colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = Color(0xFFE5E5E5),
+ focusedBorderColor = Color(0xFF4C81F5),
+ unfocusedContainerColor = Color(0xFFF8F8F8),
+ focusedContainerColor = Color(0xFFF8F8F8)),
+ singleLine = true)
+}
+
+@Composable
+private fun OrderConfirmEditContainsPhotoView(value : String? = null,
+ unit : String = "元",
+ onValueChanged : (String) -> Unit,
+ photoTemplateInfo : PhotoTemplateInfo,
+ success : (PhotoTemplateInfo) -> Unit) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = photoTemplateInfo.imageTitle ?: "",
+ color = black65,
+ fontSize = 13.sp,
+ modifier = Modifier.width(90.dp),
+ textAlign = TextAlign.Start)
+ Spacer(modifier = Modifier.width(5.dp))
+ OutlinedTextField(modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal),
+ value = value ?: "",
+ onValueChange = {
+ onValueChanged(it)
+ },
+ trailingIcon = {
+ Text(text = unit, color = black65, fontSize = 13.sp)
+ },
+ textStyle = TextStyle.Default.copy(textAlign = TextAlign.Right),
+ colors = OutlinedTextFieldDefaults.colors(focusedContainerColor = Color(0x34E6EEFF),
+ unfocusedContainerColor = Color(0xFFF8F8F8),
+ focusedBorderColor = Color(0x994C81F5),
+ unfocusedBorderColor = Color(0xFFF8F8F8)),
+ shape = RoundedCornerShape(4.dp))
+ }
+
+ Spacer(modifier = Modifier.heightIn(10.dp))
+ InServicingPhotoWithoutTitleView(photoTemplateInfo = photoTemplateInfo, success = { item ->
+ success(item)
+ })
+ }
+}
\ No newline at end of file
diff --git a/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmVm.kt b/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmVm.kt
new file mode 100644
index 0000000..105adaa
--- /dev/null
+++ b/servicing/src/main/java/com/za/ui/servicing/order_confirm/real_order_confirm/OrderConfirmVm.kt
@@ -0,0 +1,232 @@
+package com.za.ui.servicing.order_confirm.real_order_confirm
+
+
+import com.alibaba.fastjson.JSONObject
+import com.blankj.utilcode.util.ToastUtils
+import com.za.base.Const
+import com.za.base.IServicingVm
+import com.za.base.view.LoadingManager
+import com.za.bean.db.order.OrderInfo
+import com.za.bean.db.order.PhotoTemplateInfo
+import com.za.bean.request.UpdateOrderConfirmTaskRequest
+import com.za.bean.request.UpdateTaskBean
+import com.za.common.GlobalData
+import com.za.common.log.LogUtil
+import com.za.ext.getTaskNode
+import com.za.ext.toJson
+import com.za.net.BaseObserver
+import com.za.net.RetrofitHelper
+import com.za.room.RoomHelper
+import com.za.service.location.ZdLocationManager
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class OrderConfirmVm : IServicingVm() {
+ private val _uiState = MutableStateFlow(UiState())
+ val uiState get() = _uiState
+ override fun updateState(uiState : UiState) {
+ _uiState.value = uiState
+ }
+
+ override fun dispatch(action : Action) {
+ when (action) {
+ is Action.Init -> init()
+ is Action.Submit -> submit()
+ is Action.UpdateState -> updateState(action.uiState)
+ is Action.UpdatePhotoTemplate -> updateTemplate()
+ is Action.UpdateAbKm -> updateState(uiState.value.copy(abKm = action.abKm))
+ is Action.UpdateAbRoadFee -> updateState(uiState.value.copy(abRoadFee = action.abRoadFee))
+ is Action.UpdateBasementFee -> updateState(uiState.value.copy(basementFee = action.basementFee))
+ is Action.UpdateBcKm -> updateState(uiState.value.copy(bcKm = action.bcKm))
+ is Action.UpdateBcRoadFee -> updateState(uiState.value.copy(bcRoadFee = action.bcRoadFee))
+ is Action.UpdateDilemmaFee -> updateState(uiState.value.copy(dilemmaFee = action.dilemmaFee))
+ is Action.UpdateOverKm -> updateState(uiState.value.copy(overKm = action.overKm))
+ is Action.UpdateUnitKmPrice -> updateState(uiState.value.copy(unitKmPrice = action.unitKmPrice))
+ is Action.UpdateStartPrice -> updateState(uiState.value.copy(startPrice = action.startPrice))
+ }
+ }
+
+ private fun updateTemplate() {
+ getCurrentPhotoTemplate(success = { handlerPhoto(it) })
+ }
+
+ private fun submit() {
+
+ if (! getCurrentOrderOfflineTask().isNullOrEmpty()) {
+ showTipDialog("请等待离线任务提交完成!!")
+ return
+ }
+
+ val photoTemplateList = RoomHelper.db?.photoTemplateDao()
+ ?.getOrderPhotoTemplateFromTaskNode(uiState.value.orderInfo?.getTaskNode() ?: 0,
+ uiState.value.orderInfo?.userOrderId ?: 0)
+
+ if ((uiState.value.abRoadFee
+ ?: 0) > 0 && uiState.value.aBAndBCPhotos?.find { it.numbering == "P.1" }?.photoUploadPath.isNullOrBlank()
+ ) {
+ ToastUtils.showShort("请在出发段路桥费下方,拍摄发车段路桥费照片")
+ return
+ }
+ if ((uiState.value.bcRoadFee
+ ?: 0) > 0 && uiState.value.aBAndBCPhotos?.find { it.numbering == "P.2" }?.photoUploadPath.isNullOrBlank()
+ ) {
+ ToastUtils.showShort("请在背车段路桥费下方,拍摄背车段路桥费照片")
+ return
+ }
+
+ uiState.value.photoTemplateList?.forEach {
+ if (it.doHaveFilm == 1 && it.photoUploadPath.isNullOrBlank()) {
+ ToastUtils.showShort("请拍摄${it.imageTitle}照片")
+ return
+ }
+ }
+
+ if (GlobalData.driverInfo?.supplierType != Const.CHILD_COMPANY) {
+
+ // AB段不为0 BC段拖车流程不为0
+ if (uiState.value.abKm == null || uiState.value.abKm == 0) {
+ ToastUtils.showShort("出发地-事发地距离不可为0")
+ return
+ }
+ if (uiState.value.orderInfo?.flowType == Const.TUO_CHE && (uiState.value.bcKm == null || uiState.value.bcKm == 0)) {
+ ToastUtils.showShort("事发地-目的地距离不可为0")
+ return
+ }
+ }
+
+
+ val tempPhotoList = arrayListOf()
+ photoTemplateList?.forEach { item ->
+ if (item.photoType == 2 || item.photoUploadPath.isNullOrBlank()) {
+ tempPhotoList.add(null)
+ } else {
+ val jsonObject = JSONObject()
+ jsonObject["realTakePhotoTime"] = item.realTakePhotoTime ?: ""
+ jsonObject["photoSource"] = item.photoSource
+ jsonObject["path"] = item.photoUploadPath
+ jsonObject["time"] = item.time
+ jsonObject["lat"] = item.lat
+ jsonObject["lng"] = item.lng
+ jsonObject["address"] = item.address
+ tempPhotoList.add(jsonObject.toJSONString())
+ }
+ }
+
+ LoadingManager.showLoading()
+ ZdLocationManager.getSingleLocation(success = { it ->
+ LoadingManager.hideLoading()
+ val orderConfirmTaskRequest = UpdateOrderConfirmTaskRequest(type = "SETTLEMENT",
+ taskId = GlobalData.currentOrder?.taskId,
+ userId = GlobalData.driverInfo?.userId,
+ vehicleId = GlobalData.vehicleInfo?.vehicleId,
+ currentState = uiState.value.orderInfo?.taskState,
+ operateTime = System.currentTimeMillis().toString(),
+ supplierType = GlobalData.driverInfo?.supplierType,
+ settleType = GlobalData.currentOrder?.settleType,
+ carryMileage = uiState.value.bcKm,
+ startMileage = uiState.value.abKm,
+ startRoadFee = uiState.value.abRoadFee,
+ carryRoadFee = uiState.value.bcRoadFee,
+ dilemmaFee = uiState.value.dilemmaFee,
+ basementFee = uiState.value.basementFee,
+ basePrice = if (GlobalData.driverInfo?.supplierType == Const.CHILD_COMPANY) {
+ computerBaseFee()
+ } else null,
+ assistFee = if (GlobalData.driverInfo?.supplierType == Const.CHILD_COMPANY) {
+ computerAssistantsFee()
+ } else null,
+ totalFee = computerTotalFee(),
+ offlineMode = 0,
+ lat = it.latitude,
+ lng = it.longitude,
+ templatePhotoInfoList = tempPhotoList.toList())
+ LoadingManager.showLoading()
+ RetrofitHelper.getDefaultService().submitOrderConfirmTask(orderConfirmTaskRequest)
+ .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : BaseObserver() {
+ override fun doSuccess(it : UpdateTaskBean?) {
+ LoadingManager.hideLoading()
+ updateOrder(getCurrentOrder()?.copy(taskState = it?.nextState))
+ updateState(uiState.value.copy(goNextPage = it,
+ orderInfo = getCurrentOrder()))
+ }
+
+ override fun doFailure(code : Int, msg : String?) {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(msg)
+ }
+ })
+
+ }, failed = {
+ LoadingManager.hideLoading()
+ ToastUtils.showShort(it)
+ })
+ }
+
+ private fun computerBaseFee() : Double {
+ return (uiState.value.startPrice ?: 0) + (uiState.value.unitKmPrice
+ ?: 0) * (uiState.value.overKm ?: 0).toDouble()
+ }
+
+ private fun computerAssistantsFee() : Double {
+ return (uiState.value.abRoadFee ?: 0) + (uiState.value.bcRoadFee
+ ?: 0) + (uiState.value.dilemmaFee ?: 0) + (uiState.value.basementFee ?: 0).toDouble()
+ }
+
+ private fun computerTotalFee() : Double {
+ return computerBaseFee() + computerAssistantsFee()
+ }
+
+ private fun init() {
+ LoadingManager.hideLoading()
+ updateState(uiState.value.copy(orderInfo = getCurrentOrder()))
+ getCurrentPhotoTemplate(success = {
+ handlerPhoto(it)
+ })
+ }
+
+ private fun handlerPhoto(photoTemplateList : List?) {
+ LogUtil.print("handlerPhoto", photoTemplateList?.toJson() ?: "")
+ val abPhoto = photoTemplateList?.filter {
+ it.numbering == "P.1" || it.numbering == "P.2"
+ }
+ val otherPhoto = photoTemplateList?.filter {
+ it.numbering != "P.1" && it.numbering != "P.2"
+ }
+ updateState(uiState.value.copy(photoTemplateList = otherPhoto, aBAndBCPhotos = abPhoto))
+ }
+
+ sealed class Action {
+ data object Init : Action()
+ data object Submit : Action()
+ data class UpdateState(val uiState : UiState) : Action()
+ data class UpdatePhotoTemplate(val photoTemplateInfo : PhotoTemplateInfo) : Action()
+ data class UpdateStartPrice(val startPrice : Int) : Action()
+ data class UpdateUnitKmPrice(val unitKmPrice : Int) : Action()
+ data class UpdateOverKm(val overKm : Int) : Action()
+ data class UpdateAbKm(val abKm : Int) : Action()
+ data class UpdateBcKm(val bcKm : Int) : Action()
+ data class UpdateAbRoadFee(val abRoadFee : Int) : Action()
+ data class UpdateBcRoadFee(val bcRoadFee : Int) : Action()
+ data class UpdateDilemmaFee(val dilemmaFee : Int) : Action()
+ data class UpdateBasementFee(val basementFee : Int) : Action()
+ }
+
+ data class UiState(
+ val orderInfo : OrderInfo? = null,
+ val goNextPage : UpdateTaskBean? = null,
+ val isGoNextPageDialog : Boolean? = null,
+ val aBAndBCPhotos : List