您的位置:首页 > 移动开发 > Android开发

Android PullToRefreshView自定义下拉刷新控件

2015-05-16 14:53 603 查看
MyPullToRefreshView继承自LinearLayout,布局为vertical,该容器中包含三个子view,这三个view从上到下依次排列在LinearLayout中。

效果图如下:



下图中蓝色部分是充满屏幕的,HeaderView在ListView的上方,在代码中动态添加进来,使其底部Y轴坐标刚好为0,FooterView在ListView的下方,也在代码中动态添加进来,该View的TopMargin刚好为整个布局的高度。

首先看一下该控件的使用:

1.在xml中配置

<span style="font-size:14px;"><com.example.testpulltorefreshview.PullToRefreshView
android:id="@+id/my_pull_to_refresh_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</com.example.testpulltorefreshview.PullToRefreshView></span>
2.在代码中为ListView设置适配器,添加数据

<span style="font-size:14px;">ListView list=(ListView)findViewById(R.id.listView);
list.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,new String[]{"sss"}));</span>
模块一:控件测绘

在代码中动态添加HeaderView时,需要首先测量HeaderView的高度h,因为我们要是其TopMargin设置为0-h,这样才能使得HeaderView的底部刚好Y轴坐标为0。同理,FooterView的topMargin应该设置为整个控件的高度getHeight。为了简化添加FooterView的处理,只需使ListView充满整个控件后再添加FooterView。

在构造函数中强制设置LinearLayout排列方法为vertical,然后在构造函数中添加HeaderView,因为此时ListView还未被添加进来。

<span style="font-size:14px;">public MyPullToRefreshView(Context context,AttributeSet attrs){
super(context,attrs);
mContext=context;
this.setOrientation(LinearLayout.VERTICAL);//强制设置控件的排列方向为vertical
init();
}</span>
构造函数中通过init函数动态添加HeaderView,注意此时在xml文件中设置的ListView还未被添加到控件中。所以此时添加的HeaderView是LinearLayout容器中的第一个控件。

<span style="font-size:14px;">public void init(){
inflater=LayoutInflater.from(mContext);
...
此时创建动画资源,后面添加
...
addHeaderView();
}</span>
在addHeaderView函数中创建View,并测量其高度:

<span style="font-size:14px;">public void addHeaderView(){
mHeaderView=inflater.inflate(R.layout.refresh_header,this,false);
...
执行findViewById,初始化layout中的View实例
...
measureView(mHeaderView);//测量HeaderView的高度
mHeaderHeight=mHeaderView.getMeasuredHeight();//获得测量的HeaderView高度
LayoutParams params=new LayoutInflater(LayoutParams.MATCH_PARENT,mHeaderHeight);//创建HeaderView的布局参数LayoutParams
params.topMargin=-mHeaderHeight;//设置HeaderView的topMargin为-mHeaderHeight,这样HeaderView的底部Y轴坐标为0。
addView(mHeaderView,params);
}</span>
我们需要分析measureView函数,搞清楚如何可以测量HeaderView的高度。在此之前首先需要看一下R.layout.refresh_header代码。
<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_pull_to_refresh_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="5dp">"

<ImageView
android:id="@+id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="60dp"
android:contentDescription="@string/app_name"
android:src="@drawable/arrow_up"/>

<ProgressBar
android:id="@+id/pull_to_refresh_progress"
style="?android:attr/progressBarStyleInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="60dp"
android:indeterminate="true"
android:visibility="gone"/>

<TextView
android:id="@+id/tv_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_centerHorizontal="true"
android:text="@string/pull_to_refresh" />

<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/tv_state"
android:layout_below="@+id/tv_state"
android:text="更新于。。。。"
android:visibility="gone"/>

</RelativeLayout></span>
在使用上可以设置HeaderView的高度为具体的值,如
<span style="font-size:14px;"><RelativeLayout
android:layout_width=match_parent
android:layout_height="20dp"
...>
...
</RelativeLayout></span>
也可以设置HeaderView的高度刚好包裹内部的View,即wrap_content.
<span style="font-size:14px;"><RelativeLayout
android:layout_width=match_parent
android:layout_height="wrap_content"
...>
...
</RelativeLayout></span>
所以在测量控件高度是要针对两种情况考虑。
<span style="font-size:14px;">public void measureView(View child){
ViewGroup.LayoutParams params=child.getLayoutParams();
if(params==null){
params=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT);
}
//如果params的值不是具体的dp值,那么等价于MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED),如果params为具体的dp值,那么等价于MeasureSpec.makeMeasureSpec(dp值,MesureSpec.EXACTLY)
int widthMeasureSpec=getChildMeasureSpec(0,0+0,params.width);
int heightMeasureSpec;
if(params.height>0){//为具体的dp值
heightMeasureSpec=MeasureSpec.makeMeasureSpec(params.height,MeasureSpec.EXACTLY);
}
else{
heightMeasureSpec=MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
}
child.measure(widthMeasureSpec,heightMeasureSpec);
}</span>
在添加完成HeaderView后该添加FooterView了。FooterView应该添加到整个控件的最后。Activity中执行setContentView来解析xml构建View
Tree。当解析完成当前的View后会回调View的onFinishInflate,在该函数中添加FooterView,就能保证FooterView被添加到了整个控件的尾部。
<span style="font-size:14px;">@Override
protected void onFinishInflate(){
super.onFinishInflate();
addFooterView();
initContentAdapterView();
}
public void addFooterView(){
mFooterView=inflater.inflate(R.layout.refresh_footer,this,false);
...
实例化view变量
...
measureView(mFooterView);
mFooterHeight=mFooterView.getMeasuredHeight();
LayoutParams params=new LayoutParams(LayoutParams.MATCH_PARENT,mFooterHeight);
addFooterView(mFooterView,params);
}</span>
模块二:触摸分发

上面完成了控件中View的添加和布局,下面需要实现整个控件的触摸分发模块了。

下面先简单总结Android的触摸分发机制。

public boolean dispatchTouchEvent(MotionEvent ev) 分发触摸

public boolean onInterceptTouchEvent(MotionEvent ev) 拦截触摸

public boolean onTouchEvent(MotionEvent ev) 处理触摸

ViewGroup包含以上三个函数,Activity和View只包含dispatchTouchEvent和onTouchEvent两个函数。触摸事件的分发是从Activity开始的,再到ViewGroup,ViewGroup在向下传到View中。

<span style="font-size:14px;">//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
onUserInteraction();//该方法内部是空的,当每次传入触摸事件Action_down时,都会调用该方法,后续的action_move和action_up不会调用该方法。我们可以重载该方法
}
if(getWindow().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}</span>
Activity将触摸分发给了DecorView(ViewGroup),DecorView在将触摸分发该ViewGroup或View,ViewGroup将触摸分发到onInterceptTouchEvent函数中,若该函数返回true,那么后续的触摸事件就直接由ViewGroup的onTouchEvent处理,不会传到ViewGroup中的View了,若返回的是false,那么进一步将触摸传到View中。在View中再执行dispatchTouchEvent。
<span style="font-size:14px;">//View.java
public boolean dispatchTouchEvent(MotionEvent event){
...
if(li!=null && li.mOnTouchListener!=null &&...&& li.mOnTouchListener.onTouch(this,event)){
result=true;
}
if(!result && onTouchEvent(event)){
result=true;
}
...
}</span>
若View的onTouchListener不为空,执行onTouchListener的onTouch,否则执行View的onToucEvent函数。我们在MyPullToRefreshView中重载onInterceptTouchEvent函数。若onInterceptTouchEvent返回true,那么后续的触摸将在ViewGroup的onTouchEvent函数中处理了,不会分发给ViewGroup中的HeaderView,ListView和FooterView了。

我们现在需要先分析清楚在什么情况下需要拦截触摸,不让触摸传递子View中。

1.滑动位移过小,小于5,那么不会拦截触摸。

2.当ListView未被滑到最顶端或最底端的情况下,是不需要拦截触摸的,这时要让ListView可以自由滑动。

3.当ListView被滑动到最顶端,并且继续滑动的方向是向下的,那么就需要拦截触摸,然后在ViewGroup的onTouchEvent中使HeaderView向下移动。

4.当Listview被滑动到最底端,并且继续滑动的方向是向上的,那么就需要拦截触摸,然后在VrewGroup的onTouchEvent中使FooterView向上移动。

在这里我们要讨论一下填充控件的滑动视图ListView和ScrollView。

<span style="font-size:14px;">private AdapterView<?> mAdapterView;
private ScrollView mSrollView;</span>
在onFinshInflate函数中实例化:
<span style="font-size:14px;">@Override
protected void onFinishInflate(){
...
int childCount=this.getChildCount();
if(childCount<3){
//IllegalArgumentException继承自RuntimeException
throw new IllegalArgumentException("this layout must contain 3 child views,and AdapterView or"
+ " ScrollView must in the second position! ");
}
//instanceof是一个二元操作符,作用是判断操作符左侧的对象是否是右侧的类的实例
if(this.getChildAt(1) instanceof AdapterView<?>){
mAdapterView=(AdapterView<?>) this.getChildAt(1);
}
else if(this.getChildAt(1) instanceof ScrollView){
mScrollView=(ScrollView) this.getChildAt(1);
}
if(mAdapterView==null && mScrollView==null){
throw new IllegalArgumentException("must contain a AdapterView or ScrollView in the layout");
}
}</span>
在onInterceptTouchEvent中对ACTION_DOWN不拦截,我们需要拦截的是上述几种情况下的ACTION_MOVE,首先ACTION_DOWN会被传到View中处理,后续的不满足以上情况要求的部分ACTION_MOVE也会传到View中处理。一旦我们在onInterceptTouchEvent中拦截了ACTION_MOVE,那么之前处理触摸事件的view会接收到ACTION_CANCEL消息,之后所有的ACTION_MOVE全部直接被传到ViewGroup的onTouchEvent中,不会再到onInterceptTouchEvent中判读是否需要拦截了。

<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event){
int y=(int)e.getRawY();                       //getRawX/getRawY是相对于屏幕的绝对距离,getX/getY是相对于View的相对距离
switch(e.getAction){
case MotionEvent.ACTION_DOWN:
mLastActionDown=y;                         //mLastActionDown记录了上一次ACTION_DOWN的Y轴值
break;
case MotionEvent.ACTION_MOVE:
int delta=y-mLastActionDown;
if(delta>=-5 && delta<=5){return false;}   //滑动位移过小,不消费该触摸
if(isRefreshViewScroll(delta)){            //在isRefreshViewSrcoll函数中判断是否符合上述情况:ListView已滑到最顶端或最底端
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return false;
}</span>
在下拉和上拉的过程中都有三个状态:

1.下拉时HeaderView未完全显示出来,此时释放不会导致刷新;

2.下拉时HeaderView已完全显示出来,此时释放会导致刷新;

3.释放,正在刷新。

在滑动方向上又分下拉刷新和上拉加载两种。

private static int PULL_DOWN_STATE=0; //两种方向

private static int PULL_UP_STATE=1;

pribate int mPullState;//当前滑动的方向

private static int PULL_TO_REFRESH=2;//滑动时状态

private static int RELEASE_TO_REFRESH=3;

private static int REFRESHING=4;

private int mHeaderState;//Headerview当前的状态

private int mFooterState;//FooterView当前的状态

<span style="font-size:14px;">boolean isRefreshViewScroll(int delta){
if(mHeaderState==REFRESHING || mFooterState==REFRESHING){
return false;  //正在刷新时是否可以滑动控件识实际情况而定。
}
if(delta>0){
mPullState=PULL_DOWN_STATE;
}else{
mPullState=PULL_UP_STATE;
}
if(mAdapterView!=NULL){
if(delta>0){
View child=mAdapterView.getChildAt(0);
if(child==null){
//listview中无数据,不拦截
return false;
}
if(child.getTop()==0 && mAdapterView.getFirstVisiblePosition()==0){
mPullState=PULL_DOWN_STATE;
return ture;
}
//如果设置了ListView的padding或item的top,在此处添加对其的处理
}
else if(delta<0){
View lastChild=mAdapterView.getChildAt(mAdapterView.getChildCount()-1);
if(lastChild==null){
return false;
}
if(lastChild.getBottom()<=getHeight() mAdapterView.getLastVisiblePosition()==mAdapterView.getChildCount()-1){
mPullState=PULL_UP_STATE;
return true;
}
//如果设置了ListView的padding或item的bottom,在此处添加对其的处理
}
}
if(mScrollView!=null){
if(delta>0 && mScrollView.getScrollY()==0){
mPullState=PULL_UP_STATE;
return true;
}
else if(delta<0 && mScrollView.getScrollY<=getHeight()-mScrollView.getChildAt(0).getHeight()){
mPullState=PULL_DOWN_STATE;
return true;
}
}
return false;
}</span>
当isRefreshViewScroll返回true,那么后续的ACTION_MOVE就直接分发到ViewGroup的onTouchEvent中处理。要注意的是此时的Math.abs(ACTION_MOVE-

mLastActionDown)是大于5的。
<span style="font-size:14px;">@Override
public boolean onTouchEvent(MotionEvent event){
int y=event.getRawY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//在onInterceptTouchEvent中已经记录
break;
case MotionEvent.ACTION_MOVE:
int delta=y-mLastActionDown;
if(mPullState==PULL_DOWN_STATE){
headerPrepareToRefresh(delta);
}
else if(mPullState==PULL_UP_STATE){
footerPrepareToRefresh(delta)
}
mLastActionDown=y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
.....
break;
}
}</span>
在headerPrepareToRefresh或footerPrepareToRefresh中改变HeaderView或FooterView的位置,以及在HeaderView或FooterView中显示提示信息。

首先来看一下如何改变HeaderView或FooterView的位置。

<span style="font-size:14px;">public int changingHeaderViewTopMargin(int delta){
LayoutParams params=(LayoutParams)mHeaderView.getLayoutParams();
int newTopMargin=(int) (params.topMargin+deltaY*0.6);
params.topMargin=newTopMargin;
mHeaderView.setLayoutParams(params);
invalidate();
return newTopMargin;
}</span>
很简单,直接改变HeaderView的TopMargin就可以,然后调用invalidate来进行重绘。

当HeaderView完全显示出来后要将Headerview中的箭头旋转向上,此时通过旋转动画RotateAnimation来实现。

我们在构造函数中调用init来初始化动画资源。

<span style="font-size:14px;">public void init(){
inflater=LayoutInflater.from(mContext);
mFlipAnimation=new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mFlipAnimation.setInterpolator(new LinearInterpolator());
mFlipAnimation.setDuration(250);
mFlipAnimation.setFillAfter(true);//这样动画播放完会停留在最后一帧
mReverseFlipAnimation=new RotateAnimation(180,0,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
mReverseFlipAnimation.setDuration(250);
mReverseFlipAnimation.setFillAfter(true);
addHeaderView();//在构造函数中addView,可以保证是第一个添加到LinearLayout中的,此时xml中设置的子View还未被添加到LinearLayout中
}

public void headerPrepareToRefresh(int delta){
int newTopMargin=changingHeaderViewTopMargin(delta);
//根据newTopMargin的值判断是否播放动画
if(newTopMargin>=0 && mHeaderState!=RELEASE_TO_REFRESH){
mHeaderText.setText("释放完成刷新");
mHeaderImage.clearAnimation();
mHeaderImage.startAnimation(mFlipAnimation);
mHeaderState=RELEASE_TO_REFRESH;
}else if(newTopMargin<0 && mHeaderState!=PULL_TO_REFRESH){
mHeaderText.setText("下拉刷新");
mHeaderImage.clearAnimation();
mHeaderImage.startAnimation(mReverseFlipAnimation);
mHeaderState=PULL_TO_REFRESH;
}
}
public void footerPrepareToRefresh(int delta){
int newTopMargin=changingHeaderViewTopMargin(delta);
//根据newTopMargin的值判断是否播放动画
if(Math.abs(newTopMargin)>=(mHeaderHeight+mFooterHeight) && mFooterState!=RELEASE_TO_REFRESH){
mFooterText.setText("释放完成刷新");
mFooterState=RELEASE_TO_REFRESH;
}
else if(Math.abs(newTopMargin)<(mHeaderHeight+mFooterHeight) && mFooterState!=PULL_TO_REFRESH){
mFooterText.setText("上拉刷新");
mFooterState=PULL_TO_REFRESH;
}
}</span>
当滑动释放时,会分发ACTION_UP到ViewGroup的onTouchEvent中,此时我们就要进入更新状态了。
<span style="font-size:14px;">@Override
public boolean onTouchEvent(MotionEvent event){
int y=event.getRawY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//在onInterceptTouchEvent中已经记录
break;
case MotionEvent.ACTION_MOVE:
....
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//首先要通过TopMargin来查看HeaderView或FooterView是否显示完全了,否则不刷新
int topMargin=mHeaderView.getTopMargin();
if(topMargin>0 && mPullState==PULL_DOWN_STATE){
onHeaderRefreshing();
}
else if(topMargin<-mHeaderHeight-mFooterHeight && mPullState==PULL_UP_STATE){
onFooterRefreshing();
}else{
mHeaderView.setTopMargin(-mHeaderHeight);//不刷新
}
break;
}
}</span>
在onHeaderRefreshing/onFooterRefreshing中要完成显示加载进度,然后调用接口中的函数去执行一些耗时任务。

<span style="font-size:14px;">public void onHeaderRefreshing(){
mHeaderState=REFRESHING;
setHeaderTopMargin(0);
mHeaderImage.setVisibility(View.GONE);
mHeaderImage.clearAnimation();
mHeaderProgress.setVisibility(View.VISIBLE);
mHeaderText.setText("正在刷新...");
if(mOnHeaderViewListener!=null){
mOnHeaderViewListener.onHeaderRefreshing(this);
}
}

public void onFooterRefreshing(){
mFooterState=REFRESHING;
setHeaderTopMargin(-mHeaderHeight-mFooterHeight);
mFooterImage.setVisibility(View.GONE);
mFooterImage.clearAnimation();
mFooterProgress.setVisibility(View.VISIBLE);
mFooterText.setText("正在加载中...");
if(mOnFooterViewListener!=null){
mOnFooterViewListener.onFooterRefreshing(this);
}
}</span>


模块三:接口设计



我们的下拉刷新控件是观察者模式中的主题,当在下拉和上拉释放时会通知观察者。观察者会进行一些耗时处理,然后回调主题,通知其完成刷新。

1.创建接口类(该接口类可以作为控件的内部类)

<span style="font-size:14px;">interface OnHeaderViewListener{
public void onHeaderRefreshing(MyPullToRefreshView v);
}
interface OnFooterViewListener{
public void onFooterRefreshing(MyPullToRefreshView v);
}</span>

2.在控件内声明用于外界添加监听的函数
<span style="font-size:14px;">        //首先创建保存监听器(观察者)的变量
private OnHeaderViewListener mOnHeaderViewListener;
private OnFooterViewListener mOnFooterViewListener;
//外界可通过该函数来添加观察者
public void setOnHeaderViewListenr(OnHeaderViewListener listener){
mOnHeaderViewListener=listener;
}
public void setOnFooterViewListener(OnFooterViewListener listener){
mOnFooterViewListener=listener;
}</span>
3.控件在刷新时通知外界执行一些耗时任务
<span style="font-size:14px;">if(mOnHeaderViewListener!=null){
mOnHeaderViewListener.onHeaderRefreshing(this);//将事件源封装在参数中,传给外界,让外界完成耗时任务后回调MyPullToRefreshView.onRefreshComplete结束刷新。
}
if(mOnFooterViewListener!=null){
mOnFooterViewListener.OnFooterViewListener(this);
}</span>
当耗时任务完成后,需要回调MyPullToRefreshView的onHeaderRefreshComplete/onFooterRefreshComplete函数来完成刷新。
<span style="font-size:14px;">public void onHeaderRefreshComplete() { // 完成刷新
setHeaderTopMargin(-mHeaderViewHeight);
mHeaderImage.setVisibility(View.VISIBLE);
mHeaderImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
mHeaderText.setText(R.string.pull_to_refresh_pull_label);
mHeaderProgressBar.setVisibility(View.GONE);
mHeaderState = PULL_TO_REFRESH;
if (mScrollView != null) {
mScrollView.smoothScrollTo(0, 0);
}
}
public void onFooterRefreshComplete() {
setHeaderTopMargin(-mHeaderViewHeight);
mFooterImage.setVisibility(View.VISIBLE);
mFooterImage.setImageResource(R.drawable.icon_host_pull);
mFooterText.setText(R.string.pull_to_refresh_footer_pull_label);
mFooterProgressBar.setVisibility(View.GONE);
mFooterState = PULL_TO_REFRESH;
}</span>
最后来看一下在Activity中的使用。
<span style="font-size:14px;">public class MainActivity extends Activity implements OnFooterViewListener,OnHeaderViewListener {

private MyPullToRefreshView mPullToRefreshView;
private ListView mListView;
private ArrayAdapter<String> arrayAdapter;
private String[] strs={"1111","2222","3333"};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

public void init(){
mPullToRefreshView=(MyPullToRefreshView)findViewById(R.id.my_pull_to_refresh_view);
mListView=(ListView)findViewById(R.id.listView);
arrayAdapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,strs);
mListView.setAdapter(arrayAdapter);
mPullToRefreshView.setOnHeaderViewListener(this);
mPullToRefreshView.setOnFooterViewListener(this);
}

@Override
public void onFooterRefreshing(final MyPullToRefreshView v) {
//向UI的消息队列中投递一个runnable,投递成功返回true,并不代表runnable成功执行,looper可能退出导致runnable被丢弃
v.postDelayed(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
v.onFooterRefreshComplete();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 0);
}

@Override
public void onHeaderRefreshing(final MyPullToRefreshView v) {
v.postDelayed(new Runnable(){
@Override
public void run(){
try {
Thread.sleep(3000);
v.onHeaderRefreshComplete();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0);
}

}</span>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: