Browse Source

提交人:jtm
提交内容:菜谱详情

江天明 2 years ago
parent
commit
8620739aa6
31 changed files with 1987 additions and 202 deletions
  1. 71 0
      BusinessCommon/src/main/java/com/develop/common/dialog/AmountSelectDialog.kt
  2. 33 0
      BusinessCommon/src/main/java/com/develop/common/dialog/RecipeDeleteConfirmDialog.kt
  3. 86 0
      BusinessCommon/src/main/java/com/develop/common/utils/QRCodeUtils.kt
  4. 30 0
      BusinessCommon/src/main/java/com/develop/common/utils/Resource.kt
  5. 29 0
      BusinessCommon/src/main/java/com/develop/common/widget/DownloadFailedDialog.kt
  6. 64 0
      BusinessCommon/src/main/java/com/develop/common/widget/DownloadProgressView.kt
  7. 47 0
      BusinessCommon/src/main/java/com/develop/common/widget/ShareQRCodeDialog.kt
  8. 5 0
      BusinessCommon/src/main/res/color/color_cook_tab_tint.xml
  9. 37 0
      BusinessCommon/src/main/res/layout/dialog_amount_select.xml
  10. 56 0
      BusinessCommon/src/main/res/layout/dialog_download_failure.xml
  11. 58 0
      BusinessCommon/src/main/res/layout/dialog_recipe_delete_confirm.xml
  12. 79 0
      BusinessCommon/src/main/res/layout/dialog_share_qr_code.xml
  13. 19 0
      BusinessCommon/src/main/res/layout/item_cook_portion_size_item.xml
  14. 1 0
      BusinessStep/src/main/AndroidManifest.xml
  15. 387 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailActivity.kt
  16. 29 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailDescFragment.kt
  17. 57 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailServeFragment.kt
  18. 64 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailSourceFragment.kt
  19. 56 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailToolsFragment.kt
  20. 6 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/model/CookSourceItem.kt
  21. 6 0
      BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/model/CookToolItem.kt
  22. 209 202
      BusinessStep/src/main/java/com/develop/step/viewmodel/CookDetailViewModel.kt
  23. 374 0
      BusinessStep/src/main/res/layout/activity_cook_detail.xml
  24. 21 0
      BusinessStep/src/main/res/layout/fragment_detail_cook_desc.xml
  25. 14 0
      BusinessStep/src/main/res/layout/fragment_detail_cook_method.xml
  26. 38 0
      BusinessStep/src/main/res/layout/fragment_detail_cook_serving.xml
  27. 38 0
      BusinessStep/src/main/res/layout/fragment_detail_cook_source.xml
  28. 33 0
      BusinessStep/src/main/res/layout/item_cook_source.xml
  29. 34 0
      BusinessStep/src/main/res/layout/item_cook_tool.xml
  30. 5 0
      libBase/src/main/java/com/develop/base/ext/NumberExt.kt
  31. 1 0
      libThirdPart/build.gradle

+ 71 - 0
BusinessCommon/src/main/java/com/develop/common/dialog/AmountSelectDialog.kt

@@ -0,0 +1,71 @@
+package com.develop.common.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.develop.base.ext.updateText
+import com.develop.base.mvvm.FullScreenTransparentDialog
+import com.develop.common.data_repo.db.entity.DevRecipePortionSize
+import com.develop.common.databinding.DialogAmountSelectBinding
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.setup
+import com.develop.common.R
+import com.drake.brv.utils.models
+
+class AmountSelectDialog(
+    private val data: List<DevRecipePortionSize>,
+    private val onSelect: (data: DevRecipePortionSize) -> Unit
+) : FullScreenTransparentDialog() {
+
+    private lateinit var binding: DialogAmountSelectBinding
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogAmountSelectBinding.inflate(
+            inflater, container, false
+        )
+        binding.ivCancel.setOnClickListener {
+            removeSelf()
+        }
+        binding.root.setOnClickListener {
+            removeSelf()
+        }
+        val wrappers = mutableListOf<Wrapper>()
+        data.forEach {
+            wrappers.add(Wrapper(it, false))
+        }
+
+        binding.clLayer.apply {
+            linear()
+            setup {
+                addType<Wrapper>(R.layout.item_cook_portion_size_item)
+                onBind {
+                    val tvJar = findView<TextView>(R.id.tv_jars)
+                    val model = getModel<Wrapper>()
+                    tvJar.updateText(model.data.portionSize + " JAR")
+                    tvJar.isSelected = model.selected
+                }
+                R.id.tv_jars.onClick {
+                    for (wrapper in wrappers) {
+                        wrapper.selected = false
+                    }
+                    val data = getModel<Wrapper>()
+                    data.selected = true
+                    onSelect(data.data)
+                    notifyDataSetChanged()
+                }
+            }
+        }.models = wrappers
+        return binding.root
+    }
+
+    class Wrapper(
+        val data: DevRecipePortionSize,
+        var selected: Boolean
+    )
+}

+ 33 - 0
BusinessCommon/src/main/java/com/develop/common/dialog/RecipeDeleteConfirmDialog.kt

@@ -0,0 +1,33 @@
+package com.develop.common.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.develop.base.mvvm.FullScreenTransparentDialog
+import com.develop.common.databinding.DialogRecipeDeleteConfirmBinding
+
+
+class RecipeDeleteConfirmDialog(
+    private val onConfirm: () -> Unit
+): FullScreenTransparentDialog() {
+
+    private lateinit var binding: DialogRecipeDeleteConfirmBinding
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogRecipeDeleteConfirmBinding.inflate(
+            inflater, container, false
+        )
+        binding.tvYes.setOnClickListener {
+            onConfirm()
+        }
+        binding.tvCancel.setOnClickListener {
+              removeSelf()
+        }
+        return binding.root
+    }
+}

+ 86 - 0
BusinessCommon/src/main/java/com/develop/common/utils/QRCodeUtils.kt

@@ -0,0 +1,86 @@
+package com.develop.common.utils
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.text.TextUtils
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
+import com.google.zxing.WriterException
+import com.google.zxing.qrcode.QRCodeWriter
+import java.util.*
+
+object QRCodeUtils {
+
+    /**
+     * 生成简单二维码
+     *
+     * @param content                字符串内容
+     * @param width                  二维码宽度
+     * @param height                 二维码高度
+     * @param characterSet          编码方式(一般使用UTF-8)
+     * @param errorCorrectionLevel 容错率 L:7% M:15% Q:25% H:35%
+     * @param margin                 空白边距(二维码与边框的空白区域)
+     * @param colorBlack            黑色色块
+     * @param colorWhite            白色色块
+     * @return BitMap
+     */
+    fun createQRCodeBitmap(
+        content: String,
+        width: Int,
+        height: Int,
+        characterSet: String? = "UTF-8",
+        errorCorrectionLevel: String? = "L",
+        margin: String? = "1",
+        colorBlack: Int = Color.BLACK,
+        colorWhite: Int = Color.WHITE
+    ): Bitmap? {
+        // 字符串内容判空
+        if (TextUtils.isEmpty(content)) {
+            return null
+        }
+        // 宽和高>=0
+        if (width < 0 || height < 0) {
+            return null
+        }
+        return try {
+            /** 1.设置二维码相关配置  */
+            val hints =
+                Hashtable<EncodeHintType, String?>()
+            // 字符转码格式设置
+            if (!TextUtils.isEmpty(characterSet)) {
+                hints[EncodeHintType.CHARACTER_SET] = characterSet
+            }
+            // 容错率设置
+            if (!TextUtils.isEmpty(errorCorrectionLevel)) {
+                hints[EncodeHintType.ERROR_CORRECTION] = errorCorrectionLevel
+            }
+            // 空白边距设置
+            if (!TextUtils.isEmpty(margin)) {
+                hints[EncodeHintType.MARGIN] = margin
+            }
+            /** 2.将配置参数传入到QRCodeWriter的encode方法生成BitMatrix(位矩阵)对象  */
+            val bitMatrix =
+                QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
+
+            /** 3.创建像素数组,并根据BitMatrix(位矩阵)对象为数组元素赋颜色值  */
+            val pixels = IntArray(width * height)
+            for (y in 0 until height) {
+                for (x in 0 until width) {
+                    //bitMatrix.get(x,y)方法返回true是黑色色块,false是白色色块
+                    if (bitMatrix[x, y]) {
+                        pixels[y * width + x] = colorBlack //黑色色块像素设置,可以通过传入不同的颜色实现彩色二维码,例如Color.argb(1,55,206,141)等设置不同的颜色。
+                    } else {
+                        pixels[y * width + x] = colorWhite // 白色色块像素设置
+                    }
+                }
+            }
+            /** 4.创建Bitmap对象,根据像素数组设置Bitmap每个像素点的颜色值,并返回Bitmap对象  */
+            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
+            bitmap
+        } catch (e: WriterException) {
+            e.printStackTrace()
+            null
+        }
+    }
+}

+ 30 - 0
BusinessCommon/src/main/java/com/develop/common/utils/Resource.kt

@@ -0,0 +1,30 @@
+package com.develop.common.utils
+
+data class Resource<T>(
+    val data: T?,
+    val status: Status
+) {
+
+    enum class Status {
+        LOADING,
+        SUCCESS,
+        FAILURE
+    }
+
+    companion object {
+
+        fun <T> success(data: T?): Resource<T> {
+            return Resource(data, Status.SUCCESS)
+        }
+
+        fun <T> failure(errorMsg: ErrorMsg): Resource<T> {
+            return Resource(errorMsg as T, Status.FAILURE)
+        }
+
+        fun <T> loading(): Resource<T> {
+            return Resource(null, Status.LOADING)
+        }
+    }
+}
+
+data class ErrorMsg(var code: Int, var desc: String)

+ 29 - 0
BusinessCommon/src/main/java/com/develop/common/widget/DownloadFailedDialog.kt

@@ -0,0 +1,29 @@
+package com.develop.common.widget
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.develop.base.mvvm.FullScreenTransparentDialog
+import com.develop.common.databinding.DialogDownloadFailureBinding
+
+
+class DownloadFailedDialog: FullScreenTransparentDialog() {
+
+    private lateinit var binding: DialogDownloadFailureBinding
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogDownloadFailureBinding.inflate(
+            inflater, container, false
+        )
+        binding.tvOk.setOnClickListener {
+              removeSelf()
+        }
+        return binding.root
+    }
+
+}

+ 64 - 0
BusinessCommon/src/main/java/com/develop/common/widget/DownloadProgressView.kt

@@ -0,0 +1,64 @@
+package com.develop.common.widget
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+
+class DownloadProgressView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
+    private var defaultColor: Int = Color.RED
+    private var progressColor: Int = Color.RED
+    private var radius: Float = 10f
+    private var progress: Float = 1.0f
+
+    init {
+        defaultColor = Color.parseColor("#AAAAAA")
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        radius = h / 2f
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+        paint.color = defaultColor
+        canvas.drawRoundRect(
+            0f, 0f,
+            width.toFloat(), height.toFloat(),
+            radius, radius, paint
+        )
+        if (progress != 1.0f) {
+            canvas.clipRect(0, 0, (width * progress).toInt(), height)
+        }
+        paint.color = progressColor
+        canvas.drawRoundRect(
+            0f, 0f,
+            width.toFloat(), height.toFloat(),
+            radius, radius, paint
+        )
+    }
+
+    fun setProgress(progress: Float) {
+        var setting = progress
+        if (setting < 0) {
+            setting = 0f
+        }
+        if (setting > 1.0f) {
+            setting = 1.0f
+        }
+        this.progress = setting
+        invalidate()
+    }
+
+    fun setButtonColor(color: Int) {
+        progressColor = color
+        invalidate()
+    }
+}

+ 47 - 0
BusinessCommon/src/main/java/com/develop/common/widget/ShareQRCodeDialog.kt

@@ -0,0 +1,47 @@
+package com.develop.common.widget
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import com.develop.base.ext.setGone
+import com.develop.base.mvvm.FullScreenTransparentDialog
+import com.develop.base.util.ThreadUtils
+import com.develop.common.databinding.DialogShareQrCodeBinding
+import com.develop.common.utils.QRCodeUtils
+
+class ShareQRCodeDialog(private val recipeNum: String): FullScreenTransparentDialog() {
+
+    private lateinit var binding: DialogShareQrCodeBinding
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = DialogShareQrCodeBinding.inflate(inflater, container, false)
+        binding.ivQrCode.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+            override fun onPreDraw(): Boolean {
+                binding.ivQrCode.viewTreeObserver.removeOnPreDrawListener(this)
+                loadQRCode(binding.ivQrCode.width, binding.ivQrCode.height)
+                return false
+            }
+        })
+        binding.ivCancel.setOnClickListener {
+              removeSelf()
+        }
+        return binding.root
+    }
+
+    private fun loadQRCode(width: Int, height: Int) {
+        ThreadUtils.runOnWorkThread({
+            val bitmap = QRCodeUtils.createQRCodeBitmap(recipeNum, width, height)
+            binding.ivQrCode.post {
+                binding.loading.setGone()
+                binding.ivQrCode.setImageBitmap(bitmap)
+            }
+        })
+    }
+}

+ 5 - 0
BusinessCommon/src/main/res/color/color_cook_tab_tint.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="#ffffff" android:state_selected="true"/>
+    <item android:color="#ffa627" android:state_selected="false"/>
+</selector>

+ 37 - 0
BusinessCommon/src/main/res/layout/dialog_amount_select.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:background="#444">
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/cl_layer"
+            android:layout_width="@dimen/convert_780px"
+            android:layout_height="wrap_content"
+            android:background="@drawable/bg_icon_page"
+            android:paddingTop="@dimen/convert_120px"
+            android:paddingBottom="@dimen/convert_182px"
+            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
+
+        <ImageView
+            android:id="@+id/iv_cancel"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:layout_below="@+id/cl_layer"
+            android:layout_centerHorizontal="true"
+            android:src="@drawable/ic_cancel"
+            android:padding="12dp"
+            android:background="@drawable/bg_icon_cancel"
+            android:layout_marginTop="-20dp"/>
+
+    </RelativeLayout>
+
+
+</FrameLayout>

+ 56 - 0
BusinessCommon/src/main/res/layout/dialog_download_failure.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:background="#666">
+
+    <LinearLayout
+        android:layout_width="@dimen/convert_810px"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical"
+        android:gravity="center_horizontal"
+        android:paddingBottom="@dimen/convert_81px"
+        android:background="@drawable/bg_download_fail">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/error"
+            android:textSize="@dimen/convert_54px"
+            android:textColor="#E60012"
+            android:includeFontPadding="false"
+            android:layout_marginTop="@dimen/convert_90px"/>
+
+        <View
+            android:id="@+id/view_download_fail"
+            android:layout_width="@dimen/convert_240px"
+            android:layout_height="@dimen/convert_167px"
+            android:layout_marginTop="@dimen/convert_86px"
+            android:background="@drawable/ic_download_failure"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/download_failed"
+            android:textSize="@dimen/convert_54px"
+            android:textColor="#6B6B6B"
+            android:includeFontPadding="false"
+            android:layout_marginTop="@dimen/convert_35px"/>
+
+        <TextView
+            android:id="@+id/tv_ok"
+            android:layout_width="@dimen/convert_270px"
+            android:layout_height="@dimen/convert_120px"
+            android:background="@drawable/bg_cook_step_ok"
+            android:text="@string/ok"
+            android:textColor="#ffffff"
+            android:gravity="center"
+            android:textSize="@dimen/convert_54px"
+            android:layout_marginTop="@dimen/convert_71px"/>
+
+    </LinearLayout>
+
+</FrameLayout>

+ 58 - 0
BusinessCommon/src/main/res/layout/dialog_recipe_delete_confirm.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    tools:background="#000">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="@dimen/convert_780px"
+        android:layout_height="@dimen/convert_510px"
+        android:layout_gravity="center"
+        android:background="@drawable/bg_white_button">
+
+        <TextView
+            android:id="@+id/tv_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="@dimen/convert_45px"
+            android:textColor="#F30101"
+            android:text="@string/are_you_sure_to_delete"
+            android:layout_marginTop="@dimen/convert_150px"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <TextView
+            android:id="@+id/tv_yes"
+            android:layout_width="@dimen/convert_270px"
+            android:layout_height="@dimen/convert_120px"
+            android:background="@drawable/bg_tare_button"
+            android:text="@string/yes"
+            android:gravity="center"
+            android:textColor="#fff"
+            android:textSize="@dimen/convert_54px"
+            android:layout_marginStart="@dimen/convert_65px"
+            android:layout_marginBottom="@dimen/convert_71px"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+        <TextView
+            android:id="@+id/tv_cancel"
+            android:layout_width="@dimen/convert_270px"
+            android:layout_height="@dimen/convert_120px"
+            android:background="@drawable/bg_white_button"
+            android:elevation="@dimen/convert_15px"
+            android:text="@string/no"
+            android:gravity="center"
+            android:textColor="#FB900C"
+            android:textSize="@dimen/convert_54px"
+            android:layout_marginEnd="@dimen/convert_65px"
+            android:layout_marginBottom="@dimen/convert_71px"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</FrameLayout>

+ 79 - 0
BusinessCommon/src/main/res/layout/dialog_share_qr_code.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:background="#444">
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center">
+
+        <RelativeLayout
+            android:id="@+id/cl_layer"
+            android:layout_width="@dimen/convert_780px"
+            android:layout_height="wrap_content"
+            android:background="@drawable/bg_icon_page"
+            android:paddingBottom="@dimen/convert_164px"
+            android:orientation="vertical"
+            android:gravity="center_horizontal">
+
+            <TextView
+                android:id="@+id/tv_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/share_with_more_people"
+                android:textSize="@dimen/convert_45px"
+                android:textColor="#F30101"
+                android:includeFontPadding="false"
+                android:layout_marginTop="@dimen/convert_89px"
+                android:layout_centerHorizontal="true"/>
+
+            <TextView
+                android:id="@+id/tv_subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/scan_qr_code_with_camera"
+                android:textSize="@dimen/convert_36px"
+                android:textColor="#6B6B6B"
+                android:includeFontPadding="false"
+                android:layout_marginTop="@dimen/convert_48px"
+                android:layout_below="@+id/tv_title"
+                android:layout_centerHorizontal="true"/>
+
+            <ImageView
+                android:id="@+id/iv_qr_code"
+                android:layout_width="@dimen/convert_300px"
+                android:layout_height="@dimen/convert_300px"
+                android:layout_centerHorizontal="true"
+                tools:src="#aaa"
+                android:scaleType="fitCenter"
+                android:layout_below="@+id/tv_subtitle"
+                android:layout_marginTop="@dimen/convert_113px"/>
+
+            <ProgressBar
+                android:id="@+id/loading"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerHorizontal="true"
+                android:indeterminateTint="#000"
+                android:layout_marginTop="@dimen/convert_200px"
+                android:layout_below="@+id/tv_subtitle"/>
+
+        </RelativeLayout>
+
+        <ImageView
+            android:id="@+id/iv_cancel"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:layout_below="@+id/cl_layer"
+            android:layout_centerHorizontal="true"
+            android:src="@drawable/ic_cancel"
+            android:padding="12dp"
+            android:background="@drawable/bg_icon_cancel"
+            android:layout_marginTop="-20dp"/>
+
+    </RelativeLayout>
+
+</FrameLayout>

+ 19 - 0
BusinessCommon/src/main/res/layout/item_cook_portion_size_item.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="@dimen/convert_16px">
+
+    <TextView
+        android:id="@+id/tv_jars"
+        android:layout_width="@dimen/convert_474px"
+        android:layout_height="@dimen/convert_90px"
+        android:text="1 JARS"
+        android:gravity="center"
+        android:background="@drawable/bg_amount_item"
+        android:textColor="@color/color_amount_text"
+        android:layout_marginTop="@dimen/convert_62px"
+        android:layout_gravity="center_horizontal"/>
+
+</FrameLayout>

+ 1 - 0
BusinessStep/src/main/AndroidManifest.xml

@@ -5,5 +5,6 @@
     <application>
         <activity android:name=".ui.FoodListActivity" />
         <activity android:name=".ui.ModesDetailActivity" />
+        <activity android:name=".ui.recipes_detail.CookDetailActivity" />
     </application>
 </manifest>

+ 387 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailActivity.kt

@@ -0,0 +1,387 @@
+package com.develop.step.ui.recipes_detail
+
+import android.annotation.SuppressLint
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import coil.load
+import com.alibaba.android.arouter.facade.annotation.Autowired
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.alibaba.android.arouter.launcher.ARouter
+import com.blankj.utilcode.util.ToastUtils
+import com.develop.base.ext.*
+import com.develop.base.util.ThreadUtils
+import com.develop.common.data_repo.FoodDataProvider
+import com.develop.common.data_repo.db.entity.UserFavoriteRecipes
+import com.develop.common.dialog.AmountSelectDialog
+import com.develop.common.dialog.RecipeDeleteConfirmDialog
+import com.develop.common.router.Screens
+import com.develop.common.tag.CURRENT_USER_ID
+import com.develop.common.ui.CommonBVMActivity
+import com.develop.common.utils.Resource
+import com.develop.common.widget.DownloadFailedDialog
+import com.develop.common.widget.ShareQRCodeDialog
+import com.develop.step.databinding.ActivityCookDetailBinding
+import com.develop.step.viewmodel.CookDetailViewModel
+import java.util.*
+import com.develop.step.R
+import com.develop.step.ui.recipes_detail.model.CookDetailInfo
+
+@Route(path = Screens.Cook.COOK_DETAIL)
+class CookDetailActivity : CommonBVMActivity<ActivityCookDetailBinding, CookDetailViewModel>() {
+    /**
+     * source=local为本地菜谱
+     * source=remote为网络菜谱
+     */
+    @JvmField
+    @Autowired(name = "source")
+    var cookSource: String? = "remote"
+
+    @JvmField
+    @Autowired(name = "number")
+    var recipeNumber: String? = null
+
+    @JvmField
+    @Autowired(name = "isLike")
+    var isLike: Boolean = false
+
+    @JvmField
+    @Autowired(name = "recipesEdition")
+    var recipesEdition: String? = "1.0"
+
+    private var hideDetail = false
+    private val hideViews = mutableListOf<View>()
+    private val allTabViews = mutableListOf<View>()
+
+
+    override fun createViewModel(): CookDetailViewModel {
+        return getViewModel(CookDetailViewModel::class.java)
+    }
+
+    override fun createViewBinding(inflater: LayoutInflater): ActivityCookDetailBinding {
+        return ActivityCookDetailBinding.inflate(inflater)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        ARouter.getInstance().inject(this)
+        initView()
+        initData()
+        initListener()
+    }
+
+    private fun initData() {
+        viewModel.queryRecipe(
+            recipeNumber ?: "",
+            cookSource == "remote",
+            recipesEdition ?: "1.0"
+        )
+        viewModel.getRecipeLiveData().observe(this) {
+            updateCookDetail(it)
+        }
+        viewModel.getErrorCodeLiveData().observe(this) {
+            if (it == CookDetailViewModel.ERR_NO_RECIPE_MATCH) {
+                ToastUtils.showShort("No match recipe found!")
+                finish()
+            }
+        }
+        viewModel.starCountLiveData.observe(this) {
+            binding.startLayout.apply {
+                setStarCount(it)
+                setCommentNumber(it.toString())
+            }
+        }
+        viewModel.getDownloadState().observe(this) {
+            updateDownloadButton(it)
+        }
+        viewModel.portionSizeLiveData.observe(this) {
+            val jarsCount =
+                (it.portionSize ?: 1).toString() + " " + getString(com.develop.common.R.string.jar)
+            binding.tvJarCount.text = jarsCount
+        }
+    }
+
+    private fun initView() {
+        if (isLike) {
+            binding.ivLike.setBackgroundResource(com.develop.common.R.drawable.ic_like)
+        } else {
+            binding.ivLike.setBackgroundResource(com.develop.common.R.drawable.ic_unlike)
+        }
+        if (cookSource == "local") {
+            binding.tvDownload.setText(com.develop.common.R.string.start_cooking)
+            binding.viewProgress.setButtonColor(Color.RED)
+            binding.viewIcon.setImageResource(com.develop.common.R.drawable.ic_detail_cook_start)
+        } else {
+            binding.tvDownload.text =
+                getString(com.develop.common.R.string.download).uppercase(Locale.ROOT)
+            binding.viewProgress.setButtonColor(Color.parseColor("#1296db"))
+            binding.viewIcon.setImageResource(com.develop.common.R.drawable.ic_detail_cook_download)
+        }
+        hideViews.let {
+            it.add(binding.viewIcon1)
+            it.add(binding.viewIcon2)
+            it.add(binding.tvFoodTime)
+            it.add(binding.tvEasy)
+            it.add(binding.startLayout)
+        }
+        allTabViews.add(binding.ivTab1)
+        allTabViews.add(binding.ivTab2)
+        allTabViews.add(binding.ivTab3)
+        allTabViews.add(binding.ivTab4)
+        selectTab(TAB_SOURCE)
+    }
+
+    private fun initListener() {
+        binding.ivTab1.setOnClickListener {
+            selectTab(TAB_SOURCE)
+        }
+        binding.ivTab2.setOnClickListener {
+            selectTab(TAB_DESC)
+        }
+        binding.ivTab3.setOnClickListener {
+            selectTab(TAB_TOOLS)
+        }
+        binding.ivTab4.setOnClickListener {
+            selectTab(TAB_SERVE)
+        }
+        binding.clStartCooking.setOnClickListener {
+            if (cookSource == "remote") {
+                if (viewModel.isDownloadSuccess()) {
+                    navigateTo(Screens.Cook.COOK_STEP2) {
+                        withString("number", recipeNumber)
+                    }
+                } else if (!viewModel.isDownloading()) {
+                    binding.tvDownload.setText(com.develop.common.R.string.downloading)
+                    viewModel.downloadRecipe()
+                }
+            } else {
+                navigateTo(Screens.Cook.COOK_STEP2) {
+                    withString("number", recipeNumber)
+                }
+            }
+        }
+
+        binding.likeLayout.setOnClickListener {
+            //收藏/取消收藏
+            if (isLike) {
+                isLike = false
+                FoodDataProvider.getUserDatabase().runInTransaction {
+                    FoodDataProvider.getUserDatabase().userInfoDao().removeFavoriteRecipe(
+                        CURRENT_USER_ID, recipeNumber ?: ""
+                    )
+                }
+                binding.ivLike.setBackgroundResource(com.develop.common.R.drawable.ic_unlike)
+            } else {
+                isLike = true
+                binding.ivLike.setBackgroundResource(com.develop.common.R.drawable.ic_like)
+                FoodDataProvider.getUserDatabase().runInTransaction {
+                    FoodDataProvider.getUserDatabase().userInfoDao().insertFavoriteRecipe(
+                        UserFavoriteRecipes(CURRENT_USER_ID, recipeNumber ?: "")
+                    )
+                }
+            }
+        }
+
+        binding.tvMoreScore.setOnClickListener {
+            navigateTo(Screens.Cook.COOK_EVALUATE) {
+                withString("recipeId", recipeNumber)
+            }
+        }
+        binding.tvMoreDelete.setOnClickListener {
+            binding.clFuncMore.setGone()
+            showDeleteRecipeDialog()
+        }
+        binding.tvMoreShare.setOnClickListener {
+            val number = viewModel.getRecipeLiveData().value?.recipe?.number
+            if (number.isNullOrEmpty()) {
+                return@setOnClickListener
+            }
+            binding.clFuncMore.setGone()
+            supportFragmentManager
+                .beginTransaction()
+                .add(ShareQRCodeDialog(number), "ShareQRCodeDialog")
+                .commitAllowingStateLoss()
+        }
+        binding.viewServing.setOnClickListener {
+            showAmountSelectDialog()
+        }
+        binding.tvServingSize.setOnClickListener {
+            showAmountSelectDialog()
+        }
+        binding.viewBack.setOnClickListener {
+            finish()
+        }
+
+        binding.viewHide.setOnClickListener {
+            hideDetail = !hideDetail
+            if (hideDetail) {
+                binding.viewHide.setBackgroundResource(com.develop.common.R.drawable.ic_detail_hide)
+            } else {
+                binding.viewHide.setBackgroundResource(com.develop.common.R.drawable.ic_detail_show)
+            }
+            for (view in hideViews) {
+                if (hideDetail) {
+                    view.visibility = View.INVISIBLE
+                } else {
+                    view.visibility = View.VISIBLE
+                }
+            }
+        }
+
+        binding.viewMore.setOnClickListener {
+            binding.clFuncMore.setVisible()
+        }
+        binding.viewMoreClose.setOnClickListener {
+            binding.clFuncMore.setGone()
+        }
+        binding.clFuncMore.setOnClickListener {
+            binding.clFuncMore.setGone()
+        }
+    }
+
+    private fun selectTab(tab: Int) {
+        for (tabView in allTabViews) {
+            tabView.isSelected = false
+        }
+        when (tab) {
+            TAB_SOURCE -> {
+                binding.ivTab1.isSelected = true
+                replaceFragmentInstance(R.id.fl_tab_content, CookDetailSourceFragment())
+            }
+            TAB_DESC -> {
+                binding.ivTab2.isSelected = true
+                replaceFragmentInstance(R.id.fl_tab_content, CookDetailDescFragment())
+            }
+            TAB_TOOLS -> {
+                binding.ivTab3.isSelected = true
+                replaceFragmentInstance(R.id.fl_tab_content, CookDetailToolsFragment())
+            }
+            TAB_SERVE -> {
+                binding.ivTab4.isSelected = true
+                replaceFragmentInstance(R.id.fl_tab_content, CookDetailServeFragment())
+            }
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    private fun updateCookDetail(detail: CookDetailInfo) {
+        val bannerImg = FoodDataProvider.getResourcePath(detail.recipe.photoPath)
+        binding.ivTopBanner.load(bannerImg)
+        binding.tvFoodName.text = detail.recipe.name
+        val timeDescBuilder = StringBuilder()
+        timeDescBuilder.append(getString(com.develop.common.R.string.preparation))
+        timeDescBuilder.append(" ")
+        if (detail.recipe.prepareHours.isPositive()) {
+            timeDescBuilder.append(detail.recipe.prepareHours)
+            timeDescBuilder.append(getString(com.develop.common.R.string.hours))
+            timeDescBuilder.append(" ")
+        }
+        if (detail.recipe.prepareMinutes.isPositive()) {
+            timeDescBuilder.append(detail.recipe.prepareMinutes)
+            timeDescBuilder.append(getString(com.develop.common.R.string.min))
+        }
+        timeDescBuilder.append("\n")
+        timeDescBuilder.append(getString(com.develop.common.R.string.ready_in))
+        if (detail.recipe.makeHours.isPositive()) {
+            timeDescBuilder.append(detail.recipe.makeHours)
+            timeDescBuilder.append(getString(com.develop.common.R.string.hours))
+            timeDescBuilder.append(" ")
+        }
+        if (detail.recipe.makeMinutes.isPositive()) {
+            timeDescBuilder.append(detail.recipe.makeMinutes)
+            timeDescBuilder.append(getString(com.develop.common.R.string.min))
+        }
+
+        binding.tvFoodTime.text = timeDescBuilder.toString()
+        binding.tvEasy.text = detail.recipe.difficultyLevel
+        binding.startLayout.setCommentNumber(detail.recipe.useNum.toString())
+        detail.recipe.score?.let { binding.startLayout.setStarCount(it.toInt()) }
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private fun updateDownloadButton(resource: Resource<Int>) {
+        binding.tvDownload.tag = resource
+        when (resource.status) {
+            Resource.Status.FAILURE -> {
+                binding.tvDownload.setVisible()
+                binding.viewIcon.setVisible()
+                binding.tvProgress.setGone()
+                binding.tvDownloadState.setGone()
+                binding.tvDownload.text =
+                    getString(com.develop.common.R.string.download).uppercase(Locale.ROOT)
+                binding.viewProgress.setProgress(1f)
+                binding.viewProgress.setButtonColor(Color.parseColor("#1296db"))
+                binding.viewIcon.setImageResource(com.develop.common.R.drawable.ic_detail_cook_download)
+                supportFragmentManager
+                    .beginTransaction()
+                    .add(DownloadFailedDialog(), "DownloadFailedDialog")
+                    .commitAllowingStateLoss()
+            }
+            Resource.Status.LOADING -> {
+                val updateProgress = "${resource.data}%"
+                binding.tvDownload.setGone()
+                binding.viewIcon.setGone()
+                binding.tvProgress.setVisible()
+                binding.tvDownloadState.setVisible()
+                binding.tvProgress.updateText(updateProgress)
+                binding.viewProgress.setProgress(resource.data!! / 100f)
+                binding.viewProgress.setButtonColor(Color.parseColor("#79C414"))
+                binding.viewIcon.setImageResource(com.develop.common.R.drawable.ic_detail_cook_download)
+            }
+            else -> {
+                binding.tvDownload.setVisible()
+                binding.viewIcon.setVisible()
+                binding.tvProgress.setGone()
+                binding.tvDownloadState.setGone()
+                binding.tvDownload.setText(com.develop.common.R.string.start_cooking)
+                binding.viewProgress.setProgress(1f)
+                binding.viewProgress.setButtonColor(Color.RED)
+                binding.viewIcon.setImageResource(com.develop.common.R.drawable.ic_detail_cook_start)
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        viewModel.getStarCount(recipeNumber ?: "")
+    }
+
+    private fun showAmountSelectDialog() {
+        viewModel.getRecipeLiveData().value?.let {
+            AmountSelectDialog(it.portionSize) { size ->
+                viewModel.portionSizeLiveData.value = size
+            }.showDialog(supportFragmentManager, "AmountSelectDialog")
+        }
+    }
+
+    private fun showDeleteRecipeDialog() {
+        val dialog = RecipeDeleteConfirmDialog {
+            ThreadUtils.runOnWorkThread({
+                val recipe = viewModel.getRecipeLiveData().value?.recipe
+                if (recipe != null) {
+                    FoodDataProvider.getDatabase().recipeDao().deleteRecipe(recipe)
+                    FoodDataProvider.deleteRecipeResource(recipe.number ?: "")
+                    FoodDataProvider.getUserDatabase().userInfoDao()
+                        .removeOnlineRecipe(
+                            CURRENT_USER_ID, recipe.number ?: ""
+                        )
+                    runOnUiThread {
+                        finish()
+                    }
+                }
+            })
+        }
+        supportFragmentManager
+            .beginTransaction()
+            .add(dialog, "RecipeDeleteConfirmDialog")
+            .commitAllowingStateLoss()
+    }
+
+    companion object {
+        private const val TAB_SOURCE = 1
+        private const val TAB_DESC = 2
+        private const val TAB_TOOLS = 3
+        private const val TAB_SERVE = 4
+    }
+}

+ 29 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailDescFragment.kt

@@ -0,0 +1,29 @@
+package com.develop.step.ui.recipes_detail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.develop.common.ui.CommonBVMFragment
+import com.develop.step.databinding.FragmentDetailCookDescBinding
+import com.develop.step.viewmodel.CookDetailViewModel
+
+class CookDetailDescFragment :
+    CommonBVMFragment<FragmentDetailCookDescBinding, CookDetailViewModel>() {
+    override fun createViewModel(): CookDetailViewModel {
+        return getViewModelOfActivity(CookDetailViewModel::class.java)
+    }
+
+    override fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): FragmentDetailCookDescBinding {
+        return FragmentDetailCookDescBinding.inflate(inflater, container, false)
+    }
+
+    override fun onPostCreateView() {
+        super.onPostCreateView()
+        viewModel.getRecipeLiveData().observe(viewLifecycleOwner) {
+            binding.tvStepDetail.text = it.recipe.introduction
+        }
+    }
+}

+ 57 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailServeFragment.kt

@@ -0,0 +1,57 @@
+package com.develop.step.ui.recipes_detail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.develop.base.ext.updateText
+import com.develop.common.ui.CommonBVMFragment
+import com.develop.step.R
+import com.develop.step.databinding.FragmentDetailCookServingBinding
+import com.develop.step.ui.recipes_detail.model.CookSourceItem
+import com.develop.step.viewmodel.CookDetailViewModel
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.models
+import com.drake.brv.utils.setup
+
+class CookDetailServeFragment :
+    CommonBVMFragment<FragmentDetailCookServingBinding, CookDetailViewModel>() {
+    override fun createViewModel(): CookDetailViewModel {
+        return getViewModelOfActivity(CookDetailViewModel::class.java)
+    }
+
+    override fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): FragmentDetailCookServingBinding {
+        return FragmentDetailCookServingBinding.inflate(inflater, container, false)
+    }
+
+    override fun onPostCreateView() {
+        super.onPostCreateView()
+        binding.rvList.apply {
+            linear()
+            setup {
+                addType<CookSourceItem>(R.layout.item_cook_source)
+                onBind {
+                    val model = getModel<CookSourceItem>()
+                    findView<TextView>(R.id.tv_source_name).updateText(model.name)
+                    findView<TextView>(R.id.tv_source_amount).updateText(model.amount)
+                }
+            }
+        }
+        viewModel.getRecipeLiveData().observe(viewLifecycleOwner) {
+            val sourceItems = mutableListOf<CookSourceItem>()
+            for (nutrition in it.nutrition) {
+                sourceItems.add(
+                    CookSourceItem(
+                        nutrition.nutritionName ?: "",
+                        "${nutrition.amount} ${nutrition.unit}"
+                    )
+                )
+            }
+            binding.rvList.models = sourceItems
+        }
+    }
+}

+ 64 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailSourceFragment.kt

@@ -0,0 +1,64 @@
+package com.develop.step.ui.recipes_detail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.develop.base.ext.updateText
+import com.develop.common.ui.CommonBVMFragment
+import com.develop.step.R
+import com.develop.step.databinding.FragmentDetailCookSourceBinding
+import com.develop.step.ui.recipes_detail.model.CookSourceItem
+import com.develop.step.viewmodel.CookDetailViewModel
+import com.drake.brv.utils.linear
+import com.drake.brv.utils.models
+import com.drake.brv.utils.setup
+
+class CookDetailSourceFragment :
+    CommonBVMFragment<FragmentDetailCookSourceBinding, CookDetailViewModel>() {
+    override fun createViewModel(): CookDetailViewModel {
+        return getViewModelOfActivity(CookDetailViewModel::class.java)
+    }
+
+    override fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): FragmentDetailCookSourceBinding {
+        return FragmentDetailCookSourceBinding.inflate(inflater, container, false)
+    }
+
+    override fun onPostCreateView() {
+        super.onPostCreateView()
+        binding.rvList.apply {
+            linear()
+            setup {
+                addType<CookSourceItem>(R.layout.item_cook_source)
+                onBind {
+                    val model = getModel<CookSourceItem>()
+                    findView<TextView>(R.id.tv_source_name).updateText(model.name)
+                    findView<TextView>(R.id.tv_source_amount).updateText(model.amount)
+                }
+            }
+        }
+        viewModel.getRecipeLiveData().observe(viewLifecycleOwner) {
+            val sourceItems = mutableListOf<CookSourceItem>()
+            for (recipeFood in it.material) {
+                sourceItems.add(
+                    CookSourceItem(
+                        recipeFood.foodName ?: "",
+                        "${recipeFood.amount} ${recipeFood.unit}"
+                    )
+                )
+            }
+            val jarCount = it.portionSize.firstOrNull()?.portionSize?.toIntOrNull() ?: 1
+            val string = if (jarCount > 1) {
+                getString(com.develop.common.R.string.make_n_jars, jarCount.toString())
+            } else {
+                getString(com.develop.common.R.string.make_1_jar)
+            }
+            binding.tvStepTitle.text = string
+            binding.rvList.models = sourceItems
+        }
+    }
+}

+ 56 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/CookDetailToolsFragment.kt

@@ -0,0 +1,56 @@
+package com.develop.step.ui.recipes_detail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import coil.load
+import com.develop.base.ext.updateText
+import com.develop.common.data_repo.FoodDataProvider
+import com.develop.common.ui.CommonBVMFragment
+import com.develop.step.R
+import com.develop.step.databinding.FragmentDetailCookMethodBinding
+import com.develop.step.ui.recipes_detail.model.CookToolItem
+import com.develop.step.viewmodel.CookDetailViewModel
+import com.drake.brv.utils.grid
+import com.drake.brv.utils.models
+import com.drake.brv.utils.setup
+
+class CookDetailToolsFragment :
+    CommonBVMFragment<FragmentDetailCookMethodBinding, CookDetailViewModel>() {
+    override fun createViewModel(): CookDetailViewModel {
+        return getViewModelOfActivity(CookDetailViewModel::class.java)
+    }
+
+    override fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): FragmentDetailCookMethodBinding {
+        return FragmentDetailCookMethodBinding.inflate(inflater, container, false)
+    }
+
+    override fun onPostCreateView() {
+        super.onPostCreateView()
+        binding.rvTools.apply {
+            grid(3)
+            setup {
+                addType<CookToolItem>(R.layout.item_cook_tool)
+                onBind {
+                    val model = getModel<CookToolItem>()
+                    findView<TextView>(R.id.tv_tool_name).updateText(model.text)
+                    findView<ImageView>(R.id.iv_tool_icon).load(model.icon)
+                }
+            }
+        }
+        viewModel.getRecipeLiveData().observe(viewLifecycleOwner) {
+            val sourceItems = mutableListOf<CookToolItem>()
+            for (accessory in it.accessory) {
+                val path = FoodDataProvider.getResourcePath(accessory.photoPath)
+                sourceItems.add(CookToolItem(path, accessory.name ?: ""))
+            }
+            binding.rvTools.models = sourceItems
+        }
+    }
+}

+ 6 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/model/CookSourceItem.kt

@@ -0,0 +1,6 @@
+package com.develop.step.ui.recipes_detail.model
+
+data class CookSourceItem(
+    val name: String,
+    val amount: String
+)

+ 6 - 0
BusinessStep/src/main/java/com/develop/step/ui/recipes_detail/model/CookToolItem.kt

@@ -0,0 +1,6 @@
+package com.develop.step.ui.recipes_detail.model
+
+data class CookToolItem(
+    val icon: Any,
+    val text: String
+)

+ 209 - 202
BusinessStep/src/main/java/com/develop/step/viewmodel/CookDetailViewModel.kt

@@ -1,10 +1,12 @@
 package com.develop.step.viewmodel
+
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.scopeNetLife
 import com.blankj.utilcode.util.FileUtils
 import com.blankj.utilcode.util.ToastUtils
 import com.blankj.utilcode.util.ZipUtils
+import com.develop.base.ext.fromJson
 import com.develop.base.ext.globalApp
 import com.develop.base.mvvm.BaseViewModel
 import com.develop.base.util.FileKit
@@ -17,209 +19,214 @@ import com.develop.common.data_repo.net.Api
 import com.develop.common.data_repo.net.model.response.RecipeDataConfig
 import com.develop.common.data_repo.net.model.response.RecipeDetailResult
 import com.develop.common.tag.CURRENT_USER_ID
+import com.develop.common.utils.Resource
 import com.develop.step.ui.recipes_detail.model.CookDetailInfo
 import com.drake.net.Get
+import com.drake.net.component.Progress
+import com.drake.net.interfaces.ProgressListener
 import java.io.File
 
-//class CookDetailViewModel : BaseViewModel() {
-//    private val recipeLiveData = MutableLiveData<CookDetailInfo>()
-//    private val errorLiveData = MutableLiveData<Int>()
-//    val starCountLiveData = MutableLiveData<Int>()
-//    val downloadResource = MutableLiveData<Resource<Int>>()
-//    val portionSizeLiveData = MutableLiveData<DevRecipePortionSize>()
-//    var recipeNumber: String? = null
-//    private var mRecipeUrl: String? = null
-//
-//    //是否线上菜谱
-//    var isRemote = false
-//
-//
-//    /**
-//     * 数据源: 食谱编号
-//     * 关联数据: [DevRecipe] 食谱基本数据
-//     *          [DevRecipeAccessory] 食谱需要的配件
-//     *          [DevAccessory] 单个配件数据
-//     *          [DevRecipeFood] 食谱原料数据
-//     *          [DevRecipeNutrition] 食谱营养数据
-//     *          [DevRecipePortionSize] 食谱原料用量
-//     */
-//    fun queryRecipe(number: String, remote: Boolean, recipesEdition: String) {
-//        isRemote = remote
-//        this.recipeNumber = number
-//        if (remote) {
-//            scopeNetLife {
-//                Get<RecipeDetailResult>(Api.GET_RECIPES_DETAIL).await().apply {
-//                    mRecipeUrl = recipeUrl
-//                    val detailInfo = CookDetailInfo(
-//                        devRecipe,
-//                        devAccessorys,
-//                        devRecipeFoods,
-//                        devRecipeNutritions,
-//                        devRecipePortionSizes
-//                    )
-//                    portionSizeLiveData.postValue(detailInfo.portionSize.firstOrNull())
-//                    recipeLiveData.postValue(detailInfo)
-//                }
-//            }.catch {
-//                ToastUtils.showShort(it.message)
-//            }
-//            if (FoodDataProvider.isResourceDownload(number)) {
-//                downloadResource.value = Resource(100, Resource.Status.SUCCESS)
-//            }
-//        } else {
-//            FoodDataProvider.getDatabase().runInTransaction {
-//                val recipeDao = FoodDataProvider.getDatabase().recipeDao()
-//                val recipeBean = recipeDao.queryRecipe(number)
-//                if (recipeBean == null) {
-//                    // show toast
-//                    errorLiveData.postValue(ERR_NO_RECIPE_MATCH)
-//                    return@runInTransaction
-//                }
-//                val includeAccessory = mutableListOf<DevAccessory>()
-//                val accessoryIds = recipeDao.queryAccessoryIds(number)
-//                for (accessoryId in accessoryIds) {
-//                    val accessory = recipeDao.queryAccessory(accessoryId.accessoryNumber ?: "")
-//                    if (accessory != null) {
-//                        includeAccessory.add(accessory)
-//                    }
-//                }
-//                val includeMaterial = recipeDao.queryFood(number)
-//                val includeNutrition = recipeDao.queryNutrition(number)
-//                val includePortionSize = recipeDao.queryPortionSize(number)
-//                val detailInfo = CookDetailInfo(
-//                    recipeBean,
-//                    includeAccessory,
-//                    includeMaterial,
-//                    includeNutrition,
-//                    includePortionSize
-//                )
-//                portionSizeLiveData.postValue(detailInfo.portionSize.firstOrNull())
-//                recipeLiveData.postValue(detailInfo)
-//            }
-//        }
-//    }
-//
-//    fun getRecipeLiveData(): LiveData<CookDetailInfo> {
-//        return recipeLiveData
-//    }
-//
-//    fun getErrorCodeLiveData(): LiveData<Int> {
-//        return errorLiveData
-//    }
-//
-//    fun getStarCount(recipesId: String) {
-//        FoodDataProvider.getUserDatabase().runInTransaction {
-//            val userTag = FoodDataProvider.getUserDatabase().userInfoDao().queryUserTag(
-//                CURRENT_USER_ID, recipesId
-//            )
-//            userTag?.apply {
-//                starCountLiveData.postValue(starCount)
-//            }
-//        }
-//    }
-//
-//    fun isDownloading(): Boolean {
-//        return downloadResource.value?.status == Resource.Status.LOADING
-//    }
-//
-//    fun isDownloadSuccess(): Boolean {
-//        return downloadResource.value?.status == Resource.Status.SUCCESS
-//    }
-//
-//    fun getDownloadState(): LiveData<Resource<Int>> {
-//        return downloadResource
-//    }
-//
-//    fun downloadRecipe() {
-//        if (mRecipeUrl.isNullOrEmpty()) {
-//            downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//            return
-//        }
-//        downloadResource.value = Resource(0, Resource.Status.LOADING)
-//        val downloadDir = globalApp().externalCacheDir.toString()
-//        val downloadName = System.nanoTime().toString()
-//
-//        DownloadUtil.get().download(mRecipeUrl, downloadDir, downloadName,
-//            object : DownloadUtil.OnDownloadListener {
-//                override fun onDownloadSuccess(file: File) {
-//                    if (!file.exists()) {
-//                        ThreadUtils.runOnMainThread {
-//                            downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//                        }
-//                    } else {
-//                        prepareResource(file)
-//                    }
-//                }
-//
-//                override fun onDownloading(progress: Int) {
-//                    ThreadUtils.runOnMainThread {
-//                        downloadResource.value = Resource(progress, Resource.Status.LOADING)
-//                    }
-//                }
-//
-//                override fun onDownloadFailed(e: Exception?) {
-//                    ThreadUtils.runOnMainThread {
-//                        downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//                    }
-//                }
-//            })
-//    }
-//
-//    private fun prepareResource(file: File) {
-//        try {
-//            val dst = ZipUtils.unzipFile(file, FoodDataProvider.getExtRecipeResourceDir())
-//            FileUtils.delete(file)
-//            if (dst.isNullOrEmpty()) {
-//                ThreadUtils.runOnMainThread {
-//                    downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//                }
-//                return
-//            }
-//            val jsonFile = FoodDataProvider.getResourceConfigJsonPath(recipeNumber!!)
-//            if (!jsonFile.exists()) {
-//                ThreadUtils.runOnMainThread {
-//                    downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//                }
-//                return
-//            }
-//            val jsonContent = FileKit.readFileToString(jsonFile)
-//            val contentData = Gson().fromJson(jsonContent, RecipeDataConfig::class.java)
-//            contentData.resetAllCodes()
-//            FoodDataProvider.getDatabase().runInTransaction {
-//                FoodDataProvider.getDatabase().recipeDao().apply {
-//                    insertDevAccessorys(contentData.devAccessorys)
-//                    insertHotTags(contentData.devHotTags)
-//                    insertDevPortraits(contentData.devPortraits)
-//                    insertDevRecipeAccessorys(contentData.devRecipeAccessorys)
-//                    insertDevRecipeCategorys(contentData.devRecipeCategorys)
-//                    insertDevRecipeCookingSteps(contentData.devRecipeCookingSteps)
-//                    insertDevRecipeFoods(contentData.devRecipeFoods)
-//                    insertDevRecipeNutritions(contentData.devRecipeNutritions)
-//                    insertDevRecipePortionSizes(contentData.devRecipePortionSizes)
-//                    insertDevRecipeRelTags(contentData.devRecipeRelTags)
-//                    insertDevRecipeTags(contentData.devRecipeTags)
-//                    insertDevRecipes(contentData.devRecipes)
-//                }
-//                ThreadUtils.runOnMainThread {
-//                    downloadResource.value = Resource(100, Resource.Status.SUCCESS)
-//                }
-//            }
-//            FoodDataProvider.getUserDatabase().runInTransaction {
-//                contentData.devRecipes.getOrNull(0)?.also {
-//                    FoodDataProvider.getUserDatabase().userInfoDao().insertOnlineRecipe(
-//                        UserOnLineRecipes(CURRENT_USER_ID, it.number ?: "")
-//                    )
-//                }
-//            }
-//        } catch (e: Exception) {
-//            e.printStackTrace()
-//            ThreadUtils.runOnMainThread {
-//                downloadResource.value = Resource(0, Resource.Status.FAILURE)
-//            }
-//        }
-//    }
-//
-//    companion object {
-//        const val ERR_NO_RECIPE_MATCH = -1
-//    }
-//}
+class CookDetailViewModel : BaseViewModel() {
+    private val recipeLiveData = MutableLiveData<CookDetailInfo>()
+    private val errorLiveData = MutableLiveData<Int>()
+    val starCountLiveData = MutableLiveData<Int>()
+    val downloadResource = MutableLiveData<Resource<Int>>()
+    val portionSizeLiveData = MutableLiveData<DevRecipePortionSize>()
+    var recipeNumber: String? = null
+    private var mRecipeUrl: String? = null
+
+    //是否线上菜谱
+    var isRemote = false
+
+
+    /**
+     * 数据源: 食谱编号
+     * 关联数据: [DevRecipe] 食谱基本数据
+     *          [DevRecipeAccessory] 食谱需要的配件
+     *          [DevAccessory] 单个配件数据
+     *          [DevRecipeFood] 食谱原料数据
+     *          [DevRecipeNutrition] 食谱营养数据
+     *          [DevRecipePortionSize] 食谱原料用量
+     */
+    fun queryRecipe(number: String, remote: Boolean, recipesEdition: String) {
+        isRemote = remote
+        this.recipeNumber = number
+        if (remote) {
+            scopeNetLife {
+                Get<RecipeDetailResult>(Api.GET_RECIPES_DETAIL).await().apply {
+                    mRecipeUrl = recipeUrl
+                    val detailInfo = CookDetailInfo(
+                        devRecipe,
+                        devAccessorys,
+                        devRecipeFoods,
+                        devRecipeNutritions,
+                        devRecipePortionSizes
+                    )
+                    portionSizeLiveData.postValue(detailInfo.portionSize.firstOrNull())
+                    recipeLiveData.postValue(detailInfo)
+                }
+            }.catch {
+                ToastUtils.showShort(it.message)
+            }
+            if (FoodDataProvider.isResourceDownload(number)) {
+                downloadResource.value = Resource(100, Resource.Status.SUCCESS)
+            }
+        } else {
+            FoodDataProvider.getDatabase().runInTransaction {
+                val recipeDao = FoodDataProvider.getDatabase().recipeDao()
+                val recipeBean = recipeDao.queryRecipe(number)
+                if (recipeBean == null) {
+                    // show toast
+                    errorLiveData.postValue(ERR_NO_RECIPE_MATCH)
+                    return@runInTransaction
+                }
+                val includeAccessory = mutableListOf<DevAccessory>()
+                val accessoryIds = recipeDao.queryAccessoryIds(number)
+                for (accessoryId in accessoryIds) {
+                    val accessory = recipeDao.queryAccessory(accessoryId.accessoryNumber ?: "")
+                    if (accessory != null) {
+                        includeAccessory.add(accessory)
+                    }
+                }
+                val includeMaterial = recipeDao.queryFood(number)
+                val includeNutrition = recipeDao.queryNutrition(number)
+                val includePortionSize = recipeDao.queryPortionSize(number)
+                val detailInfo = CookDetailInfo(
+                    recipeBean,
+                    includeAccessory,
+                    includeMaterial,
+                    includeNutrition,
+                    includePortionSize
+                )
+                portionSizeLiveData.postValue(detailInfo.portionSize.firstOrNull())
+                recipeLiveData.postValue(detailInfo)
+            }
+        }
+    }
+
+    fun getRecipeLiveData(): LiveData<CookDetailInfo> {
+        return recipeLiveData
+    }
+
+    fun getErrorCodeLiveData(): LiveData<Int> {
+        return errorLiveData
+    }
+
+    fun getStarCount(recipesId: String) {
+        FoodDataProvider.getUserDatabase().runInTransaction {
+            val userTag = FoodDataProvider.getUserDatabase().userInfoDao().queryUserTag(
+                CURRENT_USER_ID, recipesId
+            )
+            userTag?.apply {
+                starCountLiveData.postValue(starCount)
+            }
+        }
+    }
+
+    fun isDownloading(): Boolean {
+        return downloadResource.value?.status == Resource.Status.LOADING
+    }
+
+    fun isDownloadSuccess(): Boolean {
+        return downloadResource.value?.status == Resource.Status.SUCCESS
+    }
+
+    fun getDownloadState(): LiveData<Resource<Int>> {
+        return downloadResource
+    }
+
+    fun downloadRecipe() {
+        if (mRecipeUrl.isNullOrEmpty()) {
+            downloadResource.value = Resource(0, Resource.Status.FAILURE)
+            return
+        }
+        downloadResource.value = Resource(0, Resource.Status.LOADING)
+        val downloadDir = globalApp().externalCacheDir.toString()
+        val downloadName = System.nanoTime().toString()
+        scopeNetLife {
+            mRecipeUrl?.apply {
+                val result = Get<File>(this) {
+                    setDownloadFileName(downloadName)
+                    setDownloadDir(downloadDir)
+                    addDownloadListener(object : ProgressListener() {
+                        override fun onProgress(p: Progress) {
+                            ThreadUtils.runOnMainThread {
+                                downloadResource.value =
+                                    Resource(p.progress(), Resource.Status.LOADING)
+                            }
+                        }
+                    })
+                }.await()
+                if (!result.exists()) {
+                    ThreadUtils.runOnMainThread {
+                        downloadResource.value = Resource(0, Resource.Status.FAILURE)
+                    }
+                } else {
+                    prepareResource(result)
+                }
+            }
+        }.catch {
+            ThreadUtils.runOnMainThread {
+                downloadResource.value = Resource(0, Resource.Status.FAILURE)
+            }
+        }
+    }
+
+    private fun prepareResource(file: File) {
+        try {
+            val dst = ZipUtils.unzipFile(file, FoodDataProvider.getExtRecipeResourceDir())
+            FileUtils.delete(file)
+            if (dst.isNullOrEmpty()) {
+                ThreadUtils.runOnMainThread {
+                    downloadResource.value = Resource(0, Resource.Status.FAILURE)
+                }
+                return
+            }
+            val jsonFile = FoodDataProvider.getResourceConfigJsonPath(recipeNumber!!)
+            if (!jsonFile.exists()) {
+                ThreadUtils.runOnMainThread {
+                    downloadResource.value = Resource(0, Resource.Status.FAILURE)
+                }
+                return
+            }
+            val jsonContent = FileKit.readFileToString(jsonFile)
+            val contentData = jsonContent.fromJson() as RecipeDataConfig
+            contentData.resetAllCodes()
+            FoodDataProvider.getDatabase().runInTransaction {
+                FoodDataProvider.getDatabase().recipeDao().apply {
+                    insertDevAccessorys(contentData.devAccessorys)
+                    insertHotTags(contentData.devHotTags)
+                    insertDevPortraits(contentData.devPortraits)
+                    insertDevRecipeAccessorys(contentData.devRecipeAccessorys)
+                    insertDevRecipeCategorys(contentData.devRecipeCategorys)
+                    insertDevRecipeCookingSteps(contentData.devRecipeCookingSteps)
+                    insertDevRecipeFoods(contentData.devRecipeFoods)
+                    insertDevRecipeNutritions(contentData.devRecipeNutritions)
+                    insertDevRecipePortionSizes(contentData.devRecipePortionSizes)
+                    insertDevRecipeRelTags(contentData.devRecipeRelTags)
+                    insertDevRecipeTags(contentData.devRecipeTags)
+                    insertDevRecipes(contentData.devRecipes)
+                }
+                ThreadUtils.runOnMainThread {
+                    downloadResource.value = Resource(100, Resource.Status.SUCCESS)
+                }
+            }
+            FoodDataProvider.getUserDatabase().runInTransaction {
+                contentData.devRecipes.getOrNull(0)?.also {
+                    FoodDataProvider.getUserDatabase().userInfoDao().insertOnlineRecipe(
+                        UserOnLineRecipes(CURRENT_USER_ID, it.number ?: "")
+                    )
+                }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            ThreadUtils.runOnMainThread {
+                downloadResource.value = Resource(0, Resource.Status.FAILURE)
+            }
+        }
+    }
+
+    companion object {
+        const val ERR_NO_RECIPE_MATCH = -1
+    }
+}

+ 374 - 0
BusinessStep/src/main/res/layout/activity_cook_detail.xml

@@ -0,0 +1,374 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:background="#ffffff"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:background="#555">
+    
+    <ImageView
+        android:id="@+id/iv_top_banner"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/convert_807px"
+        android:src="#ccc"
+        android:scaleType="centerCrop"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/convert_807px"
+        android:background="#80000000"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <View
+        android:id="@+id/view_back"
+        android:layout_width="@dimen/convert_92px"
+        android:layout_height="@dimen/convert_92px"
+        android:background="@drawable/ic_detail_back"
+        android:layout_marginTop="@dimen/convert_33px"
+        android:layout_marginStart="@dimen/convert_45px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <View
+        android:id="@+id/view_more"
+        android:layout_width="@dimen/convert_92px"
+        android:layout_height="@dimen/convert_92px"
+        android:background="@drawable/ic_detail_more"
+        android:layout_marginTop="@dimen/convert_33px"
+        android:layout_marginEnd="@dimen/convert_45px"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <View
+        android:id="@+id/view_hide"
+        android:layout_width="@dimen/convert_92px"
+        android:layout_height="@dimen/convert_92px"
+        android:background="@drawable/ic_detail_show"
+        android:layout_marginTop="@dimen/convert_33px"
+        android:layout_marginEnd="@dimen/convert_33px"
+        app:layout_constraintEnd_toStartOf="@+id/view_more"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <RelativeLayout
+        android:id="@+id/like_layout"
+        android:layout_width="@dimen/convert_92px"
+        android:layout_height="@dimen/convert_92px"
+        android:layout_marginTop="@dimen/convert_33px"
+        android:layout_marginEnd="@dimen/convert_33px"
+        android:background="@drawable/white_circle"
+        app:layout_constraintEnd_toStartOf="@+id/view_hide"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:id="@+id/iv_like"
+            android:layout_width="@dimen/convert_48px"
+            android:layout_height="@dimen/convert_42px"
+            android:layout_centerInParent="true"
+            android:background="@drawable/ic_like" />
+    </RelativeLayout>
+
+
+    <com.develop.common.widget.StarView
+        android:id="@+id/start_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/convert_70px"
+        android:layout_marginStart="@dimen/convert_45px"
+        app:layout_constraintTop_toBottomOf="@+id/view_back"
+        app:layout_constraintStart_toStartOf="parent"/>
+
+    <TextView
+        android:id="@+id/tv_food_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="Guacamole"
+        android:textColor="#ffffff"
+        android:textSize="@dimen/convert_54px"
+        android:textStyle="bold"
+        android:layout_marginTop="@dimen/convert_160px"
+        android:layout_marginStart="@dimen/convert_47px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/view_back"/>
+
+    <TextView
+        android:id="@+id/tv_food_time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="Preparation: 10 min\nReady in: 40 min"
+        android:textColor="#ffffff"
+        android:textSize="@dimen/convert_39px"
+        android:lineSpacingExtra="@dimen/convert_16px"
+        android:layout_marginTop="@dimen/convert_20px"
+        android:layout_marginStart="@dimen/convert_102px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/tv_food_name"/>
+
+    <TextView
+        android:id="@+id/tv_easy"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="Easy"
+        android:textColor="#ffffff"
+        android:textSize="@dimen/convert_39px"
+        android:layout_marginTop="@dimen/convert_10px"
+        app:layout_constraintStart_toStartOf="@+id/tv_food_time"
+        app:layout_constraintTop_toBottomOf="@+id/tv_food_time"/>
+
+    <View
+        android:id="@+id/view_icon1"
+        android:layout_width="@dimen/convert_69px"
+        android:layout_height="@dimen/convert_74px"
+        android:background="@drawable/ic_detail_time"
+        android:layout_marginTop="@dimen/convert_14px"
+        app:layout_constraintTop_toBottomOf="@+id/tv_food_name"
+        app:layout_constraintEnd_toStartOf="@+id/tv_food_time"/>
+
+    <View
+        android:id="@+id/view_icon2"
+        android:layout_width="@dimen/convert_69px"
+        android:layout_height="@dimen/convert_74px"
+        android:background="@drawable/ic_detail_easy"
+        android:layout_marginTop="@dimen/convert_46px"
+        app:layout_constraintStart_toStartOf="@+id/view_icon1"
+        app:layout_constraintTop_toBottomOf="@+id/view_icon1"/>
+
+    <TextView
+        android:id="@+id/tv_jar_count"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="2 JARS"
+        android:textColor="#fff"
+        android:textSize="@dimen/convert_54px"
+        android:layout_marginStart="@dimen/convert_47px"
+        android:layout_marginTop="@dimen/convert_50px"
+        app:layout_constraintTop_toBottomOf="@+id/view_icon2"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <TextView
+        android:id="@+id/tv_serving_size"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/serving_sizes"
+        android:textColor="#fff"
+        android:textSize="@dimen/convert_54px"
+        android:layout_marginEnd="@dimen/convert_47px"
+        android:layout_marginTop="@dimen/convert_608px"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <View
+        android:id="@+id/view_serving"
+        android:layout_width="@dimen/convert_50px"
+        android:layout_height="@dimen/convert_50px"
+        android:background="@drawable/ic_cook_serving"
+        android:layout_marginEnd="@dimen/convert_21px"
+        app:layout_constraintEnd_toStartOf="@+id/tv_serving_size"
+        app:layout_constraintTop_toTopOf="@+id/tv_serving_size"
+        app:layout_constraintBottom_toBottomOf="@+id/tv_serving_size"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_start_cooking"
+        android:layout_width="@dimen/convert_675px"
+        android:layout_height="@dimen/convert_180px"
+        android:background="@drawable/bg_download_button"
+        app:layout_constraintTop_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintBottom_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:elevation="@dimen/convert_15px">
+
+        <com.develop.common.widget.DownloadProgressView
+            android:id="@+id/view_progress"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_margin="@dimen/convert_15px"/>
+
+        <ImageView
+            android:id="@+id/view_icon"
+            android:layout_width="@dimen/convert_87px"
+            android:layout_height="@dimen/convert_87px"
+            android:src="@drawable/ic_detail_cook_start"
+            android:layout_marginStart="@dimen/convert_66px"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+        <TextView
+            android:id="@+id/tv_download"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="@dimen/convert_54px"
+            android:textColor="#ffffff"
+            android:layout_gravity="center_vertical"
+            android:text="@string/start_cooking"
+            android:layout_marginEnd="@dimen/convert_26px"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/view_icon"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+        <TextView
+            android:id="@+id/tv_progress"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#fff"
+            android:textSize="@dimen/convert_54px"
+            android:text="0%"
+            android:visibility="gone"
+            android:includeFontPadding="false"
+            app:layout_constraintVertical_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@+id/tv_download_state"/>
+
+        <TextView
+            android:id="@+id/tv_download_state"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="@dimen/convert_54px"
+            android:textColor="#fff"
+            android:text="@string/downloading"
+            android:visibility="gone"
+            android:includeFontPadding="false"
+            app:layout_constraintTop_toBottomOf="@+id/tv_progress"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <ImageView
+        android:id="@+id/iv_tab1"
+        android:layout_width="@dimen/convert_249px"
+        android:layout_height="@dimen/convert_182px"
+        android:layout_marginTop="@dimen/convert_171px"
+        android:src="@drawable/ic_detail_tab1"
+        android:padding="@dimen/convert_28px"
+        android:background="@drawable/selector_detail_tab_left"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintTop_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/iv_tab2"
+        app:tint="@color/color_cook_tab_tint"/>
+
+    <ImageView
+        android:id="@+id/iv_tab2"
+        android:layout_width="@dimen/convert_249px"
+        android:layout_height="@dimen/convert_182px"
+        android:layout_marginTop="@dimen/convert_171px"
+        android:src="@drawable/ic_detail_tab2"
+        android:padding="@dimen/convert_28px"
+        android:background="@drawable/selector_detail_tab_center"
+        app:layout_constraintTop_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintStart_toEndOf="@+id/iv_tab1"
+        app:layout_constraintEnd_toStartOf="@+id/iv_tab3"
+        app:tint="@color/color_cook_tab_tint"/>
+
+    <ImageView
+        android:id="@+id/iv_tab3"
+        android:layout_width="@dimen/convert_249px"
+        android:layout_height="@dimen/convert_182px"
+        android:layout_marginTop="@dimen/convert_171px"
+        android:src="@drawable/ic_detail_tab3"
+        android:padding="@dimen/convert_28px"
+        android:background="@drawable/selector_detail_tab_center"
+        app:layout_constraintTop_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintStart_toEndOf="@+id/iv_tab2"
+        app:layout_constraintEnd_toStartOf="@+id/iv_tab4"
+        app:tint="@color/color_cook_tab_tint"/>
+
+    <ImageView
+        android:id="@+id/iv_tab4"
+        android:layout_width="@dimen/convert_249px"
+        android:layout_height="@dimen/convert_182px"
+        android:layout_marginTop="@dimen/convert_171px"
+        android:src="@drawable/ic_detail_tab4"
+        android:padding="@dimen/convert_28px"
+        android:background="@drawable/selector_detail_tab_right"
+        app:layout_constraintTop_toBottomOf="@+id/iv_top_banner"
+        app:layout_constraintStart_toEndOf="@+id/iv_tab3"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:tint="@color/color_cook_tab_tint" />
+
+    <FrameLayout
+        android:id="@+id/fl_tab_content"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toBottomOf="@+id/iv_tab1"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_func_more"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#66000000"
+        android:elevation="@dimen/convert_20px"
+        android:visibility="gone">
+
+        <View
+            android:id="@+id/view_more_close"
+            android:layout_width="@dimen/convert_92px"
+            android:layout_height="@dimen/convert_92px"
+            android:background="@drawable/ic_detail_more"
+            android:layout_marginTop="@dimen/convert_33px"
+            android:layout_marginEnd="@dimen/convert_45px"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"/>
+
+        <LinearLayout
+            android:layout_width="@dimen/convert_381px"
+            android:layout_height="wrap_content"
+            android:paddingVertical="@dimen/convert_69px"
+            android:layout_marginEnd="@dimen/convert_53px"
+            app:layout_constraintTop_toBottomOf="@+id/view_more_close"
+            app:layout_constraintEnd_toEndOf="parent"
+            android:layout_marginTop="@dimen/convert_35px"
+            android:background="@drawable/bg_cook_detail_more"
+            android:paddingStart="@dimen/convert_63px"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/tv_more_score"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:drawableStart="@drawable/ic_more_star"
+                android:text="@string/score"
+                android:textColor="#6B6B6B"
+                android:textSize="@dimen/convert_54px"
+                android:gravity="center_vertical"
+                android:drawablePadding="@dimen/convert_29px"/>
+
+            <TextView
+                android:id="@+id/tv_more_share"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:drawableStart="@drawable/ic_more_share"
+                android:text="@string/share"
+                android:textColor="#6B6B6B"
+                android:textSize="@dimen/convert_54px"
+                android:gravity="center_vertical"
+                android:drawablePadding="@dimen/convert_29px"
+                android:layout_marginTop="@dimen/convert_60px"/>
+
+            <TextView
+                android:id="@+id/tv_more_delete"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:drawableStart="@drawable/ic_more_delete"
+                android:text="@string/delete"
+                android:textColor="#6B6B6B"
+                android:textSize="@dimen/convert_54px"
+                android:gravity="center_vertical"
+                android:drawablePadding="@dimen/convert_29px"
+                android:layout_marginTop="@dimen/convert_60px"/>
+
+        </LinearLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 21 - 0
BusinessStep/src/main/res/layout/fragment_detail_cook_desc.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:paddingHorizontal="@dimen/convert_50px">
+
+    <TextView
+        android:id="@+id/tv_step_detail"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="MAKES 2 JARS (300 ML EACH)"
+        android:textSize="@dimen/convert_39px"
+        android:textColor="#6B6B6B"
+        android:lineSpacingExtra="@dimen/convert_10px"
+        android:layout_marginTop="@dimen/convert_83px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+</ScrollView>

+ 14 - 0
BusinessStep/src/main/res/layout/fragment_detail_cook_method.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rv_tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingBottom="36dp"
+        android:clipToPadding="false"/>
+
+</FrameLayout>

+ 38 - 0
BusinessStep/src/main/res/layout/fragment_detail_cook_serving.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:paddingHorizontal="@dimen/convert_47px">
+
+    <TextView
+        android:id="@+id/tv_step_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/per_serving"
+        android:textSize="@dimen/convert_45px"
+        android:textColor="#FFA627"
+        android:layout_marginTop="@dimen/convert_83px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <ImageView
+        android:id="@+id/iv_collapse"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/ic_detail_collapse"
+        app:layout_constraintTop_toTopOf="@+id/tv_step_title"
+        app:layout_constraintBottom_toBottomOf="@+id/tv_step_title"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rv_list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/convert_59px"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintTop_toBottomOf="@+id/tv_step_title"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 38 - 0
BusinessStep/src/main/res/layout/fragment_detail_cook_source.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:paddingHorizontal="@dimen/convert_47px">
+
+    <TextView
+        android:id="@+id/tv_step_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="MAKES 2 JARS (300 ML EACH)"
+        android:textSize="@dimen/convert_45px"
+        android:textColor="#FFA627"
+        android:layout_marginTop="@dimen/convert_83px"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <ImageView
+        android:id="@+id/iv_collapse"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/ic_detail_collapse"
+        app:layout_constraintTop_toTopOf="@+id/tv_step_title"
+        app:layout_constraintBottom_toBottomOf="@+id/tv_step_title"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rv_list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/convert_59px"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintTop_toBottomOf="@+id/tv_step_title"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 33 - 0
BusinessStep/src/main/res/layout/item_cook_source.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/convert_122px"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <TextView
+        android:id="@+id/tv_source_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="@dimen/convert_45px"
+        android:textColor="#6B6B6B"
+        android:layout_marginTop="@dimen/convert_28px"
+        tools:text="AAAAAAA"/>
+
+    <TextView
+        android:id="@+id/tv_source_amount"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="@dimen/convert_45px"
+        android:textColor="#6B6B6B"
+        android:layout_marginTop="@dimen/convert_28px"
+        android:layout_gravity="end"
+        tools:text="AAAAAAA"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:background="#E5E5E5"
+        android:layout_gravity="bottom"/>
+
+</FrameLayout>

+ 34 - 0
BusinessStep/src/main/res/layout/item_cook_tool.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <ImageView
+        android:id="@+id/iv_tool_icon"
+        android:layout_width="@dimen/convert_240px"
+        android:layout_height="@dimen/convert_240px"
+        android:background="@drawable/bg_cook_tool"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="@dimen/convert_60px"
+        android:scaleType="fitCenter"
+        tools:src="@drawable/ic_cook_tool1"
+        android:elevation="@dimen/convert_15px"/>
+
+    <TextView
+        android:id="@+id/tv_tool_name"
+        android:layout_width="@dimen/convert_340px"
+        android:layout_height="wrap_content"
+        android:maxWidth="@dimen/convert_200px"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="@dimen/convert_35px"
+        android:textColor="#6B6B6B"
+        android:maxLines="1"
+        android:ellipsize="end"
+        android:textSize="@dimen/convert_39px"
+        android:gravity="center"
+        tools:text="AAAAAA"/>
+
+</LinearLayout>

+ 5 - 0
libBase/src/main/java/com/develop/base/ext/NumberExt.kt

@@ -0,0 +1,5 @@
+package com.develop.base.ext
+
+fun Int?.isPositive(): Boolean {
+    return this != null && this > 0
+}

+ 1 - 0
libThirdPart/build.gradle

@@ -68,5 +68,6 @@ dependencies {
     api files('libs/commons-beanutils-1.9.4.jar')
     api 'com.github.princekin-f:EasyFloat:2.0.4'
     api 'de.hdodenhof:circleimageview:3.1.0'
+    api 'com.google.zxing:core:3.3.3'
 
 }