Android开源AndroidSideMenu实现抽屉和侧滑菜单

作者:ShouCeng 时间:2023-10-09 09:24:51 

AndroidSideMenu能够让你轻而易举地创建侧滑菜单。需要注意的是,该项目自身并不提供任何创建菜单的工具,因此,开发者可以自由创建内部菜单。

Android开源AndroidSideMenu实现抽屉和侧滑菜单

核心类如下:


/*
* Copyright dmitry.zaicew@gmail.com Dmitry Zaitsev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.agimind.widget;

import java.util.LinkedList;
import java.util.Queue;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.Region.Op;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.FrameLayout;

public class SlideHolder extends FrameLayout {

public final static int DIRECTION_LEFT = 1;
 public final static int DIRECTION_RIGHT = -1;

protected final static int MODE_READY = 0;
 protected final static int MODE_SLIDE = 1;
 protected final static int MODE_FINISHED = 2;

private Bitmap mCachedBitmap;
 private Canvas mCachedCanvas;
 private Paint mCachedPaint;
 private View mMenuView;

private int mMode = MODE_READY;
 private int mDirection = DIRECTION_LEFT;

private int mOffset = 0;
 private int mStartOffset;
 private int mEndOffset;

private boolean mEnabled = true;
 private boolean mInterceptTouch = true;
 private boolean mAlwaysOpened = false;
 private boolean mDispatchWhenOpened = false;

private Queue<Runnable> mWhenReady = new LinkedList<Runnable>();

private OnSlideListener mListener;

public SlideHolder(Context context) {
   super(context);

initView();
 }

public SlideHolder(Context context, AttributeSet attrs) {
   super(context, attrs);

initView();
 }

public SlideHolder(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);

initView();
 }

private void initView() {
   mCachedPaint = new Paint(
         Paint.ANTI_ALIAS_FLAG
         | Paint.FILTER_BITMAP_FLAG
         | Paint.DITHER_FLAG
       );
 }

@Override
 public void setEnabled(boolean enabled) {
   mEnabled = enabled;
 }

@Override
 public boolean isEnabled() {
   return mEnabled;
 }

/**
  *
  * @param direction - direction in which SlideHolder opens. Can be: DIRECTION_LEFT, DIRECTION_RIGHT
  */
 public void setDirection(int direction) {
   closeImmediately();

mDirection = direction;
 }

/**
  *
  * @param allow - if false, SlideHolder won't react to swiping gestures (but still will be able to work by manually invoking mathods)
  */
 public void setAllowInterceptTouch(boolean allow) {
   mInterceptTouch = allow;
 }

public boolean isAllowedInterceptTouch() {
   return mInterceptTouch;
 }

/**
  *
  * @param dispatch - if true, in open state SlideHolder will dispatch touch events to main layout (in other words - it will be clickable)
  */
 public void setDispatchTouchWhenOpened(boolean dispatch) {
   mDispatchWhenOpened = dispatch;
 }

public boolean isDispatchTouchWhenOpened() {
   return mDispatchWhenOpened;
 }

/**
  *
  * @param opened - if true, SlideHolder will always be in opened state (which means that swiping won't work)
  */
 public void setAlwaysOpened(boolean opened) {
   mAlwaysOpened = opened;

requestLayout();
 }

public int getMenuOffset() {
   return mOffset;
 }

public void setOnSlideListener(OnSlideListener lis) {
   mListener = lis;
 }

public boolean isOpened() {
   return mAlwaysOpened || mMode == MODE_FINISHED;
 }

public void toggle(boolean immediately) {
   if(immediately) {
     toggleImmediately();
   } else {
     toggle();
   }
 }

public void toggle() {
   if(isOpened()) {
     close();
   } else {
     open();
   }
 }

public void toggleImmediately() {
   if(isOpened()) {
     closeImmediately();
   } else {
     openImmediately();
   }
 }

public boolean open() {
   if(isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
     return false;
   }

if(!isReadyForSlide()) {
     mWhenReady.add(new Runnable() {

@Override
       public void run() {
         open();
       }
     });

return true;
   }

initSlideMode();

Animation anim = new SlideAnimation(mOffset, mEndOffset);
   anim.setAnimationListener(mOpenListener);
   startAnimation(anim);

invalidate();

return true;
 }

public boolean openImmediately() {
   if(isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
     return false;
   }

if(!isReadyForSlide()) {
     mWhenReady.add(new Runnable() {

@Override
       public void run() {
         openImmediately();
       }
     });

return true;
   }

mMenuView.setVisibility(View.VISIBLE);
   mMode = MODE_FINISHED;
   requestLayout();

if(mListener != null) {
     mListener.onSlideCompleted(true);
   }

return true;
 }

public boolean close() {
   if(!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
     return false;
   }

if(!isReadyForSlide()) {
     mWhenReady.add(new Runnable() {

@Override
       public void run() {
         close();
       }
     });

return true;
   }

initSlideMode();

Animation anim = new SlideAnimation(mOffset, mEndOffset);
   anim.setAnimationListener(mCloseListener);
   startAnimation(anim);

invalidate();

return true;
 }

public boolean closeImmediately() {
   if(!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
     return false;
   }

if(!isReadyForSlide()) {
     mWhenReady.add(new Runnable() {

@Override
       public void run() {
         closeImmediately();
       }
     });

return true;
   }

mMenuView.setVisibility(View.GONE);
   mMode = MODE_READY;
   requestLayout();

if(mListener != null) {
     mListener.onSlideCompleted(false);
   }

return true;
 }

@Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
   final int parentLeft = 0;
   final int parentTop = 0;
   final int parentRight = r - l;
   final int parentBottom = b - t;

View menu = getChildAt(0);
   int menuWidth = menu.getMeasuredWidth();

if(mDirection == DIRECTION_LEFT) {
     menu.layout(parentLeft, parentTop, parentLeft+menuWidth, parentBottom);
   } else {
     menu.layout(parentRight-menuWidth, parentTop, parentRight, parentBottom);
   }

if(mAlwaysOpened) {
     if(mDirection == DIRECTION_LEFT) {
       mOffset = menuWidth;
     } else {
       mOffset = 0;
     }
   } else if(mMode == MODE_FINISHED) {
     mOffset = mDirection*menuWidth;
   } else if(mMode == MODE_READY) {
     mOffset = 0;
   }

View main = getChildAt(1);
   main.layout(
         parentLeft + mOffset,
         parentTop,
         parentLeft + mOffset + main.getMeasuredWidth(),
         parentBottom
       );

invalidate();

Runnable rn;
   while((rn = mWhenReady.poll()) != null) {
     rn.run();
   }
 }

private boolean isReadyForSlide() {
   return (getWidth() > 0 && getHeight() > 0);
 }

@Override
 protected void onMeasure(int wSp, int hSp) {
   mMenuView = getChildAt(0);

if(mAlwaysOpened) {
     View main = getChildAt(1);

if(mMenuView != null && main != null) {
       measureChild(mMenuView, wSp, hSp);
       LayoutParams lp = (LayoutParams) main.getLayoutParams();

if(mDirection == DIRECTION_LEFT) {
         lp.leftMargin = mMenuView.getMeasuredWidth();
       } else {
         lp.rightMargin = mMenuView.getMeasuredWidth();
       }
     }
   }

super.onMeasure(wSp, hSp);
 }

private byte mFrame = 0;

@Override
 protected void dispatchDraw(Canvas canvas) {
   try {
     if(mMode == MODE_SLIDE) {
       View main = getChildAt(1);
       if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
         /*
          * On new versions we redrawing main layout only
          * if it's marked as dirty
          */
         if(main.isDirty()) {
           mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
           main.draw(mCachedCanvas);
       }
       } else {
         /*
          * On older versions we just redrawing our cache
          * every 5th frame
          */
         if(++mFrame % 5 == 0) {
           mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
           main.draw(mCachedCanvas);
         }
       }

/*
        * Draw only visible part of menu
        */

View menu = getChildAt(0);
       final int scrollX = menu.getScrollX();
       final int scrollY = menu.getScrollY();

canvas.save();

if(mDirection == DIRECTION_LEFT) {
         canvas.clipRect(0, 0, mOffset, menu.getHeight(), Op.REPLACE);
       } else {
         int menuWidth = menu.getWidth();
         int menuLeft = menu.getLeft();

canvas.clipRect(menuLeft+menuWidth+mOffset, 0, menuLeft+menuWidth, menu.getHeight());
       }

canvas.translate(menu.getLeft(), menu.getTop());
       canvas.translate(-scrollX, -scrollY);

menu.draw(canvas);

canvas.restore();

canvas.drawBitmap(mCachedBitmap, mOffset, 0, mCachedPaint);
     } else {
       if(!mAlwaysOpened && mMode == MODE_READY) {
         mMenuView.setVisibility(View.GONE);
       }

super.dispatchDraw(canvas);
     }
   } catch(IndexOutOfBoundsException e) {
     /*
      * Possibility of crashes on some devices (especially on Samsung).
      * Usually, when ListView is empty.
      */
   }
 }

private int mHistoricalX = 0;
 private boolean mCloseOnRelease = false;

@Override
 public boolean dispatchTouchEvent(MotionEvent ev) {
   if(((!mEnabled || !mInterceptTouch) && mMode == MODE_READY) || mAlwaysOpened) {
     return super.dispatchTouchEvent(ev);
   }

if(mMode != MODE_FINISHED) {
     onTouchEvent(ev);

if(mMode != MODE_SLIDE) {
       super.dispatchTouchEvent(ev);
     } else {
       MotionEvent cancelEvent = MotionEvent.obtain(ev);
       cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
       super.dispatchTouchEvent(cancelEvent);
       cancelEvent.recycle();
     }

return true;
   } else {
     final int action = ev.getAction();

Rect rect = new Rect();
     View menu = getChildAt(0);
     menu.getHitRect(rect);

if(!rect.contains((int) ev.getX(), (int) ev.getY())) {
       if (action == MotionEvent.ACTION_UP && mCloseOnRelease && !mDispatchWhenOpened) {
         close();
         mCloseOnRelease = false;
       } else {
         if(action == MotionEvent.ACTION_DOWN && !mDispatchWhenOpened) {
           mCloseOnRelease = true;
         }

onTouchEvent(ev);
       }

if(mDispatchWhenOpened) {
         super.dispatchTouchEvent(ev);
       }

return true;
     } else {
       onTouchEvent(ev);

ev.offsetLocation(-menu.getLeft(), -menu.getTop());
       menu.dispatchTouchEvent(ev);

return true;
     }
   }
 }

private boolean handleTouchEvent(MotionEvent ev) {
   if(!mEnabled) {
     return false;
   }

float x = ev.getX();

if(ev.getAction() == MotionEvent.ACTION_DOWN) {
     mHistoricalX = (int) x;

return true;
   }

if(ev.getAction() == MotionEvent.ACTION_MOVE) {

float diff = x - mHistoricalX;

if((mDirection*diff > 50 && mMode == MODE_READY) || (mDirection*diff < -50 && mMode == MODE_FINISHED)) {
       mHistoricalX = (int) x;

initSlideMode();
     } else if(mMode == MODE_SLIDE) {
       mOffset += diff;

mHistoricalX = (int) x;

if(!isSlideAllowed()) {
         finishSlide();
       }
     } else {
       return false;
     }
   }

if(ev.getAction() == MotionEvent.ACTION_UP) {
     if(mMode == MODE_SLIDE) {
       finishSlide();
     }

mCloseOnRelease = false;

return false;
   }

return mMode == MODE_SLIDE;
 }

@Override
 public boolean onTouchEvent(MotionEvent ev) {
   boolean handled = handleTouchEvent(ev);

invalidate();

return handled;
 }

private void initSlideMode() {
   mCloseOnRelease = false;

View v = getChildAt(1);

if(mMode == MODE_READY) {
     mStartOffset = 0;
     mEndOffset = mDirection*getChildAt(0).getWidth();
   } else {
     mStartOffset = mDirection*getChildAt(0).getWidth();
     mEndOffset = 0;
   }

mOffset = mStartOffset;

if(mCachedBitmap == null || mCachedBitmap.isRecycled() || mCachedBitmap.getWidth() != v.getWidth()) {
     mCachedBitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
     mCachedCanvas = new Canvas(mCachedBitmap);
   } else {
     mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
   }

v.setVisibility(View.VISIBLE);

mCachedCanvas.translate(-v.getScrollX(), -v.getScrollY());
   v.draw(mCachedCanvas);

mMode = MODE_SLIDE;

mMenuView.setVisibility(View.VISIBLE);
 }

private boolean isSlideAllowed() {
   return (mDirection*mEndOffset > 0 && mDirection*mOffset < mDirection*mEndOffset && mDirection*mOffset >= mDirection*mStartOffset)
       || (mEndOffset == 0 && mDirection*mOffset > mDirection*mEndOffset && mDirection*mOffset <= mDirection*mStartOffset);
 }

private void completeOpening() {
   mOffset = mDirection*mMenuView.getWidth();
   requestLayout();

post(new Runnable() {

@Override
     public void run() {
       mMode = MODE_FINISHED;
       mMenuView.setVisibility(View.VISIBLE);
     }
   });

if(mListener != null) {
     mListener.onSlideCompleted(true);
   }
 }

private Animation.AnimationListener mOpenListener = new Animation.AnimationListener() {

@Override
   public void onAnimationStart(Animation animation) {}

@Override
   public void onAnimationRepeat(Animation animation) {}

@Override
   public void onAnimationEnd(Animation animation) {
     completeOpening();
   }
 };

private void completeClosing() {
   mOffset = 0;
   requestLayout();

post(new Runnable() {

@Override
     public void run() {
       mMode = MODE_READY;
       mMenuView.setVisibility(View.GONE);
     }
   });

if(mListener != null) {
     mListener.onSlideCompleted(false);
   }
 }

private Animation.AnimationListener mCloseListener = new Animation.AnimationListener() {

@Override
   public void onAnimationStart(Animation animation) {}

@Override
   public void onAnimationRepeat(Animation animation) {}

@Override
   public void onAnimationEnd(Animation animation) {
     completeClosing();
   }
 };

private void finishSlide() {
   if(mDirection*mEndOffset > 0) {
     if(mDirection*mOffset > mDirection*mEndOffset/2) {
       if(mDirection*mOffset > mDirection*mEndOffset) mOffset = mEndOffset;

Animation anim = new SlideAnimation(mOffset, mEndOffset);
       anim.setAnimationListener(mOpenListener);
       startAnimation(anim);
     } else {
       if(mDirection*mOffset < mDirection*mStartOffset) mOffset = mStartOffset;

Animation anim = new SlideAnimation(mOffset, mStartOffset);
       anim.setAnimationListener(mCloseListener);
       startAnimation(anim);
     }
   } else {
     if(mDirection*mOffset < mDirection*mStartOffset/2) {
       if(mDirection*mOffset < mDirection*mEndOffset) mOffset = mEndOffset;

Animation anim = new SlideAnimation(mOffset, mEndOffset);
       anim.setAnimationListener(mCloseListener);
       startAnimation(anim);
     } else {
       if(mDirection*mOffset > mDirection*mStartOffset) mOffset = mStartOffset;

Animation anim = new SlideAnimation(mOffset, mStartOffset);
       anim.setAnimationListener(mOpenListener);
       startAnimation(anim);
     }
   }
 }

private class SlideAnimation extends Animation {

private static final float SPEED = 0.6f;

private float mStart;
   private float mEnd;

public SlideAnimation(float fromX, float toX) {
     mStart = fromX;
     mEnd = toX;

setInterpolator(new DecelerateInterpolator());

float duration = Math.abs(mEnd - mStart) / SPEED;
     setDuration((long) duration);
   }

@Override
   protected void applyTransformation(float interpolatedTime, Transformation t) {
     super.applyTransformation(interpolatedTime, t);

float offset = (mEnd - mStart) * interpolatedTime + mStart;
     mOffset = (int) offset;

postInvalidate();
   }

}

public static interface OnSlideListener {
   public void onSlideCompleted(boolean opened);
 }

}

使用:


package com.agimind.sidemenuexample;

import com.agimind.widget.SlideHolder;

import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.app.ActionBar;
import android.app.Activity;

public class MainActivity extends Activity {

private SlideHolder mSlideHolder;

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

mSlideHolder = (SlideHolder) findViewById(R.id.slideHolder);
   // mSlideHolder.setAllowInterceptTouch(false);
   // mSlideHolder.setAlwaysOpened(true);
   /*
    * toggleView can actually be any view you want. Here, for simplicity,
    * we're using TextView, but you can easily replace it with button.
    *
    * Note, when menu opens our textView will become invisible, so it quite
    * pointless to assign toggle-event to it. In real app consider using UP
    * button instead. In our case toggle() can be replaced with open().
    */

ActionBar actionBar = getActionBar();
   actionBar.setDisplayShowHomeEnabled(true);
   actionBar.setHomeButtonEnabled(true);

View toggleView = findViewById(R.id.textView);
   toggleView.setOnClickListener(new View.OnClickListener() {

@Override
     public void onClick(View v) {
       mSlideHolder.toggle();
     }
   });
 }

@Override
 public boolean onOptionsItemSelected(MenuItem item) {
   switch (item.getItemId()) {
   case android.R.id.home:
     mSlideHolder.toggle();
     break;

default:
     break;
   }
   return super.onOptionsItemSelected(item);
 }
}

布局如下:


<com.agimind.widget.SlideHolder xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/slideHolder"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 tools:context=".MainActivity" >

<ScrollView
   android:layout_width="200dp"
   android:layout_height="fill_parent"
   android:background="@android:color/black" >

<LinearLayout
     android:layout_width="200dp"
     android:layout_height="wrap_content"
     android:orientation="vertical" >

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />

<Button
       android:layout_width="fill_parent"
       android:layout_height="wrap_content"
       android:text="@string/menu_settings" />
   </LinearLayout>
 </ScrollView>

<RelativeLayout
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" >

<TextView
     android:id="@+id/textView"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_centerHorizontal="true"
     android:layout_centerVertical="true"
     android:text="@string/swipe"
     android:textSize="25sp" />

</RelativeLayout>

</com.agimind.widget.SlideHolder>

下载:AndroidSideMenu

标签:Android,AndroidSideMenu,菜单
0
投稿

猜你喜欢

  • Java SSM配置文件案例详解

    2022-10-14 09:43:19
  • c# socket编程udp客户端实现代码分享

    2023-06-16 05:03:31
  • 基于Avalonia实现自定义弹窗的示例详解

    2022-02-27 16:30:45
  • C# JWT权限验证的实现

    2022-11-24 00:57:13
  • SpringBoot集成POI导出Execl表格之统一工具类

    2023-06-12 09:55:51
  • Flutter Navigator路由传参的实现

    2021-12-10 04:46:58
  • Java执行SQL脚本文件到数据库详解

    2023-08-08 08:30:00
  • java Swing组件setBounds()简单用法实例分析

    2023-11-23 13:35:54
  • Mybatis-Plus环境配置与入门案例分析

    2022-02-05 01:15:01
  • @Autowired注解注入的xxxMapper报错问题及解决

    2022-10-01 10:31:02
  • C# 系统热键注册实现代码

    2021-10-07 00:12:14
  • C# 重写Notification提示窗口的示例代码

    2021-12-26 19:57:59
  • Springboot整合Netty实现RPC服务器的示例代码

    2023-07-14 11:35:35
  • Entity Framework模型优先与实体对象查询

    2022-11-18 07:19:36
  • Java Swing实现窗体添加背景图片的2种方法详解

    2021-10-26 19:01:18
  • android多开器解析与检测实现方法示例

    2022-04-01 11:43:15
  • Android studio点击跳转WebView详解

    2022-12-12 05:11:17
  • Android修改源码解决Alertdialog触摸对话框边缘消失的问题

    2021-12-23 23:29:30
  • C# 索引器的使用教程

    2022-08-25 05:11:59
  • Mybatis-plus配置分页插件返回统一结果集

    2022-05-27 19:15:06
  • asp之家 软件编程 m.aspxhome.com