一般实现比较复杂的交互效果,都会选择重写ViewGroup,并通过onTouchEvent和onInterceptTouchEvent等实现对各种事件的处理,但对事件的处理是很不容易的一个事情。
本文主要是借助v4包中的ViewDragHelper这个帮助类,来重写ViewGroup,把事件完全交给帮助类去完成,并借助帮助类的几个回调方法来完成各种复杂交互效果,完全避开了事件处理。
对ViewDragHelper的使用,网上有很多资料,在此就不重新介绍了,这里我通过两个实现的例子,以及例子中的注释,来让大家明白怎么轻松实现各种效果。
一、带手柄的侧滑菜单效果HandleDrawerLayout
Android v4包中自带侧滑菜单效果DrawerLayout是很好的一种交互,但是某些需求下,需要多一个类似手柄的东西来拉开和关闭侧滑菜单,用DrawerLayout是尝遍各种方式都无法实现。看以下效果图就明白了
1、布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--内部的布局需要按照1、2、3的顺序,3嵌套4、5布局的结构来布局-->
<com.dway.testwork.viewdrag.HandleDrawerLayout
android:id="@+id/hdl_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--1 内容布局-->
<FrameLayout
android:id="@+id/hdl_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f7f7f7">
</FrameLayout>
<!--2 内容布局上面的半透明布局-->
<View
android:id="@+id/hdl_scrim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#7f000000"/>
<!--3 侧滑菜单布局和手柄布局的父布局-->
<LinearLayout
android:id="@+id/hdl_drawer_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<!--4 侧滑菜单布局-->
<FrameLayout
android:id="@+id/hdl_menu_view"
android:layout_width="245dp"
android:layout_height="match_parent"
android:background="#2d3144">
</FrameLayout>
<!--5 侧滑手柄布局-->
<RelativeLayout
android:id="@+id/hdl_handle_view"
android:layout_width="40dp"
android:layout_height="173dp"
android:gravity="center"
android:layout_gravity="center_vertical"
android:background="#3fff00ff">
<TextView
android:id="@+id/hdl_handle_view_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="1"
android:maxEms="1"
android:gravity="center"
android:textSize="23sp"
android:text="我是手柄"
android:layout_gravity="center_vertical" />
</RelativeLayout>
</LinearLayout>
</com.dway.testwork.viewdrag.HandleDrawerLayout>
</FrameLayout>
2、布局类HandleDrawerLayout.java:
package com.dway.testwork.viewdrag;
import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* 带手柄的侧滑菜单布局。
* drawer收起的时候,menu不可见,但是handle可见
* 布局内部结构包含:content、scrim(drawer打开后的半透明背景)、drawer(为横向LinearLayout布局,包含menu和handle)
* Created by dway on 2017/12/19.
*/
public class HandleDrawerLayout extends ViewGroup {
//内容view
private View mContentView;
//当侧滑菜单展开时,显示在mContentView上面的半透明view
private View mScrimView;
//侧滑菜单和手柄组成的布局,因为侧滑菜单和手柄是同时移动的,所以套了一层父布局,方便一起移动
private ViewGroup mDrawerGroup;
//mDrawerGroup中的菜单view
private View mMenuView;
//mDrawerGroup中的手柄view
private View mHandleView;
//强大的帮助类
private ViewDragHelper mHelper;
//菜单展开的程度。取值在0到1之间,0表示侧滑菜单隐藏,1表示侧滑菜单完全展开
private float mOpenPercent = 0;
private boolean mInLayout = false;
private boolean mFirstLayout = true;
public HandleDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 限定child横向的坐标范围:
// 参数left是child想要移动到left位置(仅仅是想移动,但还没移动)
// 如果不想child在X轴上被移动,返回0
if(child == mDrawerGroup){
// 这里代表-mMenuView.getMeasuredWidth() <= left <= 0
// 即child的横向坐标只能在这个范围内
return Math.max(-mMenuView.getMeasuredWidth(), Math.min(left, 0));
}
return 0;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// 限定child纵向的坐标范围,此处暂没用到
return super.clampViewPositionVertical(child, top, dy);
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 粗碰child时对child进行尝试捕获,返回true代表该view可以被捕获,false则相反
// 比如手指按下child这个view时,如果需要交互操作则返回true
return child == mDrawerGroup;
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
// 边缘检测,可通过下面的mHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)设置左右上下边缘检测
// 以下代表屏幕左边缘划进来,强制捕获到mDrawerGroup,相当于人工的tryCaptureView
mHelper.captureChildView(mDrawerGroup, pointerId);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 松开手的时候,释放view,根据当前速度做相应处理
if(releasedChild == mDrawerGroup){
int menuViewWidth = mMenuView.getWidth();
float offset = (menuViewWidth + releasedChild.getLeft()) * 1.0f / menuViewWidth;
// 设置释放后的view慢慢移动到指定位置
mHelper.settleCapturedViewAt(xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -menuViewWidth, releasedChild.getTop());
// 要调用invalidate()才会开始移动
invalidate();
}
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
// 被捕获的view位置改变的回调,left和top为changedView即将移动到的位置
if(changedView == mDrawerGroup){
int menuViewWidth = mMenuView.getWidth();
mOpenPercent = (float) (menuViewWidth + left) / menuViewWidth;
mScrimView.setAlpha(mOpenPercent);
if(floatCompare(mOpenPercent, 0f)){
mScrimView.setVisibility(GONE);
}else{
mScrimView.setVisibility(VISIBLE);
}
if(mDrawerListener != null){
mDrawerListener.onDrawer(mOpenPercent);
}
}
}
@Override
public int getViewHorizontalDragRange(View child) {
//指定child横向可拖拽的范围
return child == mDrawerGroup ? mMenuView.getWidth() : 0;
}
});
// 设置左边缘检测,即从屏幕左边划进屏幕时,会回调onEdgeDragStarted
mHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置各个子view的大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
// 按照xml文件的布局,获取各个子view
mContentView = getChildAt(0);
mScrimView = getChildAt(1);
mDrawerGroup = (ViewGroup) getChildAt(2);
mMenuView = mDrawerGroup.getChildAt(0);
mHandleView = mDrawerGroup.getChildAt(1);
MarginLayoutParams lp = (MarginLayoutParams) mContentView.getLayoutParams();
int contentWidthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
int contentHeightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
mContentView.measure(contentWidthSpec, contentHeightSpec);
lp = (MarginLayoutParams) mScrimView.getLayoutParams();
int bgWidthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
int bgHeightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
mScrimView.measure(bgWidthSpec, bgHeightSpec);
lp = (MarginLayoutParams) mDrawerGroup.getLayoutParams();
int menuGroupWidthSpec = getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin, lp.width);
int menuGroupHeightSpec = getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin, lp.height);
mDrawerGroup.measure(menuGroupWidthSpec, menuGroupHeightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 设置各子view的位置,注意第一次初始化位置和以后设置的位置略有区别
mInLayout = true;
MarginLayoutParams lp = (MarginLayoutParams) mContentView.getLayoutParams();
mContentView.layout(lp.leftMargin, lp.topMargin,
lp.leftMargin + mContentView.getMeasuredWidth(),
lp.topMargin + mContentView.getMeasuredHeight());
lp = (MarginLayoutParams) mScrimView.getLayoutParams();
mScrimView.layout(lp.leftMargin, lp.topMargin,
lp.leftMargin + mScrimView.getMeasuredWidth(),
lp.topMargin + mScrimView.getMeasuredHeight());
lp = (MarginLayoutParams) mDrawerGroup.getLayoutParams();
int groupLeft;// = - mMenuView.getMeasuredWidth() + lp.leftMargin;
if(mFirstLayout){
groupLeft = - mMenuView.getMeasuredWidth() + lp.leftMargin;
}else{
groupLeft = mDrawerGroup.getLeft();
}
mDrawerGroup.layout(groupLeft, lp.topMargin,
groupLeft + mDrawerGroup.getMeasuredWidth(),
lp.topMargin + mDrawerGroup.getMeasuredHeight());
initScrimView();
mInLayout = false;
mFirstLayout = false;
}
@Override
public void requestLayout() {
if(!mInLayout) {
super.requestLayout();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
private void initScrimView(){
if(mFirstLayout && mScrimView != null){
mScrimView.setAlpha(mOpenPercent);
if(floatCompare(mOpenPercent, 0f)){
mScrimView.setVisibility(GONE);
}else{
mScrimView.setVisibility(VISIBLE);
}
mScrimView.setOnClickListener(onClickListener);
}
}
private OnClickListener onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if(isDrawerOpen()){
closeDrawer();
}
}
};
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 把事件处理交给ViewDragHelper
return mHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 把事件处理交给ViewDragHelper
mHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
// 滚动过程的计算,也是交给ViewDragHelper,按以下这么写就好了
if (mHelper.continueSettling(true)) {
invalidate();
}
}
// 以下三个方法需要重写,没啥特殊要求直接返回MarginLayoutParams就可以了
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
/**
* 两个float类型的大小比较
* @return true 相等,false 不相等
*/
private boolean floatCompare(float f1, float f2){
return Math.abs(f1 - f2) < Float.MIN_VALUE;
}
/**
* 是否drawer已打开。float相等直接比较可能有存在问题
*/
public boolean isDrawerOpen(){
return floatCompare(mOpenPercent, 1f);
}
/**
* 打开drawer
*/
public void openDrawer() {
mOpenPercent = 1.0f;
mHelper.smoothSlideViewTo(mDrawerGroup, 0, mDrawerGroup.getTop());
invalidate();
}
/**
* 关闭drawer
*/
public void closeDrawer() {
mOpenPercent = 0.0f;
mHelper.smoothSlideViewTo(mDrawerGroup, -mMenuView.getWidth(), mDrawerGroup.getTop());
invalidate();
}
private OnDrawerListener mDrawerListener = null;
/**
* 外部可设置监听drawer的位置回调
* @param listener
*/
public void setOnDrawerListener(OnDrawerListener listener) {
mDrawerListener = listener;
}
public interface OnDrawerListener{
/**
* 抽屉打开关闭过程监听
* @param percent 取值区间[0, 1],0代表完全关闭,1代表完全打开
*/
void onDrawer(float percent);
}
}
3、Activity中也可以设置手柄的点击处理:
final HandleDrawerLayout handleDrawerLayout = getActivity().findViewById(R.id.hdl_layout);
View handleView = getActivity().findViewById(R.id.hdl_handle_view);
handleView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(handleDrawerLayout.isDrawerOpen()){
handleDrawerLayout.closeDrawer();
}else{
handleDrawerLayout.openDrawer();
}
}
});
二、可上下切换,类似纵向的ViewPager,并且上划时向下弹出菜单效果。以下的注释会比较少,因为跟上面的例子类似,所以不过多注释。看效果图:
1、布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dway.testwork.viewdrag.SlideUpLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<FrameLayout
android:id="@+id/slide_layout_up"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1f00ffff">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="up"/>
</FrameLayout>
<FrameLayout
android:id="@+id/slide_layout_down"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1fffff00">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="down"/>
</FrameLayout>
<FrameLayout
android:id="@+id/slide_layout_slide"
android:layout_width="match_parent"
android:layout_height="100dp"
android:clickable="true"
android:background="#1fff00ff">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="slide"/>
</FrameLayout>
</com.dway.testwork.viewdrag.SlideUpLayout>
</FrameLayout>
2、SlideUpLayout.java类:
package com.dway.testwork.viewdrag;
import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* 可上下切换,类似纵向的ViewPager,并且上划时向下弹出菜单效果
* Created by dway on 2018/1/23.
*/
public class SlideUpLayout extends ViewGroup {
private View mUpView;
private View mDownView;
private View mSlideView;
private ViewDragHelper mHelper;
//上下滑的程度,0表示在upView,1表示在downView
private float mSlidePercent = 0;
private boolean mInLayout = false;
private boolean mFirstLayout = true;
public SlideUpLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if(child == mUpView){
return Math.max(- mUpView.getMeasuredHeight() + mSlideView.getMeasuredHeight(), Math.min(top, 0));
}else if(child == mDownView){
return Math.max(mSlideView.getMeasuredHeight(), Math.min(top, mUpView.getMeasuredHeight()));
}else if(child == mSlideView){
return Math.max(- mSlideView.getMeasuredHeight(), Math.min(top, 0));
}
return 0;
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mUpView || child == mDownView;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(releasedChild == mUpView){
int upViewHeight = mUpView.getMeasuredHeight();
int slideViewHeight = mSlideView.getMeasuredHeight();
float offset = (upViewHeight + releasedChild.getTop() - slideViewHeight) * 1.0f / (upViewHeight - slideViewHeight);
mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? 0 : -upViewHeight + slideViewHeight);
invalidate();
}else if(releasedChild == mDownView){
int downViewHeight = mDownView.getMeasuredHeight();
int slideViewHeight = mSlideView.getMeasuredHeight();
float offset = (releasedChild.getTop() - slideViewHeight) * 1.0f / downViewHeight;
mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? mUpView.getMeasuredHeight() : slideViewHeight);
invalidate();
}
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView == mUpView){
mDownView.setTop(top + mUpView.getMeasuredHeight());
mSlidePercent = (float) (-top) / mDownView.getMeasuredHeight();
if(mSlidePercent > 0.9f){
mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
}else{
mSlideView.setTop(-mSlideView.getMeasuredHeight());
}
}else if(changedView == mDownView){
mUpView.setTop(top - mUpView.getMeasuredHeight());
mSlidePercent = (float) (mUpView.getMeasuredHeight() - top) / mDownView.getMeasuredHeight();
if(mSlidePercent > 0.9f){
mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight()));
}else{
mSlideView.setTop(-mSlideView.getMeasuredHeight());
}
}
requestLayout();
}
@Override
public int getViewVerticalDragRange(View child) {
return child == mUpView ? mUpView.getMeasuredHeight() - mSlideView.getMeasuredHeight() :
child == mDownView ? mDownView.getMeasuredHeight() :
child == mSlideView ? mSlideView.getMeasuredHeight() : 0;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
mUpView = getChildAt(0);
mDownView = getChildAt(1);
mSlideView = getChildAt(2);
//up
MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
int widthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
mUpView.measure(widthSpec, heightSpec);
//slide
lp = (MarginLayoutParams) mSlideView.getLayoutParams();
widthSpec = getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin, lp.width);
heightSpec = getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin, lp.height);
mSlideView.measure(widthSpec, heightSpec);
//down
lp = (MarginLayoutParams) mDownView.getLayoutParams();
widthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
heightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin - mSlideView.getMeasuredHeight(), MeasureSpec.EXACTLY);
mDownView.measure(widthSpec, heightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams();
int upTop;
if(mFirstLayout){
upTop = lp.topMargin;
}else{
upTop = mUpView.getTop();
}
mUpView.layout(lp.leftMargin, upTop,
lp.leftMargin + mUpView.getMeasuredWidth(),
upTop + mUpView.getMeasuredHeight());
lp = (MarginLayoutParams) mDownView.getLayoutParams();
int downTop;
if(mFirstLayout){
downTop = mUpView.getMeasuredHeight() + lp.topMargin;
}else{
downTop = mDownView.getTop();
}
mDownView.layout(lp.leftMargin, downTop,
lp.leftMargin + mDownView.getMeasuredWidth(),
downTop + mDownView.getMeasuredHeight());
lp = (MarginLayoutParams) mSlideView.getLayoutParams();
int slideTop;
if(mFirstLayout){
slideTop = - mSlideView.getMeasuredHeight() + lp.topMargin;
}else{
slideTop = mSlideView.getTop();
}
mSlideView.layout(lp.leftMargin, slideTop,
lp.leftMargin + mSlideView.getMeasuredWidth(),
slideTop + mSlideView.getMeasuredHeight());
mInLayout = false;
mFirstLayout = false;
}
@Override
public void requestLayout() {
if(!mInLayout) {
super.requestLayout();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
if (mHelper.continueSettling(true)) {
invalidate();
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
public float getSlidePercent(){
return mSlidePercent;
}
public void slideToDown(){
mHelper.smoothSlideViewTo(mDownView, mDownView.getLeft(), mSlideView.getMeasuredHeight());
invalidate();
}
public void slideToUp(){
mHelper.smoothSlideViewTo(mUpView, mUpView.getLeft(), 0);
invalidate();
}
}
3、Activity中可通过如下方法设置自动切换上下页面:
SlideUpLayout layout = (SlideUpLayout)findViewById(R.id.slide_layout);
if(layout.getSlidePercent() == 0) {
layout.slideToDown();
}
三、掌握了ViewDragHelper的使用,应该做什么拖拽之类效果都很轻松了
四、这里面其实还有个事件分派拦截的问题,下一篇文章会继续讲。参考《重写ViewGroup并借助ViewDragHelper实现各种拖拽交互效果(二)》,地址 http://blog.csdn.net/lin_dianwei/article/details/79210877