当前位置: 编程技术>移动开发
本页文章导读:
▪便利实用的下拉刷新控件,支持ScrollView、AbsListView 方便实用的下拉刷新控件,支持ScrollView、AbsListView
最近要做一个下拉刷新的功能,网上找了很多例子,也看了一些开源的下拉刷新项目,但是小例子比较简单,效果和稳定性都差强人意,而.........
▪ 写在20110613:vendor机制、批改应用编译.mk文件、USB调试 写在20110613:vendor机制、修改应用编译.mk文件、USB调试
1.Vendor机制: 在主分支上建立分量,达到差异化编译的目的,主要体现在***_BUILD和***_CUSTOM两个文件,顾名思义,前者是差异化按.........
▪ 写在20110616:FM主要功能、两个定做、Handler、消息队列 写在20110616:FM主要功能、两个定制、Handler、消息队列
1..txt文本文件短信支持发送2..txt/网页书签的Mimetype均为text/plain,如何区分呢?3.两个定制:Flash U 同时支持内外部存储,不同存储设.........
[1]便利实用的下拉刷新控件,支持ScrollView、AbsListView
来源: 互联网 发布时间: 2014-02-18
方便实用的下拉刷新控件,支持ScrollView、AbsListView
最近要做一个下拉刷新的功能,网上找了很多例子,也看了一些开源的下拉刷新项目,但是小例子比较简单,效果和稳定性都差强人意,而开源的项目又太庞大,看起来耗时费劲,所以只好综合一下各处的代码掌握其原理,自己实现一套下拉刷新功能。
该控件特点:
1.子控件必须是一个ScrollView或ListView;
2.支持自定义下拉布局;
3.自定义下拉布局可以不用处理下拉的各种状态(只需要实现几个接口即可),也可以自己处理各种下拉的状态。
先来看看效果图:
上代码:
首先看如何使用:
1.使用的布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.example.pulldown.PullDownScrollView android:id="@+id/refresh_root" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:background="#161616" android:orientation="vertical" > <ScrollView android:id="@+id/scrollview" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scrollbars="none" > <LinearLayout android:id="@+id/mainView" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#1f1f1f" android:orientation="vertical" > <!-- 自已的布局 --> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="18sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="18sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="18sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:gravity="center" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="18sp" /> </LinearLayout> </ScrollView> </com.example.pulldown.PullDownScrollView> </LinearLayout>
2.UI使用:
首先,Activity实现接口:
implements RefreshListener
部分代码如下:
package com.example.pulldown; import com.example.pulldown.PullDownScrollView.RefreshListener; import android.os.Bundle; import android.os.Handler; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity implements RefreshListener{ private PullDownScrollView mPullDownScrollView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPullDownScrollView = (PullDownScrollView) findViewById(R.id.refresh_root); mPullDownScrollView.setRefreshListener(this); mPullDownScrollView.setPullDownElastic(new PullDownElasticImp(this)); } @Override public void onRefresh(PullDownScrollView view) { new Handler().postDelayed(new Runnable() { @Override public void run() { // TODO Auto-generated method stub mPullDownScrollView.finishRefresh("上次刷新时间:12:23"); } }, 2000); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } }
3.再来看看控件代码:
import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.widget.AbsListView; import android.widget.LinearLayout; import android.widget.ScrollView; /** * @author xwangly@163.com * @date 2013-7-9 * */ public class PullDownScrollView extends LinearLayout { private static final String TAG = "PullDownScrollView"; private int refreshTargetTop = -60; private int headContentHeight; private RefreshListener refreshListener; private RotateAnimation animation; private RotateAnimation reverseAnimation; private final static int RATIO = 2; private int preY = 0; private boolean isElastic = false; private int startY; private int state; private String note_release_to_refresh = "松开更新"; private String note_pull_to_refresh = "下拉刷新"; private String note_refreshing = "正在更新..."; private IPullDownElastic mElastic; public PullDownScrollView(Context context) { super(context); init(); } public PullDownScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); animation.setInterpolator(new LinearInterpolator()); animation.setDuration(250); animation.setFillAfter(true); reverseAnimation = new RotateAnimation(-180, 0, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); reverseAnimation.setInterpolator(new LinearInterpolator()); reverseAnimation.setDuration(200); reverseAnimation.setFillAfter(true); } /** * 刷新监听 * @param listener */ public void setRefreshListener(RefreshListener listener) { this.refreshListener = listener; } /** * 下拉布局 * @param elastic */ public void setPullDownElastic(IPullDownElastic elastic) { mElastic = elastic; headContentHeight = mElastic.getElasticHeight(); refreshTargetTop = - headContentHeight; LayoutParams lp = new LinearLayout.LayoutParams( LayoutParams.FILL_PARENT, headContentHeight); lp.topMargin = refreshTargetTop; addView(mElastic.getElasticLayout(), 0, lp); } /** * 设置更新提示语 * @param pullToRefresh 下拉刷新提示语 * @param releaseToRefresh 松开刷新提示语 * @param refreshing 正在刷新提示语 */ public void setRefreshTips(String pullToRefresh, String releaseToRefresh, String refreshing) { note_pull_to_refresh = pullToRefresh; note_release_to_refresh = releaseToRefresh; note_refreshing = refreshing; } /* * 该方法一般和ontouchEvent 一起用 (non-Javadoc) * * @see * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent) */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Logger.d(TAG, "onInterceptTouchEvent"); printMotionEvent(ev); if (ev.getAction() == MotionEvent.ACTION_DOWN) { preY = (int) ev.getY(); } if (ev.getAction() == MotionEvent.ACTION_MOVE) { Logger.d(TAG, "isElastic:" + isElastic + " canScroll:"+ canScroll() + " ev.getY() - preY:"+(ev.getY() - preY)); if (!isElastic && canScroll() && (int) ev.getY() - preY >= headContentHeight / (3*RATIO) && refreshListener != null && mElastic != null) { isElastic = true; startY = (int) ev.getY(); Logger.i(TAG, "在move时候记录下位置startY:" + startY); return true; } } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Logger.d(TAG, "onTouchEvent"); printMotionEvent(event); handleHeadElastic(event); return super.onTouchEvent(event); } private void handleHeadElastic(MotionEvent event) { if (refreshListener != null && mElastic != null) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Logger.i(TAG, "down"); break; case MotionEvent.ACTION_UP: Logger.i(TAG, "up"); if (state != IPullDownElastic.REFRESHING && isElastic) { if (state == IPullDownElastic.DONE) { // 什么都不做 setMargin(refreshTargetTop); } if (state == IPullDownElastic.PULL_To_REFRESH) { state = IPullDownElastic.DONE; setMargin(refreshTargetTop); changeHeaderViewByState(state, false); Logger.i(TAG, "由下拉刷新状态,到done状态"); } if (state == IPullDownElastic.RELEASE_To_REFRESH) { state = IPullDownElastic.REFRESHING; setMargin(0); changeHeaderViewByState(state, false); onRefresh(); Logger.i(TAG, "由松开刷新状态,到done状态"); } } isElastic = false; break; case MotionEvent.ACTION_MOVE: Logger.i(TAG, "move"); int tempY = (int) event.getY(); if (state != IPullDownElastic.REFRESHING && isElastic) { // 可以松手去刷新了 if (state == IPullDownElastic.RELEASE_To_REFRESH) { if (((tempY - startY) / RATIO < headContentHeight) && (tempY - startY) > 0) { state = IPullDownElastic.PULL_To_REFRESH; changeHeaderViewByState(state, true); Logger.i(TAG, "由松开刷新状态转变到下拉刷新状态"); } else if (tempY - startY <= 0) { state = IPullDownElastic.DONE; changeHeaderViewByState(state, false); Logger.i(TAG, "由松开刷新状态转变到done状态"); } } if (state == IPullDownElastic.DONE) { if (tempY - startY > 0) { state = IPullDownElastic.PULL_To_REFRESH; changeHeaderViewByState(state, false); } } if (state == IPullDownElastic.PULL_To_REFRESH) { // 下拉到可以进入RELEASE_TO_REFRESH的状态 if ((tempY - startY) / RATIO >= headContentHeight) { state = IPullDownElastic.RELEASE_To_REFRESH; changeHeaderViewByState(state, false); Logger.i(TAG, "由done或者下拉刷新状态转变到松开刷新"); } else if (tempY - startY <= 0) { state = IPullDownElastic.DONE; changeHeaderViewByState(state, false); Logger.i(TAG, "由DOne或者下拉刷新状态转变到done状态"); } } if (tempY - startY > 0) { setMargin((tempY - startY)/2 + refreshTargetTop); } } break; } } } /** * */ private void setMargin(int top) { LinearLayout.LayoutParams lp = (LayoutParams) mElastic.getElasticLayout() .getLayoutParams(); lp.topMargin = top; // 修改后刷新 mElastic.getElasticLayout().setLayoutParams(lp); mElastic.getElasticLayout().invalidate(); } private void changeHeaderViewByState(int state, boolean isBack) { mElastic.changeElasticState(state, isBack); switch (state) { case IPullDownElastic.RELEASE_To_REFRESH: mElastic.showArrow(View.VISIBLE); mElastic.showProgressBar(View.GONE); mElastic.showLastUpdate(View.VISIBLE); mElastic.setTips(note_release_to_refresh); mElastic.clearAnimation(); mElastic.startAnimation(animation); Logger.i(TAG, "当前状态,松开刷新"); break; case IPullDownElastic.PULL_To_REFRESH: mElastic.showArrow(View.VISIBLE); mElastic.showProgressBar(View.GONE); mElastic.showLastUpdate(View.VISIBLE); mElastic.setTips(note_pull_to_refresh); mElastic.clearAnimation(); // 是由RELEASE_To_REFRESH状态转变来的 if (isBack) { mElastic.startAnimation(reverseAnimation); } Logger.i(TAG, "当前状态,下拉刷新"); break; case IPullDownElastic.REFRESHING: mElastic.showArrow(View.GONE); mElastic.showProgressBar(View.VISIBLE); mElastic.showLastUpdate(View.GONE); mElastic.setTips(note_refreshing); mElastic.clearAnimation(); Logger.i(TAG, "当前状态,正在刷新..."); break; case IPullDownElastic.DONE: mElastic.showProgressBar(View.GONE); mElastic.clearAnimation(); // arrowImageView.setImageResource(R.drawable.goicon); // tipsTextview.setText("下拉刷新"); // lastUpdatedTextView.setVisibility(View.VISIBLE); Logger.i(TAG, "当前状态,done"); break; } } private void onRefresh() { // downTextView.setVisibility(View.GONE); // scroller.startScroll(0, i, 0, 0 - i); // invalidate(); if (refreshListener != null) { refreshListener.onRefresh(this); } } /** * */ @Override public void computeScroll() { // if (scroller.computeScrollOffset()) { // int i = this.scroller.getCurrY(); // LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView // .getLayoutParams(); // int k = Math.max(i, refreshTargetTop); // lp.topMargin = k; // this.refreshView.setLayoutParams(lp); // this.refreshView.invalidate(); // invalidate(); // } } /** * 结束刷新事件,UI刷新完成后必须回调此方法 * @param text 一般传入:“上次更新时间:12:23” */ public void finishRefresh(String text) { if (mElastic == null) { Logger.d(TAG, "finishRefresh mElastic:" + mElastic); return; } state = IPullDownElastic.DONE; mElastic.setLastUpdateText(text); changeHeaderViewByState(state,false); Logger.i(TAG, "执行了=====finishRefresh"); mElastic.showArrow(View.VISIBLE); mElastic.showLastUpdate(View.VISIBLE); setMargin(refreshTargetTop); // scroller.startScroll(0, i, 0, refreshTargetTop); // invalidate(); } private boolean canScroll() { View childView; if (getChildCount() > 1) { childView = this.getChildAt(1); if (childView instanceof AbsListView) { int top = ((AbsListView) childView).getChildAt(0).getTop(); int pad = ((AbsListView) childView).getListPaddingTop(); if ((Math.abs(top - pad)) < 3 && ((AbsListView) childView).getFirstVisiblePosition() == 0) { return true; } else { return false; } } else if (childView instanceof ScrollView) { if (((ScrollView) childView).getScrollY() == 0) { return true; } else { return false; } } } return canScroll(this); } /** * 子类重写此方法可以兼容其它的子控件,目前只兼容AbsListView和ScrollView * @param view * @return */ public boolean canScroll(PullDownScrollView view) { return false; } private void printMotionEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Logger.d(TAG, "down"); break; case MotionEvent.ACTION_MOVE: Logger.d(TAG, "move"); break; case MotionEvent.ACTION_UP: Logger.d(TAG, "up"); default: break; } } /** * 刷新监听接口 */ public interface RefreshListener { public void onRefresh(PullDownScrollView view); } }
4.接口:
import android.view.View; import android.view.animation.Animation; /** * @author xwangly@163.com * @date 2013-7-10 * 下拉控件接口 */ public interface IPullDownElastic { public final static int RELEASE_To_REFRESH = 0; public final static int PULL_To_REFRESH = 1; public final static int REFRESHING = 2; public final static int DONE = 3; public View getElasticLayout(); public int getElasticHeight(); public void showArrow(int visibility); public void startAnimation(Animation animation); public void clearAnimation(); public void showProgressBar(int visibility); public void setTips(String tips); public void showLastUpdate(int visibility); public void setLastUpdateText(String text); /** * 可以不用实现此方法,PullDownScrollView会处理ElasticLayout布局中的状态 * 如果需要特殊处理,可以实现此方法进行处理 * * @param state @see RELEASE_To_REFRESH * @param isBack 是否是松开回退 */ public void changeElasticState(int state, boolean isBack); }
5.默认实现:
import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; /** * @author xwangly@163.com * @date 2013-7-10 * 默认下拉控件布局实现 */ public class PullDownElasticImp implements IPullDownElastic { private View refreshView; private ImageView arrowImageView; private int headContentHeight; private ProgressBar progressBar; private TextView tipsTextview; private TextView lastUpdatedTextView; private Context mContext; public PullDownElasticImp(Context context) { mContext = context; init(); } private void init() { // 刷新视图顶端的的view refreshView = LayoutInflater.from(mContext).inflate( R.layout.refresh_top_item, null); // 指示器view arrowImageView = (ImageView) refreshView .findViewById(R.id.head_arrowImageView); // 刷新bar progressBar = (ProgressBar) refreshView .findViewById(R.id.head_progressBar); // 下拉显示text tipsTextview = (TextView) refreshView.findViewById(R.id.refresh_hint); // 下来显示时间 lastUpdatedTextView = (TextView) refreshView .findViewById(R.id.refresh_time); headContentHeight = Utils.dip2px(mContext, 50); } /** * @return * */ @Override public View getElasticLayout() { return refreshView; } /** * @return * */ @Override public int getElasticHeight() { return headContentHeight; } /** * @param show * */ @Override public void showArrow(int visibility) { arrowImageView.setVisibility(visibility); } /** * @param animation * */ @Override public void startAnimation(Animation animation) { arrowImageView.startAnimation(animation); } /** * * */ @Override public void clearAnimation() { arrowImageView.clearAnimation(); } /** * @param show * */ @Override public void showProgressBar(int visibility) { progressBar.setVisibility(visibility); } /** * @param tips * */ @Override public void setTips(String tips) { tipsTextview.setText(tips); } /** * @param show * */ @Override public void showLastUpdate(int visibility) { lastUpdatedTextView.setVisibility(visibility); } /** * @param text * */ public void setLastUpdateText(String text) { lastUpdatedTextView.setText(text); } /** * @param state * @param isBack * */ @Override public void changeElasticState(int state, boolean isBack) { // TODO Auto-generated method stub } }
6.默认实现的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="-50.0dip" android:orientation="vertical" > <LinearLayout android:layout_width="fill_parent" android:layout_height="0.0dip" android:layout_weight="1.0" android:gravity="center" android:orientation="horizontal" > <!-- 箭头图像、进度条 --> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="30dip" > <!-- 箭头 --> <ImageView android:id="@+id/head_arrowImageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="/blog_article/@drawable/goicon/index.html" /> <!-- 进度条 --> <ProgressBar android:id="@+id/head_progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" /> </FrameLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <!-- 提示 --> <TextView android:id="@+id/refresh_hint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下拉刷新" android:textColor="#f2f2f2" android:textSize="16sp" /> <!-- 最近更新 --> <TextView android:id="@+id/refresh_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="上次更新" android:textColor="#b89766" android:textSize="10sp" /> </LinearLayout> </LinearLayout> </LinearLayout>
6.图片资源:
@drawable/goicon
完结
修改:从原工程中独立出来,并附上简单的工程,见附件。
1 楼
freezingsky
19 小时前
写得很好。继续努力!
[2] 写在20110613:vendor机制、批改应用编译.mk文件、USB调试
来源: 互联网 发布时间: 2014-02-18
写在20110613:vendor机制、修改应用编译.mk文件、USB调试
1.Vendor机制:
在主分支上建立分量,达到差异化编译的目的,主要体现在***_BUILD和***_CUSTOM两个文件,顾名思义,前者是差异化按项目分门别类的编译脚本,后者是按项目分类的各应用相关定制的文件抽取存放位置
2.是否把某个应用编译进去,可以在该应用对应的工程目录下的.mk文件中修改脚本
3.ADB设备识别不了:检查手机驱动是否安装、设置中“允许USB调试”选项是否开启。
1.Vendor机制:
在主分支上建立分量,达到差异化编译的目的,主要体现在***_BUILD和***_CUSTOM两个文件,顾名思义,前者是差异化按项目分门别类的编译脚本,后者是按项目分类的各应用相关定制的文件抽取存放位置
2.是否把某个应用编译进去,可以在该应用对应的工程目录下的.mk文件中修改脚本
3.ADB设备识别不了:检查手机驱动是否安装、设置中“允许USB调试”选项是否开启。
[3] 写在20110616:FM主要功能、两个定做、Handler、消息队列
来源: 互联网 发布时间: 2014-02-18
写在20110616:FM主要功能、两个定制、Handler、消息队列
1..txt文本文件短信支持发送
2..txt/网页书签的Mimetype均为text/plain,如何区分呢?
3.两个定制:Flash U 同时支持内外部存储,不同存储设备之间相互拷贝等操作
DRM 用到第三方API或者自研的API
4.MessageQueue和Handler、Looper
4.1 Message
Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。
4.2 Handler
Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。
4.3 Message Queue
Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。
4.4 Looper
Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,查看API Doc
1..txt文本文件短信支持发送
2..txt/网页书签的Mimetype均为text/plain,如何区分呢?
3.两个定制:Flash U 同时支持内外部存储,不同存储设备之间相互拷贝等操作
DRM 用到第三方API或者自研的API
4.MessageQueue和Handler、Looper
4.1 Message
Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。
4.2 Handler
Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。
4.3 Message Queue
Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。
4.4 Looper
Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,查看API Doc
最新技术文章: