Android自定義控制項(二)

来源:http://www.cnblogs.com/all88/archive/2016/03/28/5329749.html
-Advertisement-
Play Games

這一篇主要來講一下自定義控制項中的自定義viewgroup,我們以項目中最常用的下拉刷新和載入更多組件為例 簡單介紹一下自定義viewgroup時應該怎麼做。 分析:下拉刷新和載入更多的原理和步驟 自定義一個viewgroup,將headerview、contentview和footerview從上到 ...


這一篇主要來講一下自定義控制項中的自定義viewgroup,我們以項目中最常用的下拉刷新和載入更多組件為例

簡單介紹一下自定義viewgroup時應該怎麼做。

分析:下拉刷新和載入更多的原理和步驟

自定義一個viewgroup,將headerview、contentview和footerview從上到下依次佈局,然後在初始化的時候

通過Scrooller滾動使得該組件在y軸方向上滾動headerview的高度,這樣headerview就被隱藏了。而contentview的

寬度和高度都是match_parent的,因此屏幕上 headerview和footerview就都被隱藏在屏幕之外了。當contentview被

滾動到頂部,如果此時用戶繼續下拉,那麼下拉刷新組件將攔截觸摸事件,然後根據用戶的觸摸事件獲取到手指滑動的

y軸距離,並通過scroller將該下拉組件在y軸上滾動手指滑動的距離,實現headerview的顯示和隱藏,從而達到下拉的效果

。當用戶滑動到最底部時會觸發載入更多的操作,此時會通過scroller滾動該下拉刷新組件,將footerview顯示出來,實現載入更多

的效果。具體步驟如下:

第一步:初始化View即headerView contentView和footerView
第二步:測量三個view的大小,並計算出viewgroup的大小
第三步:佈局,將三個view在界面上佈局,按照上中下的順序
第四步:監聽屏幕的觸摸事件,判斷是否下拉刷新或者載入更多
第五步:觸發下拉刷新和載入更多事件執行下拉刷新和載入更多
第六步:下拉刷新和載入更多執行完後的重置操作

示例代碼:

自定義的viewgroup

  1 package com.jiao.simpleimageview.view;
  2 
  3 import android.content.Context;
  4 import android.graphics.Color;
  5 import android.support.v4.view.MotionEventCompat;
  6 import android.util.AttributeSet;
  7 import android.view.LayoutInflater;
  8 import android.view.MotionEvent;
  9 import android.view.View;
 10 import android.view.ViewGroup;
 11 import android.view.animation.RotateAnimation;
 12 import android.widget.AbsListView;
 13 import android.widget.AbsListView.OnScrollListener;
 14 import android.widget.ImageView;
 15 import android.widget.ProgressBar;
 16 import android.widget.Scroller;
 17 import android.widget.TextView;
 18 
 19 import com.jiao.simpleimageview.R;
 20 import com.jiao.simpleimageview.listener.OnLoadListener;
 21 import com.jiao.simpleimageview.listener.OnRefreshListener;
 22 
 23 import java.text.SimpleDateFormat;
 24 import java.util.Date;
 25 
 26 /**
 27  * Created by jiaocg on 2016/3/24.
 28  */
 29 public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements
 30         OnScrollListener {
 31 
 32     /**
 33      *
 34      */
 35     protected Scroller mScroller;
 36 
 37     /**
 38      * 下拉刷新時顯示的header view
 39      */
 40     protected View mHeaderView;
 41 
 42     /**
 43      * 上拉載入更多時顯示的footer view
 44      */
 45     protected View mFooterView;
 46 
 47     /**
 48      * 本次觸摸滑動y坐標上的偏移量
 49      */
 50     protected int mYOffset;
 51 
 52     /**
 53      * 內容視圖, 即用戶觸摸導致下拉刷新、上拉載入的主視圖. 比如ListView, GridView等.
 54      */
 55     protected T mContentView;
 56 
 57     /**
 58      * 最初的滾動位置.第一次佈局時滾動header的高度的距離
 59      */
 60     protected int mInitScrollY = 0;
 61     /**
 62      * 最後一次觸摸事件的y軸坐標
 63      */
 64     protected int mLastY = 0;
 65 
 66     /**
 67      * 空閑狀態
 68      */
 69     public static final int STATUS_IDLE = 0;
 70 
 71     /**
 72      * 下拉或者上拉狀態, 還沒有到達可刷新的狀態
 73      */
 74     public static final int STATUS_PULL_TO_REFRESH = 1;
 75 
 76     /**
 77      * 下拉或者上拉狀態
 78      */
 79     public static final int STATUS_RELEASE_TO_REFRESH = 2;
 80     /**
 81      * 刷新中
 82      */
 83     public static final int STATUS_REFRESHING = 3;
 84 
 85     /**
 86      * LOADING中
 87      */
 88     public static final int STATUS_LOADING = 4;
 89 
 90     /**
 91      * 當前狀態
 92      */
 93     protected int mCurrentStatus = STATUS_IDLE;
 94 
 95     /**
 96      * header中的箭頭圖標
 97      */
 98     private ImageView mArrowImageView;
 99     /**
100      * 箭頭是否向上
101      */
102     private boolean isArrowUp;
103     /**
104      * header 中的文本標簽
105      */
106     private TextView mTipsTextView;
107     /**
108      * header中的時間標簽
109      */
110     private TextView mTimeTextView;
111     /**
112      * header中的進度條
113      */
114     private ProgressBar mProgressBar;
115     /**
116      * 屏幕高度
117      */
118     private int mScreenHeight;
119     /**
120      * Header 高度
121      */
122     private int mHeaderHeight;
123     /**
124      * 下拉刷新監聽器
125      */
126     protected OnRefreshListener mOnRefreshListener;
127     /**
128      * 載入更多回調
129      */
130     protected OnLoadListener mLoadListener;
131 
132     /**
133      * @param context
134      */
135     public RefreshLayoutBase(Context context) {
136         this(context, null);
137     }
138 
139     /**
140      * @param context
141      * @param attrs
142      */
143     public RefreshLayoutBase(Context context, AttributeSet attrs) {
144         this(context, attrs, 0);
145     }
146 
147     /**
148      * @param context
149      * @param attrs
150      * @param defStyle
151      */
152     public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyle) {
153         super(context, attrs);
154 
155         // 初始化Scroller對象
156         mScroller = new Scroller(context);
157 
158         // 獲取屏幕高度
159         mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
160         // header 的高度為屏幕高度的 1/4
161         mHeaderHeight = mScreenHeight / 4;
162 
163         // 初始化整個佈局
164         initLayout(context);
165     }
166 
167     /**
168      * 第一步:初始化整個佈局
169      *
170      * @param context
171      */
172     private final void initLayout(Context context) {
173         // header view
174         setupHeaderView(context);
175         // 設置內容視圖
176         setupContentView(context);
177         // 設置佈局參數
178         setDefaultContentLayoutParams();
179         // 添加mContentView
180         addView(mContentView);
181         // footer view
182         setupFooterView(context);
183 
184     }
185 
186     /**
187      * 初始化 header view
188      */
189     protected void setupHeaderView(Context context) {
190         mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,
191                 false);
192         mHeaderView
193                 .setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
194                         mHeaderHeight));
195         mHeaderView.setBackgroundColor(Color.RED);
196         mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
197         addView(mHeaderView);
198 
199         // HEADER VIEWS
200         mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
201         mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
202         mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
203         mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
204     }
205 
206 
207     /**
208      * 初始化Content View, 子類覆寫.
209      */
210     protected abstract void setupContentView(Context context);
211 
212     /**
213      * 設置Content View的預設佈局參數
214      */
215     protected void setDefaultContentLayoutParams() {
216         ViewGroup.LayoutParams params =
217                 new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
218                         LayoutParams.MATCH_PARENT);
219         mContentView.setLayoutParams(params);
220     }
221 
222     /**
223      * 初始化footer view
224      */
225     protected void setupFooterView(Context context) {
226         mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
227                 this, false);
228         addView(mFooterView);
229     }
230 
231 
232     /**
233      * 第二步:測量
234      * 丈量視圖的寬、高。寬度為用戶設置的寬度,高度則為header,
235      * content view, footer這三個子控制項的高度之和。
236      *
237      * @param widthMeasureSpec
238      * @param heightMeasureSpec
239      */
240     @Override
241     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
242         int width = MeasureSpec.getSize(widthMeasureSpec);
243         int childCount = getChildCount();
244         int finalHeight = 0;
245         for (int i = 0; i < childCount; i++) {
246             View child = getChildAt(i);
247             // measure
248             measureChild(child, widthMeasureSpec, heightMeasureSpec);
249             // 該view所需要的總高度
250             finalHeight += child.getMeasuredHeight();
251         }
252         setMeasuredDimension(width, finalHeight);
253     }
254 
255 
256     /**
257      * 第三步:佈局
258      * 佈局函數,將header, content view,
259      * footer這三個view從上到下佈局。佈局完成後通過Scroller滾動到header的底部,
260      * 即滾動距離為header的高度 +本視圖的paddingTop,從而達到隱藏header的效果.
261      */
262     @Override
263     protected void onLayout(boolean changed, int l, int t, int r, int b) {
264 
265         int childCount = getChildCount();
266         int top = getPaddingTop();
267         for (int i = 0; i < childCount; i++) {
268             View child = getChildAt(i);
269             child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
270             top += child.getMeasuredHeight();
271         }
272 
273         // 計算初始化滑動的y軸距離
274         mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
275         // 滑動到header view高度的位置, 從而達到隱藏header view的效果
276         scrollTo(0, mInitScrollY);
277     }
278 
279 
280     /**
281      * 第四步:監聽滑動事件
282      * 與Scroller合作,實現平滑滾動。在該方法中調用Scroller的computeScrollOffset來判斷滾動是否結束。
283      * 如果沒有結束,
284      * 那麼滾動到相應的位置,並且調用postInvalidate方法重繪界面,
285      * 從而再次進入到這個computeScroll流程,直到滾動結束。
286      */
287     @Override
288     public void computeScroll() {
289         if (mScroller.computeScrollOffset()) {
290             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
291             postInvalidate();
292         }
293     }
294 
295     /*
296      * 在適當的時候攔截觸摸事件,這裡指的適當的時候是當mContentView滑動到頂部,
297      * 並且是下拉時攔截觸摸事件,否則不攔截,交給其child
298      * view 來處理。
299      */
300     @Override
301     public boolean onInterceptTouchEvent(MotionEvent ev) {
302 
303         final int action = MotionEventCompat.getActionMasked(ev);
304         // Always handle the case of the touch gesture being complete.
305         if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
306             // Do not intercept touch event, let the child handle it
307             return false;
308         }
309 
310         switch (action) {
311 
312             case MotionEvent.ACTION_DOWN:
313                 mLastY = (int) ev.getRawY();
314                 break;
315 
316             case MotionEvent.ACTION_MOVE:
317                 // int yDistance = (int) ev.getRawY() - mYDown;
318                 mYOffset = (int) ev.getRawY() - mLastY;
319                 // 如果拉到了頂部, 並且是下拉,則攔截觸摸事件,從而轉到onTouchEvent來處理下拉刷新事件
320                 if (isTop() && mYOffset > 0) {
321                     return true;
322                 }
323                 break;
324 
325         }
326         // Do not intercept touch event, let the child handle it
327         return false;
328     }
329 
330     /**
331      * 第五步:下拉刷新
332      * 1、滑動view顯示出headerview
333      * 2、進度條滾動,修改標題內容
334      * 3、執行下拉刷新監聽
335      * 4、刷新成功或失敗後重置:隱藏headerview 修改標題內容
336      * 在這裡處理觸摸事件以達到下拉刷新或者上拉自動載入的問題
337      *
338      * @see android.view.View#onTouchEvent(android.view.MotionEvent)
339      */
340     @Override
341     public boolean onTouchEvent(MotionEvent event) {//下拉刷新的處理
342         switch (event.getAction()) {
343             case MotionEvent.ACTION_MOVE:
344                 int currentY = (int) event.getRawY();
345                 mYOffset = currentY - mLastY;
346                 if (mCurrentStatus != STATUS_LOADING) {
347                     changeScrollY(mYOffset);
348                 }
349 
350                 rotateHeaderArrow();//旋轉箭頭
351                 changeTips();//重置文本
352                 mLastY = currentY;
353                 break;
354 
355             case MotionEvent.ACTION_UP:
356                 // 下拉刷新的具體操作
357                 doRefresh();
358                 break;
359             default:
360                 break;
361         }
362         return true;
363     }
364 
365     /**
366      * 設置滾動的參數
367      *
368      * @param yOffset
369      */
370     private void startScroll(int yOffset) {
371         mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
372         invalidate();
373     }
374 
375     /**
376      * y軸上滑動到指定位置
377      *
378      * @param distance
379      * @return
380      */
381     protected void changeScrollY(int distance) {
382         // 最大值為 scrollY(header 隱藏), 最小值為0 ( header 完全顯示).
383         int curY = getScrollY();
384         // 下拉
385         if (distance > 0 && curY - distance > getPaddingTop()) {
386             scrollBy(0, -distance);
387         } else if (distance < 0 && curY - distance <= mInitScrollY) {
388             // 上拉過程
389             scrollBy(0, -distance);
390         }
391 
392         curY = getScrollY();
393         int slop = mInitScrollY / 2;
394         //
395         if (curY > 0 && curY < slop) {
396             mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
397         } else if (curY > 0 && curY > slop) {
398             mCurrentStatus = STATUS_PULL_TO_REFRESH;
399         }
400     }
401 
402 
403     /**
404      * 旋轉箭頭圖標
405      */
406     protected void rotateHeaderArrow() {
407 
408         if (mCurrentStatus == STATUS_REFRESHING) {
409             return;
410         } else if (mCurrentStatus == STATUS_PULL_TO_REFRESH && !isArrowUp) {
411             return;
412         } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH && isArrowUp) {
413             return;
414         }
415 
416         mProgressBar.setVisibility(View.GONE);
417         mArrowImageView.setVisibility(View.VISIBLE);
418         float pivotX = mArrowImageView.getWidth() / 2f;
419         float pivotY = mArrowImageView.getHeight() / 2f;
420         float fromDegrees = 0f;
421         float toDegrees = 0f;
422         if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
423             fromDegrees = 180f;
424             toDegrees = 360f;
425         } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
426             fromDegrees = 0f;
427             toDegrees = 180f;
428         }
429 
430         RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
431         animation.setDuration(100);
432         animation.setFillAfter(true);
433         mArrowImageView.startAnimation(animation);
434 
435         if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
436             isArrowUp = true;
437         } else {
438             isArrowUp = false;
439         }
440     }
441 
442     /**
443      * 根據當前狀態修改header view中的文本標簽
444      */
445     protected void changeTips() {
446         if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
447             mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
448         } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
449             mTipsTextView.setText(R.string.pull_to_refresh_release_label);
450         }
451     }
452 
453 
454     /**
455      * 手指抬起時,根據用戶下拉的高度來判斷是否是有效的下拉刷新操作。
456      * 如果下拉的距離超過header view的
457      * 1/2那麼則認為是有效的下拉刷新操作,否則恢複原來的視圖狀態.
458      */
459     private void changeHeaderViewStaus() {
460         int curScrollY = getScrollY();
461         // 超過1/2則認為是有效的下拉刷新, 否則還原
462         if (curScrollY < mInitScrollY / 2) {
463             mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()
464                     - curScrollY);
465             mCurrentStatus = STATUS_REFRESHING;
466             mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);
467             mArrowImageView.clearAnimation();
468             mArrowImageView.setVisibility(View.GONE);
469             mProgressBar.setVisibility(View.VISIBLE);
470         } else {
471             mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
472             mCurrentStatus = STATUS_IDLE;
473         }
474 
475         invalidate();
476     }
477 
478     /**
479      * 執行下拉刷新
480      */
481     protected void doRefresh() {
482         changeHeaderViewStaus();
483         // 執行刷新操作
484         if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
485             mOnRefreshListener.onRefresh();
486         }
487     }
488 
489     /**
490      * 刷新結束,恢復狀態
491      */
492     public void refreshComplete() {
493         mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
494         mCurrentStatus = STATUS_IDLE;
495         invalidate();
496         updateHeaderTimeStamp();
497 
498         // 200毫秒後處理arrow和progressbar,免得太突兀
499         this.postDelayed(new Runnable() {
500 
501             @Override
502             public void run() {
503                 mArrowImageView.setVisibility(View.VISIBLE);
504                 mProgressBar.setVisibility(View.GONE);
505             }
506         }, 100);
507 
508     }
509 
510     /**
511      * 修改header上的最近更新時間
512      */
513     private void updateHeaderTimeStamp() {
514         // 設置更新時間
	   

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

-Advertisement-
Play Games
更多相關文章
  • 上一節中我們學會瞭如何通過點擊不同按鈕切換頁面,這節專註於完成反饋頁面的功能以及細節動畫。 導入項目 添加新組件 同步新組件 完成頁面佈局 輸入時加動畫效果 彈出日期選擇 直接引用UI頁面 將要學習的demo效果圖如下所示 1. 導入完整項目 本節示例demo請參考下載地址,可以導入到設計器中學習。 ...
  • 體驗效果:http://hovertree.com/texiao/html5/25/效果圖:代碼如下: 關註微信公眾號 何問起 ,賬號ihewenqi ,或者微信掃描下麵二維碼關註。然後發送"橡皮擦"查看效果。參考:使用CSS實現圖片磨砂玻璃效果 轉自:http://hovertree.com/h/ ...
  • 從github上下載的jquery文件是沒有經過壓縮和合併的,根據jquery README.md 中提供的說明在window構建jquery,進行到最後一步運行grunt時會直接編輯器中打開grunt.js,不能夠完成構建,需要先在命令行執行:DOSKEY grunt=grunt.cmd $*。下 ...
  • 介紹 JavaScript 高漲的人氣帶來了很多變化,以至於如今使用其進行網路開發的形式也變得截然不同了。就如同在瀏覽器中一樣,現在我們也可以在伺服器上運行 JavaScript ,從前端跨越到後端,這樣巨大的反差讓人難以想象,因為僅僅在幾年前 Javascript 還如同 Flash 或者 Jav ...
  • 在以前我們的博客文章中,我們討論了在web設計和開發項目中使用Twitter Bootstrap的好處。Twitter Bootstrap也有很多的缺點。讓我們看看這些主要的問題: 1,它不遵循最佳實踐 我們在使用Twitter Bootstrap時遇到的最大問題之一是你的DOM元素上將擁擠大量的類 ...
  • × 目錄 [1]white-space [2]word-wrap [3]word-break 前面的話 CSS3新增了兩個換行屬性word-wrap和word-break。把空白符和換行放在一起說,是因為實際上空白符是包括換行的,且常用的文本不換行是使用的空白符的屬性white-space: now ...
  • 這篇博文基於上一篇iOS_CNBlog項目開發 (基於博客園api開發)所寫. 過了剛好兩個星期, 這次基於上一次的1.0版本, 完善了新的功能, 也修複了之前的一些bug, 算是完成1.1版本吧, 一次進步一下點總是好的, 貼上github:)地址, 喜歡的可以玩弄玩弄 https://githu ...
  • UIAlertView 與 UIActionSheet UIAlertView 樣式 實現 註意 其“確定”按鈕的顏色與“取消”按鈕的外觀一樣(沒有顯示紅色,即 normal) UIActionSheet 樣式 實現 註意 其“確定”按鈕的顏色與“取消”按鈕的外觀不一樣(顯示紅色,即 destruc ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...