DragLayout: QQ5.0側拉菜單的新特效

来源:http://www.cnblogs.com/vincent0519/archive/2016/11/11/6055275.html
-Advertisement-
Play Games

一、項目概要 1.1 項目效果如圖: 1.2 需要使用到的技術 ViewDragHelper: 要實現和QQ5.0側滑的特效,需要藉助谷歌在2013年I/O大會上發佈的ViewDragHelper類,提供這個類目的就是為瞭解決拖拽滑動問題 1.3 側滑菜單的實現方式 1. SlidingMenu 第 ...


一、項目概要

  1.1 項目效果如圖:

  gif

  

  1.2 需要使用到的技術

     ViewDragHelper: 要實現和QQ5.0側滑的特效,需要藉助谷歌在2013I/O大會上發佈的ViewDragHelper類,提供這個類目的就是為瞭解決拖拽滑動問題

 

  1.3 側滑菜單的實現方式

    1. SlidingMenu 第三方庫

    2. DrawerLayout v4包中的類

    3. 自定義控制項 

 

  1.4 一些回調方法 

    - tryCaptureView: 用來決定是否可以拖動
    - clampViewPositionHorizontal: 用來設置子控制項將要顯示的位置 [限制子控制項拖動的範圍]
    - getViewHorizontalDragRange:返回水平方向拖動的最大範圍,返回大於0的值才可以拖動
    - onViewPositionChanged: 位置改變時調用 [關聯菜單與主界面的滑動,監聽拖動狀態,伴隨動畫]
    - onViewReleased: 拖動結束後,鬆開手時調用 [平滑地打開或關閉側滑菜單]

 

二、項目實現

  2.1 創建DragLayout

1 public class DragLayout extends FrameLayout {
2     public DragLayout(Context context) {
3         super(context);
4     }
5     public DragLayout(Context context, AttributeSet attrs) {
6         super(context, attrs);
7     }
8 }

 

    2.2 創建側滑面板佈局

 1 <com.xiaowu.draglayout.view.DragLayout 
 2     xmlns:android="http://schemas.android.com/apk/res/android"
 3     xmlns:tools="http://schemas.android.com/tools"
 4     android:id="@+id/drag_layout"
 5     android:layout_width="match_parent"
 6     android:layout_height="match_parent"
 7     android:background="@drawable/bg" >
 8 
 9     <!-- 側滑菜單佈局 -->
10     <LinearLayout
11         android:layout_width="match_parent"
12         android:layout_height="match_parent"
13         android:background="#33ff0000" />
14 
15     <!-- 主界面佈局 -->
16     <LinearLayout
17         android:layout_width="match_parent"
18         android:layout_height="match_parent"
19         android:background="#3300ff00" />
20 
21 </com.xiaowu.draglayout.view.DragLayout>

 

      2.3 DragLayout的主程式代碼,下麵代碼中有詳細的講解,我就不多分步驟實現了

  1 package com.xiaowu.draglayout.view;
  2 
  3 import android.content.Context;
  4 import android.graphics.Color;
  5 import android.graphics.PorterDuff;
  6 import android.graphics.drawable.Drawable;
  7 import android.support.v4.view.ViewCompat;
  8 import android.support.v4.widget.ViewDragHelper;
  9 import android.util.AttributeSet;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 import android.widget.FrameLayout;
 13 
 14 /**
 15  * Created by ${VINCENT} on 2016/11/8.
 16  */
 17 
 18 public class DragLayout extends FrameLayout {
 19 
 20     private ViewDragHelper mViewDragHelper;
 21     private View mMenuView;
 22     private View mMainView;
 23     private int mRange;
 24     private int mWidth;
 25     private int mHeight;
 26 
 27     public DragLayout(Context context) {
 28         super(context);
 29         init();
 30     }
 31 
 32     public DragLayout(Context context, AttributeSet attrs) {
 33         super(context, attrs);
 34         init();
 35     }
 36 
 37     /** 填充完成後調用此方法 */
 38     @Override
 39     protected void onFinishInflate() {
 40         super.onFinishInflate();
 41         // 健壯性判斷
 42         if (getChildCount() < 2) {
 43             throw new IllegalStateException("DrawLayout至少要有兩個子控制項");
 44         }
 45         mMenuView = getChildAt(0);
 46         mMainView = getChildAt(1);
 47     }
 48 
 49     // step1:創建ViewDragHelper對象
 50     private void init() {
 51         float sensitivity = 1.0f; //值越大,靈敏度越高
 52         mViewDragHelper = ViewDragHelper.create(this, sensitivity, mCallBack);
 53     }
 54 
 55     // step2:由ViewDragHelper決定是否攔截事件
 56     @Override
 57     public boolean onInterceptTouchEvent(MotionEvent ev) {
 58         return mViewDragHelper.shouldInterceptTouchEvent(ev);
 59     }
 60 
 61     // step3:把觸摸事件交給ViewDragHelper處理
 62     @Override
 63     public boolean onTouchEvent(MotionEvent event) {
 64         mViewDragHelper.processTouchEvent(event);
 65         return true; //讓mViewDragHelper持續接收到觸摸事件
 66     }
 67 
 68     // step4:處理ViewDragHelper的Callback方法
 69     ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() {
 70 
 71         // (1)捕獲子控制項,返回true表示子控制項可以拖動
 72         @Override
 73         public boolean tryCaptureView(View child, int pointerId) {
 74             return true;
 75         }
 76 
 77         // (2)子控制項顯示的方向(horizontal, vertical)
 78         // left: 被拖動控制項的將要顯示的位置
 79         // dx: 位置的偏移量 = left - 當前的left
 80         @Override
 81         public int clampViewPositionHorizontal(View child, int left, int dx) {
 82             if (child == mMainView) {
 83                 left = reviseLeft(left);
 84             }
 85             return left;
 86         }
 87 
 88         // (3)返回水平方向拖動的最大範圍mRange,內部會根據返回值計算動畫執行的時間
 89         @Override
 90         public int getViewHorizontalDragRange(View child) {
 91             return mRange;
 92         }
 93 
 94         // (4)位置發生改變的回調
 95         @Override
 96         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
 97             // (a) 關聯子控制項的滑動
 98             if (changedView == mMenuView) {
 99                 // 側拉菜單界面不變時
100                 mMenuView.layout(0, 0, mWidth, mHeight);
101                 // 主菜單界面的新位置
102                 int newLeft = mMenuView.getLeft() + dx;
103                 newLeft = reviseLeft(newLeft);
104                 mMainView.layout(newLeft, 0, mWidth + newLeft, mHeight);
105             }
106             // (b) 事件的監聽(打開,拖動,關閉)
107             listenDragStatus();
108             // (c) 事件伴隨的動畫
109             animateChildren();
110         }
111 
112         // (5) 拖動結束時回調的方法
113         // xvel:釋放時的回調速度,在這裡向右為正
114         @Override
115         public void onViewReleased(View releasedChild, float xvel, float yvel) {
116             if (xvel > 0) {
117                 open();
118             } else if (xvel == 0 && mMainView.getLeft() > mRange / 2) {
119                 open();
120             } else {
121                 close();
122             }
123         }
124     };
125 
126     //============================動畫的定義=====================================
127     /** 估值器:變化值 = 開始值 + (結束值 - 開始值) * 百分比 */
128     public float evaluate(float start, float end, float percent) {
129         return start + (end - start) * percent;
130     }
131 
132     protected void animateChildren() {
133         float percent = ((float) mMainView.getLeft()) / mRange;
134 
135         // 1.主界面的縮放
136         mMainView.setScaleX(evaluate(1f, 0.8f, percent));
137         mMainView.setScaleY(evaluate(1f, 0.8f, percent));
138         // 2.側拉菜單的縮放
139         mMenuView.setTranslationX((int) evaluate(-mRange, 0, percent)); // 平移
140         mMenuView.setScaleX(evaluate(0.5f, 1.0f, percent));
141         mMenuView.setScaleY(evaluate(0.5f, 1.0f, percent));
142         mMenuView.setAlpha(evaluate(0.5f, 1.0f, percent));
143         // 3.背景圖片:亮度的變化
144         Drawable background = getBackground();
145         if (background != null) {
146             // 過渡的顏色
147             int color = (int)evaluate2(percent, Color.BLACK, Color.TRANSPARENT);
148             background.setColorFilter(color, PorterDuff.Mode.SRC_OVER);
149         }
150     }
151 
152     /** 處理顏色漸變的相容性問題 */
153     public Object evaluate2(float fraction, Object startValue, Object endValue) {
154         int startInt = (Integer) startValue;
155         int startA = (startInt >> 24) & 0xff;
156         int startR = (startInt >> 16) & 0xff;
157         int startG = (startInt >> 8) & 0xff;
158         int startB = startInt & 0xff;
159 
160         int endInt = (Integer) endValue;
161         int endA = (endInt >> 24) & 0xff;
162         int endR = (endInt >> 16) & 0xff;
163         int endG = (endInt >> 8) & 0xff;
164         int endB = endInt & 0xff;
165 
166         return ((startA + (int)(fraction * (endA - startA))) << 24) |
167                 ((startR + (int)(fraction * (endR - startR))) << 16) |
168                 ((startG + (int)(fraction * (endG - startG))) << 8) |
169                 ((startB + (int)(fraction * (endB - startB))));
170     }
171 
172     //============================狀態的監聽begin================================
173     /** 事件的監聽 */
174     protected void listenDragStatus() {
175         int left = mMainView.getLeft();
176         if (left == 0) {
177             mCurrentStatus = DragStatus.CLOSE;
178         } else if (left == mRange) {
179             mCurrentStatus = DragStatus.OPEN;
180         } else {
181             mCurrentStatus = DragStatus.DRAGGING;
182         }
183 
184         //當事件發生時,調用監聽器中的方法
185         if (mOnDragListener != null) {
186             if (mCurrentStatus == DragStatus.OPEN) {
187                 mOnDragListener.onOpen();
188             } else if (mCurrentStatus == DragStatus.CLOSE) {
189                 mOnDragListener.onClose();
190             } else {
191                 float percent = ((float) mMainView.getLeft()) / mRange;
192                 mOnDragListener.onDragging(percent);
193             }
194         }
195     }
196 
197     /** 狀態的定義 */
198     public enum DragStatus {
199         OPEN, CLOSE, DRAGGING
200     }
201 
202     /** 當前的狀態 */
203     private DragStatus mCurrentStatus = DragStatus.CLOSE;
204 
205     public DragStatus getCurrentStatus() {
206         return mCurrentStatus;
207     }
208 
209     /** 定義介面 */
210     public interface OnDragListener {
211         void onOpen();
212         void onClose();
213         void onDragging(float percent);
214     }
215 
216     private OnDragListener mOnDragListener;
217 
218     /** 提供設置監聽器的set方法 */
219     public void setOnDragListener(OnDragListener onDragListener) {
220         this.mOnDragListener = onDragListener;
221     }
222 
223     //============================狀態的監聽end================================
224 
225     @Override
226     public void computeScroll() {
227         super.computeScroll();
228         // 若如果沒有移動到正確的位置,需要刷新
229         if (mViewDragHelper.continueSettling(true)) {
230             ViewCompat.postInvalidateOnAnimation(this);
231         }
232     }
233 
234     /** 限定主界面的滑動範圍 */
235     protected int reviseLeft(int left) {
236         if (left < 0) {
237             left = 0;
238         } else if (left > mRange) {
239             left = mRange;
240         }
241         return left;
242     }
243 
244     /** 控制項尺寸發生改變時,回調該方法 */
245     @Override
246     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
247         super.onSizeChanged(w, h, oldw, oldh);
248         // 獲取DrawLayout的寬高
249         mWidth = getMeasuredWidth();
250         mHeight = getMeasuredHeight();
251         // 拖拽的比例
252         mRange = (int) (mWidth * 0.6f);
253     }
254 
255     /** 打開側拉菜單 */
256     protected void open() {
257         mViewDragHelper.smoothSlideViewTo(mMainView, mRange, 0);
258         // 刷新界面
259         ViewCompat.postInvalidateOnAnimation(this);
260     }
261 
262     /** 關閉側拉菜單 */
263     protected void close() {
264         mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
265         // 刷新界面
266         ViewCompat.postInvalidateOnAnimation(this);
267     }
268 
269     /** 側滑菜單是否打開 */
270     public boolean isOpen() {
271         return mCurrentStatus == DragStatus.OPEN;
272     }
273 
274 }

  

   2.4 創建MyLinearLayout.java文件,處理側拉與主菜單的衝突事件

 1 package com.xiaowu.draglayout.view;
 2 
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.view.MotionEvent;
 6 import android.widget.LinearLayout;
 7 
 8 /**
 9  * Created by ${VINCENT} on 2016/11/9.
10  */
11 
12 public class MyLinearLayout extends LinearLayout {
13 
14     private DragLayout mDragLayout;
15 
16     public MyLinearLayout(Context context) {
17         super(context);
18     }
19 
20     public MyLinearLayout(Context context, AttributeSet attrs) {
21         super(context, attrs);
22     }
23 
24     /** 根據它的打開狀態決定是否要攔截事件 */
25     public void setDragLayout(DragLayout dragLayout) {
26         this.mDragLayout = dragLayout;
27     }
28 
29     /** 如果側滑菜單打開了,禁止主菜單的列表滑動 */
30     @Override
31     public boolean onInterceptTouchEvent(MotionEvent ev) {
32         if (mDragLayout.isOpen()) {
33             return true;
34         }
35         return super.onInterceptTouchEvent(ev);
36     }
37 
38     /** 如果側滑菜單打開了,消費主菜單的觸摸事件,禁止通過滑動主菜單使側拉菜單的列表滑動 */
39     @Override
40     public boolean onTouchEvent(MotionEvent event) {
41         if (mDragLayout.isOpen()) {
42             return true;
43         }
44         return super.onTouchEvent(event);
45     }
46 }

 

   2.5 接下來是MainActivity的代碼實現

 1 package com.xiaowu.draglayout;
 2 
 3 import android.graphics.Color;
 4 import android.support.v7.app.AppCompatActivity;
 5 import android.os.Bundle;
 6 import android.view.View;
 7 import android.view.ViewGroup;
 8 import android.view.Window;
 9 import android.widget.ArrayAdapter;
10 import android.widget.ImageView;
11 import android.widget.ListView;
12 import android.widget.TextView;
13 import android.widget.Toast;
14 
15 import com.xiaowu.draglayout.view.DragLayout;
16 import com.xiaowu.draglayout.view.MyLinearLayout;
17 
18 public class MainActivity extends AppCompatActivity {
19 
20     private ImageView mIvHeader;
21     private MyLinearLayout mMyLinearLayout;
22     private DragLayout mDragLayout;
23 
24     @Override
25     protected void onCreate(Bundle savedInstanceState) {
26         super.onCreate(savedInstanceState);
27         requestWindowFeature(Window.FEATURE_NO_TITLE);
28 
29         setContentView(R.layout.activity_main);
30         mIvHeader = (ImageView) findViewById(R.id.iv_header);
31 
32         initDragLayout();
33         mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_ll);
34         // 根據打開的狀態決定是否攔截事件
35         mMyLinearLayout.setDragLayout(mDragLayout);
36 
37         initListView();
38     }
39 
40     private void initListView() {
41         ListView lvMenu = (ListView) findViewById(R.id.lv_menu);
42         ListView lvMain = (ListView) findViewById(R.id.lv_main);
43 
44         lvMenu.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
45                 Constant.MENUS){
46             @Override
47             public View getView(int position, View convertView, ViewGroup parent) {
48                 TextView view = (TextView) super.getView(position, convertView, parent);
49                 view.setTextSize(dp2px(16));
50                 view.setTextColor(Color.WHITE);
51                 return view;
52             }
53         });
54 
55         lvMain.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
56                 Constant.LIST_DATAS));
57 
58     }
59 
60     private void initDragLayout() {
61         mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
62         mDragLayout.setOnDragListener(new DragLayout.OnDragListener() {
63             @Override
64             public void onOpen() {
65                 showToast("打開");
66             }
67 
68             @Override
69             public void onClose() {
70                 showToast("關閉");
71             }
72 
73             @Override
74             public void onDragging(float percent) {
75                 mIvHeader.setAlpha(1 - percent );
76             }
77         });
78     }
79 
80     /** toast使用單例模式,可以隨狀態刷新 */
81     private Toast mToast;
82 
83     public void showToast(String msg) {
84         if (mToast == null) {
85             mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
86         }
87         mToast.setText(msg);
88         mToast.show();
89     }
90 
91     public int dp2px(int dp) {
92         float density = this.getResources().getDisplayMetrics().density;
93         return (int) (dp * density + 0.5f);
94     }
95 
96 }

 

   2.6 佈局文件的最終完善

 1 <com.xiaowu.draglayout.view.DragLayout
 2     xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:id="@+id/drag_layout"
 4     android:layout_width="match_parent"
 5     android:layout_height="match_parent"
 6     android:background="@drawable/bg">
 7 
 8     <!-- 側拉菜單界面 -->
 9     <LinearLayout
10         android:layout_width="match_parent"
11         android:layout_height="match_parent"
12         android:orientation="vertical"
13         android:gravity="center_vertical"
14         android:padding="15dp" >
15 
16         <ImageView
17             android:layout_width="40dp"
18             android:layout_height="40dp"
19             android:background="@drawable/head"/>
20 
21         <ListView
22             android:id="@+id/lv_menu"
23             android:layout_width="match_parent"
24             android:layout_height="match_parent"
25             android:layout_marginTop="10dp" />
26 
27     </LinearLayout>
28 
29     <!-- 主菜單界面 -->
30     <com.xiaowu.draglayout.view.MyLinearLayout
31         android:id="@+id/my_ll"
32         android:layout_width="match_parent"
33         android:layout_height="match_parent"
	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 公司對於搭建本地私有npm庫有如下要求: 私有包托管在內部伺服器中 項目中使用了公共倉庫上的公共包,也使用了內部伺服器上的私有包 希望下載的時候,公共包走公共倉庫,私有包走內部伺服器的私有倉庫 伺服器硬碟有限,希望只緩存下載過的包,而不是全部同步。 ... ...
  • 關於egret接入第三方庫的方法,egret也有文檔,可參考官方的接入方法接入http://developer.egret.com/cn/github/egret-docs/extension/threes/instructions/index.html。 這裡記錄一下接入puremvc庫的過程,關 ...
  • 在複習廖雪峰老師的JS教程時,看到數組,又遇到了之前做過的那道小題,題目如下: 練習:在新生歡迎會上,你已經拿到了新同學的名單,請排序後顯示:歡迎XXX,XXX,XXX和XXX同學!: 給出了數組: 這道題作為一個練習並不是很難,當時自己第一次看也是按照最直接的辦法,使用了${arr[i]}來拿到對 ...
  • Redux就是個數據中心,不依附於任何框架在哪使用都行。但是和它最搭配的應該就是React了,而且大家學習它的動力大多也是解決React狀態管理的問題。都說Redux文檔詳盡清晰,但我感覺並不友好,它沒有用最簡單直觀的方式告訴你如何搭配React使用。研究了兩天的文檔和示例,終於在項目中用上了我認為 ...
  • 目前 "AlloyFinger" 作為騰訊手機QQ web手勢解決方案,在各大項目中都發揮著作用。 感興趣的同學可以去Github看看: "https://github.com/AlloyTeam/AlloyFinger" 在騰訊,如:興趣部落、QQ群、QQ動漫、騰訊學院、TEDxTencent、 ...
  • 1、跨域請求: Cross Domain Request:跨功能變數名稱的HTTP請求,瀏覽器從某個功能變數名稱下的資源訪問了另一功能變數名稱下的另一資源(協議、功能變數名稱或是埠號不同); ①瀏覽器允許跨域請求的情形: <img>、<link>、<script>、<iframe> ②禁止跨域請求的情形: XHR——瀏覽器預設出 ...
  • 前言 DOM的作用是將網頁轉為一個javascript對象,從而可以使用javascript對網頁進行各種操作(比如增刪內容)。瀏覽器會根據DOM模型,將HTML文檔解析成一系列的節點,再由這些節點組成一個樹狀結構。DOM的最小組成單位叫做節點(node),文檔的樹形結構(DOM樹)由12種類型的節 ...
  • 簡介 沒有用過Node,記的這些只是學習的筆記,有什麼錯的地方,望各位前輩指正。 Node是一個伺服器端Javascript解釋器,依賴於Chrome v8引擎進行代碼編譯,事件驅動、非阻塞I/O都是他顯著的特點。 伺服器能夠處理的併發連接的最大數量是目前Web應用程式架構的瓶頸,Node的出現就打 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...