├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── sss │ │ └── simpleDrawerLayout │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── sss │ │ │ └── simpleDrawerLayout │ │ │ ├── MainActivity.java │ │ │ └── SimpleDrawerLayout.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── sss │ └── simpleDrawerLayout │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── demo.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleDrawerLayout 2 | 3 | No picture u say a j8! 4 | 5 | 少啰嗦,先看效果 6 | 7 | ![闭嘴看图](https://github.com/michael007js/SimpleDrawerLayout/blob/master/images/demo.gif "闭嘴看图") 8 | 9 | 10 | ## 项目介绍 11 | 12 | 这个抽屉菜单扩展了一些原生不支持的动画交互效果, 支持上下左右各个方向拉出,可当做上拉框用 13 | 14 | 15 | 16 | 重要的事情说三遍: 17 | 全程就一个文件!全程就一个文件!全程就一个文件! 18 | 19 | [真·一个文件](https://github.com/michael007js/SimpleDrawerLayout/blob/master/app/src/main/java/com/sss/simpleDrawerLayout/SimpleDrawerLayout.java) 20 | 21 | 22 | 极大的降低耦合,想怎么折腾就怎么折腾 23 | 24 | # 亮点:与系统的drawerLayout可以做到无缝对接,直接将drawerLayout替换成simpleDrawerLayout即可,其余方法逻辑不需要动 25 | 26 | 唯一的回调事件 : 27 | 28 | setOnSimpleDrawerLayoutCallBack(new SimpleDrawerLayout.OnSimpleDrawerLayoutCallBack() { 29 | @Override 30 | public void onDrawerStatusChanged(SimpleDrawerLayout drawerLayout, int status, int amount, int state) { 31 | 32 | } 33 | }); 34 | 35 | 常用状态介绍: 36 | 37 | public final static int OPENING = 4;//抽屉打开中 38 | public final static int OPENED = 5;//抽屉被打开 39 | public final static int CLOSING = 6;//抽屉关闭中 40 | public final static int CLOSED = 7;//抽屉被关闭 41 | 42 | public final static int START = 8;//抽屉开始移动 43 | public final static int MOVING = 9;//抽屉移动中 44 | public final static int END = 10;//抽屉移动结束 45 | 46 | 47 | Api: 48 | 49 | /** 50 | * 设置抽屉方向 51 | */ 52 | public void setGravity(int gravity) 53 | 54 | /** 55 | * 获取抽屉方向 56 | */ 57 | public int getGravity() 58 | 59 | /** 60 | * 打开抽屉 61 | */ 62 | public void openDrawer(int gravity) 63 | 64 | /** 65 | * 关闭抽屉 66 | */ 67 | public void closeDrawers() 68 | 69 | /** 70 | * 设置抽屉尺寸(0f-1f) 71 | */ 72 | public void setDrawerPercent(float drawerPercent) 73 | 74 | /** 75 | * 获取抽屉的状态 76 | */ 77 | public int getDrawerStatus() 78 | 79 | /** 80 | * 贝塞尔颜色是否依附抽屉背景色 81 | */ 82 | public void setAttachDrawerColor(boolean attachDrawerColor) 83 | 84 | /** 85 | * 设置抽屉背景色 86 | */ 87 | public void setBackgroundColor(@ColorInt int color) 88 | 89 | /** 90 | * 设置贝塞尔颜色 91 | */ 92 | public void setBesselColor(int alpha, int red, int green, int blue) 93 | 94 | over 95 | 96 | By SSS 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.sss.simpleDrawerLayout" 7 | minSdkVersion 17 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | } 29 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/sss/simpleDrawerLayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.sss.simpleDrawerLayout; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.sss.simpleDrawerLayout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/sss/simpleDrawerLayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.sss.simpleDrawerLayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.util.Log; 6 | import android.view.Gravity; 7 | import android.view.View; 8 | import android.widget.Toast; 9 | 10 | import java.util.Random; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | private SimpleDrawerLayout simpleDrawerLayout; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | simpleDrawerLayout = findViewById(R.id.simpleDrawerLayout); 21 | simpleDrawerLayout.setOnSimpleDrawerLayoutCallBack(new SimpleDrawerLayout.OnSimpleDrawerLayoutCallBack() { 22 | @Override 23 | public void onDrawerStatusChanged(SimpleDrawerLayout drawerLayout, int status, int amount, int state) { 24 | // Log.e("SSS", status + "---" + amount + "---" + state); 25 | } 26 | }); 27 | } 28 | 29 | float percent = 0.7f; 30 | 31 | public void percent(View view) { 32 | percent = percent == 0.7f ? 0.5f : 0.7f; 33 | simpleDrawerLayout.setDrawerPercent(percent); 34 | } 35 | 36 | 37 | public void fromRight(View view) { 38 | simpleDrawerLayout.setGravity(Gravity.RIGHT); 39 | go(); 40 | } 41 | 42 | public void fromLeft(View view) { 43 | simpleDrawerLayout.setGravity(Gravity.LEFT); 44 | go(); 45 | } 46 | 47 | public void fromTop(View view) { 48 | simpleDrawerLayout.setGravity(Gravity.TOP); 49 | go(); 50 | } 51 | 52 | public void fromBottom(View view) { 53 | simpleDrawerLayout.setGravity(Gravity.BOTTOM); 54 | go(); 55 | } 56 | 57 | private void go() { 58 | if (simpleDrawerLayout.getDrawerStatus() == SimpleDrawerLayout.CLOSED) { 59 | simpleDrawerLayout.openDrawer(simpleDrawerLayout.getGravity()); 60 | } else { 61 | simpleDrawerLayout.closeDrawers(); 62 | } 63 | } 64 | 65 | Random random=new Random(); 66 | public void color(View view) { 67 | simpleDrawerLayout.setBesselColor(random.nextInt(256),random.nextInt(256),random.nextInt(256),random.nextInt(256)); 68 | } 69 | 70 | public void drawerColor(View view) { 71 | simpleDrawerLayout.setBackgroundColor(getResources().getColor(R.color.colorAccent)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/sss/simpleDrawerLayout/SimpleDrawerLayout.java: -------------------------------------------------------------------------------- 1 | package com.sss.simpleDrawerLayout; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.content.res.Resources; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.Rect; 13 | import android.graphics.drawable.ColorDrawable; 14 | import android.graphics.drawable.Drawable; 15 | import android.support.annotation.ColorInt; 16 | import android.util.AttributeSet; 17 | import android.util.TypedValue; 18 | import android.view.Gravity; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.view.animation.AccelerateDecelerateInterpolator; 23 | import android.view.animation.DecelerateInterpolator; 24 | 25 | import java.text.DecimalFormat; 26 | 27 | /** 28 | * 一个简单的抽屉菜单,扩展了一些原生不支持的交互效果 29 | */ 30 | public class SimpleDrawerLayout extends ViewGroup { 31 | /***************************以下为内部状态*****************************/ 32 | private final static int NONE = 0;//初始状态,没有被操作过 33 | private final static int PREPARE = 1;//预打开 34 | private final static int DRAG = 2;//拖动 35 | private final static int OUT = 3;//贝塞尔退场 36 | 37 | /***************************以下为抽屉状态*****************************/ 38 | public final static int OPENING = 4;//抽屉打开中 39 | public final static int OPENED = 5;//抽屉被打开 40 | public final static int CLOSING = 6;//抽屉关闭中 41 | public final static int CLOSED = 7;//抽屉被关闭 42 | public final static int START = 8;//抽屉开始移动 43 | public final static int MOVING = 9;//抽屉移动中 44 | public final static int END = 10;//抽屉移动结束 45 | 46 | public final static int BACKGROUND_COLOR = Color.parseColor("#ededed");//抽屉默认背景 47 | 48 | 49 | //第一次的触摸x点 50 | private int x = 0; 51 | //第一次的触摸y点 52 | private int y = 0; 53 | //实时的贝塞尔x点 54 | private int besselX = 0; 55 | //实时的贝塞尔y点 56 | private int besselY = 0; 57 | //方向 58 | private int gravity = Gravity.RIGHT; 59 | //当前内部状态 60 | private int status = NONE; 61 | //当前抽屉状态 62 | private int drawerStatus = CLOSED; 63 | //贝塞尔路径 64 | private Path besselPath = new Path(); 65 | //贝塞尔画笔 66 | private Paint besselPaint = new Paint(); 67 | //拉出抽屉菜单所触发的阀值 68 | private int triggerThreshold = dp2px(50); 69 | //贝塞尔阀值 70 | private int besselThreshold = triggerThreshold; 71 | //贝塞尔离场动画 72 | private ValueAnimator besselOutAnimator; 73 | //贝塞尔动画时间 74 | private int besselOutDuration = 500; 75 | //贝塞尔动画速度 76 | private int besselOutSpeed = dp2px(5); 77 | //贝塞尔颜色 78 | private int besselColor; 79 | //抽屉动画 80 | private ValueAnimator drawerAnimator; 81 | //抽屉尺寸百分比 82 | private float drawerPercent = 0.7f; 83 | //抽屉动画时间 84 | private int drawerDuration = 500; 85 | //抽屉进场插值器 86 | private DecelerateInterpolator openInterpolator = new DecelerateInterpolator(); 87 | //抽屉进场插值器 88 | private DecelerateInterpolator closeInterpolator = new DecelerateInterpolator(); 89 | //抽屉位移量 90 | private int amount = 0; 91 | //遮罩 92 | private Rect maskRect = new Rect(); 93 | //遮罩画笔 94 | private Paint maskPaint = new Paint(); 95 | //遮罩透明度 96 | private float maskAlphaPercent = 0.7f; 97 | //贝塞尔颜色是否依附抽屉背景色 98 | private boolean attachDrawerColor; 99 | 100 | 101 | private OnSimpleDrawerLayoutCallBack onSimpleDrawerLayoutCallBack; 102 | 103 | public void setOnSimpleDrawerLayoutCallBack(OnSimpleDrawerLayoutCallBack onSimpleDrawerLayoutCallBack) { 104 | this.onSimpleDrawerLayoutCallBack = onSimpleDrawerLayoutCallBack; 105 | } 106 | 107 | public void clear() { 108 | if (besselOutAnimator != null) { 109 | besselOutAnimator.removeUpdateListener(animatorUpdateListener); 110 | besselOutAnimator.removeListener(animatorListenerAdapter); 111 | } 112 | besselOutAnimator = null; 113 | if (drawerAnimator != null) { 114 | drawerAnimator.removeUpdateListener(animatorUpdateListener); 115 | drawerAnimator.removeListener(animatorListenerAdapter); 116 | } 117 | drawerAnimator = null; 118 | } 119 | 120 | private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 121 | @Override 122 | public void onAnimationUpdate(ValueAnimator animation) { 123 | if (animation == besselOutAnimator) { 124 | besselCoordinates(besselX, besselY); 125 | invalidate(); 126 | } 127 | if (animation == drawerAnimator) { 128 | amount = (int) animation.getAnimatedValue(); 129 | drawerLocation(); 130 | maskColor(); 131 | getChildAt(1).setAlpha(getAlphaPercent()); 132 | if (onSimpleDrawerLayoutCallBack != null) { 133 | onSimpleDrawerLayoutCallBack.onDrawerStatusChanged(SimpleDrawerLayout.this, drawerStatus, amount, MOVING); 134 | } 135 | } 136 | } 137 | }; 138 | 139 | private AnimatorListenerAdapter animatorListenerAdapter = new AnimatorListenerAdapter() { 140 | @Override 141 | public void onAnimationStart(Animator animation) { 142 | super.onAnimationStart(animation); 143 | if (animation == drawerAnimator) { 144 | if (drawerAnimator.getInterpolator() == openInterpolator) { 145 | drawerStatus = OPENING; 146 | } else if (drawerAnimator.getInterpolator() == closeInterpolator) { 147 | drawerStatus = CLOSING; 148 | } 149 | if (onSimpleDrawerLayoutCallBack != null) { 150 | onSimpleDrawerLayoutCallBack.onDrawerStatusChanged(SimpleDrawerLayout.this, drawerStatus, amount, START); 151 | } 152 | } 153 | } 154 | 155 | @Override 156 | public void onAnimationEnd(Animator animation) { 157 | super.onAnimationEnd(animation); 158 | if (animation == besselOutAnimator) { 159 | reset(); 160 | } 161 | if (animation == drawerAnimator) { 162 | if (drawerAnimator.getInterpolator() == openInterpolator) { 163 | drawerStatus = OPENED; 164 | maskPaint.setAlpha((int) (255 * maskAlphaPercent)); 165 | invalidate(); 166 | } else if (drawerAnimator.getInterpolator() == closeInterpolator) { 167 | drawerStatus = CLOSED; 168 | maskPaint.setAlpha(0); 169 | invalidate(); 170 | } 171 | if (onSimpleDrawerLayoutCallBack != null) { 172 | onSimpleDrawerLayoutCallBack.onDrawerStatusChanged(SimpleDrawerLayout.this, drawerStatus, amount, END); 173 | } 174 | } 175 | } 176 | }; 177 | 178 | public SimpleDrawerLayout(Context context) { 179 | super(context); 180 | init(); 181 | } 182 | 183 | public SimpleDrawerLayout(Context context, AttributeSet attrs) { 184 | super(context, attrs); 185 | init(); 186 | } 187 | 188 | public SimpleDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 189 | super(context, attrs, defStyleAttr); 190 | init(); 191 | } 192 | 193 | /** 194 | * 初始化 195 | */ 196 | private void init() { 197 | besselPaint.setAntiAlias(true); 198 | maskPaint.setAntiAlias(true); 199 | post(new Runnable() { 200 | @Override 201 | public void run() { 202 | if (getChildAt(1) != null) { 203 | initDrawerSize(); 204 | besselBackgroundColor(getChildAt(1).getBackground()); 205 | besselPaint.setColor(attachDrawerColor ? besselColor : BACKGROUND_COLOR); 206 | getChildAt(1).setOnTouchListener(new OnTouchListener() { 207 | @Override 208 | public boolean onTouch(View v, MotionEvent event) { 209 | //TODO 这里不做什么操作,仅仅拦截抽屉的触摸事件不让其穿透 210 | return true; 211 | } 212 | }); 213 | } 214 | } 215 | }); 216 | } 217 | 218 | /** 219 | * 初始化抽屉尺寸 220 | */ 221 | private void initDrawerSize() { 222 | LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 223 | switch (gravity) { 224 | case Gravity.RIGHT: 225 | case Gravity.END: 226 | case Gravity.LEFT: 227 | case Gravity.START: 228 | layoutParams.width = (int) (getWidth() * drawerPercent); 229 | layoutParams.height = getHeight(); 230 | break; 231 | case Gravity.TOP: 232 | case Gravity.BOTTOM: 233 | layoutParams.width = getWidth(); 234 | layoutParams.height = (int) (getHeight() * drawerPercent); 235 | break; 236 | } 237 | getChildAt(1).setLayoutParams(layoutParams); 238 | } 239 | 240 | /** 241 | * 初始化贝塞尔背景色 242 | */ 243 | private void besselBackgroundColor(Drawable drawable) { 244 | if (drawable == null) { 245 | besselColor = BACKGROUND_COLOR; 246 | return; 247 | } 248 | besselColor = drawable instanceof ColorDrawable ? ((ColorDrawable) drawable).getColor() : BACKGROUND_COLOR; 249 | } 250 | 251 | @Override 252 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 253 | if (getChildCount() != 2) { 254 | throw new RuntimeException("子布局数量错误!请在xml中放入两个子布局,第一个为主布局,第二个为抽屉布局。"); 255 | } 256 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 257 | measureChildren(widthMeasureSpec, heightMeasureSpec); 258 | } 259 | 260 | @Override 261 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 262 | getChildAt(0).layout(0, 0, getChildAt(0).getMeasuredWidth(), getChildAt(0).getMeasuredHeight()); 263 | } 264 | 265 | @Override 266 | protected void dispatchDraw(Canvas canvas) { 267 | super.dispatchDraw(canvas); 268 | canvas.drawPath(besselPath, besselPaint); 269 | canvas.drawRect(maskRect, maskPaint); 270 | } 271 | 272 | @Override 273 | public boolean onInterceptTouchEvent(MotionEvent ev) { 274 | switch (ev.getAction()) { 275 | case MotionEvent.ACTION_DOWN: 276 | return isContactPointEffective(ev) && drawerStatus == CLOSED || drawerStatus == CLOSING || drawerStatus == OPENING || drawerStatus == OPENED && isContactPointMaskRect(ev); 277 | } 278 | 279 | return super.onInterceptTouchEvent(ev); 280 | } 281 | 282 | @Override 283 | public boolean onTouchEvent(MotionEvent event) { 284 | if (drawerStatus == OPENING || drawerStatus == CLOSING) { 285 | return super.onTouchEvent(event); 286 | } 287 | switch (event.getAction()) { 288 | case MotionEvent.ACTION_DOWN: 289 | x = (int) event.getX(); 290 | y = (int) event.getY(); 291 | if (trigger(event, false) && drawerStatus == OPENED) { 292 | closeDrawers(); 293 | } else { 294 | if (drawerStatus != OPENED && isContactPointEffective(event)) { 295 | status = PREPARE; 296 | } 297 | } 298 | return true; 299 | case MotionEvent.ACTION_MOVE: 300 | if (outOfRect(event) && drawerStatus != OPENED) { 301 | status = OUT; 302 | besselOutAnimation(); 303 | return true; 304 | } 305 | if (status == PREPARE) { 306 | status = DRAG; 307 | } 308 | if (event.getX() - x < besselThreshold && status == DRAG) { 309 | besselCoordinates((int) event.getX(), (int) event.getY()); 310 | } 311 | invalidate(); 312 | return true; 313 | case MotionEvent.ACTION_UP: 314 | if (!outOfRect(event) && status == DRAG) { 315 | if (drawerStatus == CLOSED) { 316 | status = OUT; 317 | besselOutAnimation(); 318 | } 319 | if (trigger(event, true)) { 320 | openDrawer(); 321 | } 322 | if (drawerStatus == OPENED && !isContactPointMaskRect(event)) { 323 | getChildAt(1).invalidate(); 324 | } 325 | } 326 | break; 327 | } 328 | return super.onTouchEvent(event); 329 | 330 | } 331 | 332 | /** 333 | * 计算抽屉位置 334 | */ 335 | private void drawerLocation() { 336 | switch (gravity) { 337 | case Gravity.RIGHT: 338 | case Gravity.END: 339 | getChildAt(1).layout(getWidth() - amount, 0, getWidth() - amount + getDrawerSize(), getChildAt(1).getMeasuredHeight()); 340 | break; 341 | case Gravity.LEFT: 342 | case Gravity.START: 343 | getChildAt(1).layout(-getDrawerSize() + amount, 0, amount, getChildAt(1).getMeasuredHeight()); 344 | break; 345 | case Gravity.TOP: 346 | getChildAt(1).layout(0, -getDrawerSize() + amount, getWidth(), amount); 347 | break; 348 | case Gravity.BOTTOM: 349 | getChildAt(1).layout(0, getHeight() - amount, getWidth(), getHeight() + getDrawerSize() - amount); 350 | break; 351 | } 352 | } 353 | 354 | /** 355 | * 计算遮罩范围及透明度 356 | */ 357 | private void maskColor() { 358 | switch (gravity) { 359 | case Gravity.RIGHT: 360 | case Gravity.END: 361 | maskRect.left = 0; 362 | maskRect.top = 0; 363 | maskRect.right = getWidth() - amount; 364 | maskRect.bottom = getHeight(); 365 | break; 366 | case Gravity.LEFT: 367 | case Gravity.START: 368 | maskRect.left = amount; 369 | maskRect.top = 0; 370 | maskRect.right = getWidth(); 371 | maskRect.bottom = getHeight(); 372 | break; 373 | case Gravity.TOP: 374 | maskRect.left = 0; 375 | maskRect.top = amount; 376 | maskRect.right = getWidth(); 377 | maskRect.bottom = getHeight(); 378 | break; 379 | case Gravity.BOTTOM: 380 | maskRect.left = 0; 381 | maskRect.top = 0; 382 | maskRect.right = getWidth(); 383 | maskRect.bottom = getHeight() - amount; 384 | break; 385 | } 386 | maskPaint.setAlpha((int) (getAlphaPercent() * 255 * maskAlphaPercent)); 387 | invalidate(); 388 | } 389 | 390 | /** 391 | * 计算遮罩范围及透明度 392 | */ 393 | private boolean isContactPointMaskRect(MotionEvent event) { 394 | return event.getX() > maskRect.left && event.getX() < maskRect.right && event.getY() > maskRect.top && event.getY() < maskRect.bottom; 395 | } 396 | 397 | private DecimalFormat decimalFormat = new DecimalFormat("0.000"); 398 | 399 | /** 400 | * 计算抽屉透明度 401 | */ 402 | private float getAlphaPercent() { 403 | switch (gravity) { 404 | case Gravity.LEFT: 405 | case Gravity.START: 406 | case Gravity.RIGHT: 407 | case Gravity.END: 408 | return Float.parseFloat(decimalFormat.format(amount * 1.0 / getChildAt(1).getWidth())); 409 | case Gravity.TOP: 410 | case Gravity.BOTTOM: 411 | return Float.parseFloat(decimalFormat.format(amount * 1.0 / getChildAt(1).getHeight())); 412 | } 413 | return 255f; 414 | } 415 | 416 | /** 417 | * 计算触点是否越界 418 | */ 419 | private boolean outOfRect(MotionEvent event) { 420 | if (event.getX() > getWidth() || event.getX() < 0 || event.getY() > getHeight() || event.getY() < 0) { 421 | return true; 422 | } 423 | return false; 424 | } 425 | 426 | /** 427 | * 计算触点是否位于屏幕四周(贝塞尔绘制区域) 428 | */ 429 | private boolean isContactPointEffective(MotionEvent event) { 430 | switch (gravity) { 431 | case Gravity.LEFT: 432 | case Gravity.START: 433 | return event.getX() < triggerThreshold; 434 | case Gravity.RIGHT: 435 | case Gravity.END: 436 | return event.getX() > getWidth() - triggerThreshold; 437 | case Gravity.TOP: 438 | return event.getY() < triggerThreshold; 439 | case Gravity.BOTTOM: 440 | return event.getY() > getHeight() - triggerThreshold; 441 | } 442 | return false; 443 | } 444 | 445 | /** 446 | * 计算贝塞尔坐标 447 | */ 448 | private void besselCoordinates(int eventX, int eventY) { 449 | besselPath.reset(); 450 | switch (gravity) { 451 | case Gravity.LEFT: 452 | case Gravity.START: 453 | besselPath.moveTo(0, 0); 454 | if (status == OUT) { 455 | besselPath.cubicTo(0, 0, besselX -= besselOutSpeed, besselY, 0, getHeight()); 456 | } else { 457 | if (Math.abs(x - eventX) > besselThreshold) { 458 | besselX = besselThreshold; 459 | besselY = eventY; 460 | besselPath.cubicTo(0, 0, besselX, besselY, 0, getHeight()); 461 | } else { 462 | besselX = Math.abs(x - eventX); 463 | besselY = eventY; 464 | besselPath.cubicTo(0, 0, besselX, besselY, 0, getHeight()); 465 | } 466 | } 467 | break; 468 | case Gravity.RIGHT: 469 | case Gravity.END: 470 | besselPath.moveTo(getWidth(), 0); 471 | if (status == OUT) { 472 | besselPath.cubicTo(getWidth(), 0, besselX += besselOutSpeed, besselY, getWidth(), getHeight()); 473 | } else { 474 | if (Math.abs(eventX - x) > besselThreshold) { 475 | besselX = getWidth() - besselThreshold; 476 | besselY = eventY; 477 | besselPath.cubicTo(getWidth(), 0, besselX, besselY, getWidth(), getHeight()); 478 | } else { 479 | besselX = getWidth() - Math.abs(eventX - x); 480 | besselY = eventY; 481 | besselPath.cubicTo(getWidth(), 0, besselX, besselY, getWidth(), getHeight()); 482 | } 483 | } 484 | break; 485 | case Gravity.TOP: 486 | besselPath.moveTo(0, 0); 487 | if (status == OUT) { 488 | besselPath.cubicTo(0, 0, besselX, besselY -= besselOutSpeed, getWidth(), 0); 489 | } else { 490 | if (Math.abs(y - eventY) > besselThreshold) { 491 | besselX = eventX; 492 | besselY = besselThreshold; 493 | besselPath.cubicTo(0, 0, besselX, besselY, getWidth(), 0); 494 | } else { 495 | besselX = eventX; 496 | besselY = Math.abs(y - eventY); 497 | besselPath.cubicTo(0, 0, besselX, besselY, getWidth(), 0); 498 | } 499 | } 500 | break; 501 | case Gravity.BOTTOM: 502 | besselPath.moveTo(0, getHeight()); 503 | if (status == OUT) { 504 | besselPath.cubicTo(0, getHeight(), besselX, besselY += besselOutSpeed, getWidth(), getHeight()); 505 | } else { 506 | 507 | if (Math.abs(y - eventY) > besselThreshold) { 508 | besselX = eventX; 509 | besselY = getHeight() - besselThreshold; 510 | besselPath.cubicTo(0, getHeight(), besselX, besselY, getWidth(), getHeight()); 511 | } else { 512 | besselX = eventX; 513 | besselY = getHeight() - Math.abs(y - eventY); 514 | besselPath.cubicTo(0, getHeight(), besselX, besselY, getWidth(), getHeight()); 515 | } 516 | } 517 | break; 518 | } 519 | besselPath.close(); 520 | } 521 | 522 | /** 523 | * 计算触点是否达到开启/关闭抽屉的程度 524 | */ 525 | private boolean trigger(MotionEvent event, boolean isOpen) { 526 | switch (gravity) { 527 | case Gravity.LEFT: 528 | case Gravity.START: 529 | return isOpen ? event.getX() > triggerThreshold : event.getX() > getDrawerSize(); 530 | case Gravity.RIGHT: 531 | case Gravity.END: 532 | return isOpen ? event.getX() < getWidth() - triggerThreshold : event.getX() < getWidth() - getDrawerSize(); 533 | case Gravity.TOP: 534 | return isOpen ? event.getY() > triggerThreshold : event.getY() > getDrawerSize(); 535 | case Gravity.BOTTOM: 536 | return isOpen ? event.getY() < getHeight() - triggerThreshold : event.getY() < getHeight() - getDrawerSize(); 537 | } 538 | return false; 539 | } 540 | 541 | /** 542 | * 按百分比获取侧滑抽屉宽高尺寸 543 | */ 544 | private int getDrawerSize() { 545 | switch (gravity) { 546 | case Gravity.RIGHT: 547 | case Gravity.END: 548 | case Gravity.LEFT: 549 | case Gravity.START: 550 | return (int) (getWidth() * drawerPercent); 551 | case Gravity.TOP: 552 | case Gravity.BOTTOM: 553 | return (int) (getHeight() * drawerPercent); 554 | } 555 | return 0; 556 | } 557 | 558 | /** 559 | * 贝塞尔出场动画 560 | */ 561 | private void besselOutAnimation() { 562 | if (besselOutAnimator == null) { 563 | besselOutAnimator = ValueAnimator.ofInt(0, 100); 564 | besselOutAnimator.addUpdateListener(animatorUpdateListener); 565 | besselOutAnimator.addListener(animatorListenerAdapter); 566 | besselOutAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 567 | besselOutAnimator.setDuration(besselOutDuration); 568 | } 569 | besselOutAnimator.start(); 570 | } 571 | 572 | /** 573 | * 设置抽屉方向 574 | */ 575 | public void setGravity(int gravity) { 576 | if (gravity == Gravity.LEFT || 577 | gravity == Gravity.START || 578 | gravity == Gravity.RIGHT || 579 | gravity == Gravity.END || 580 | gravity == Gravity.TOP || 581 | gravity == Gravity.BOTTOM) { 582 | this.gravity = gravity; 583 | } 584 | } 585 | 586 | /** 587 | * 获取抽屉方向 588 | */ 589 | public int getGravity() { 590 | return gravity; 591 | } 592 | 593 | /** 594 | * 打开抽屉(内部调用) 595 | */ 596 | private void openDrawer() { 597 | createDrawerAnimator(); 598 | drawerAnimator.setRepeatMode(ValueAnimator.RESTART); 599 | drawerAnimator.setIntValues(0, getDrawerSize()); 600 | drawerAnimator.setInterpolator(openInterpolator); 601 | drawerAnimator.start(); 602 | } 603 | 604 | /** 605 | * 打开抽屉 606 | */ 607 | public void openDrawer(int gravity) { 608 | if (drawerStatus != OPENED) { 609 | setGravity(gravity); 610 | openDrawer(); 611 | } 612 | } 613 | 614 | /** 615 | * 关闭抽屉 616 | */ 617 | public void closeDrawers() { 618 | if (drawerStatus != CLOSED) { 619 | createDrawerAnimator(); 620 | drawerAnimator.setRepeatMode(ValueAnimator.REVERSE); 621 | drawerAnimator.setIntValues(getDrawerSize(), 0); 622 | drawerAnimator.setInterpolator(closeInterpolator); 623 | drawerAnimator.start(); 624 | } 625 | } 626 | 627 | /** 628 | * 创建抽屉动画 629 | */ 630 | private void createDrawerAnimator() { 631 | if (drawerAnimator == null) { 632 | drawerAnimator = ValueAnimator.ofInt(0, getDrawerSize()); 633 | drawerAnimator.addUpdateListener(animatorUpdateListener); 634 | drawerAnimator.addListener(animatorListenerAdapter); 635 | drawerAnimator.setDuration(drawerDuration); 636 | } 637 | } 638 | 639 | /** 640 | * dp转px 641 | */ 642 | public int dp2px(float dpVal) { 643 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, Resources.getSystem().getDisplayMetrics()); 644 | } 645 | 646 | /** 647 | * 设置抽屉尺寸(0f-1f) 648 | */ 649 | public void setDrawerPercent(float drawerPercent) { 650 | this.drawerPercent = drawerPercent; 651 | openDrawer(); 652 | } 653 | 654 | /** 655 | * 获取抽屉的状态 656 | */ 657 | public int getDrawerStatus() { 658 | return drawerStatus; 659 | } 660 | 661 | /** 662 | * 贝塞尔颜色是否依附抽屉背景色 663 | */ 664 | public void setAttachDrawerColor(boolean attachDrawerColor) { 665 | this.attachDrawerColor = attachDrawerColor; 666 | besselPaint.setColor(this.attachDrawerColor ? besselColor : BACKGROUND_COLOR); 667 | } 668 | 669 | /** 670 | * 设置抽屉背景色 671 | */ 672 | public void setBackgroundColor(@ColorInt int color) { 673 | if (getChildCount() == 2) { 674 | getChildAt(1).setBackgroundColor(color); 675 | besselBackgroundColor(getChildAt(1).getBackground()); 676 | if (attachDrawerColor) { 677 | besselPaint.setColor(besselColor); 678 | } 679 | } 680 | } 681 | 682 | /** 683 | * 设置贝塞尔颜色 684 | */ 685 | public void setBesselColor(int alpha, int red, int green, int blue) { 686 | if (attachDrawerColor) { 687 | return; 688 | } 689 | this.besselColor = (alpha > -1 && red > -1 && green > -1 && blue > -1) ? Color.argb(alpha, red, green, blue) : BACKGROUND_COLOR; 690 | besselPaint.setColor(this.besselColor); 691 | } 692 | 693 | /** 694 | * 重置 695 | */ 696 | private void reset() { 697 | besselPath.reset(); 698 | status = NONE; 699 | x = 0; 700 | y = 0; 701 | besselX = 0; 702 | besselY = 0; 703 | } 704 | 705 | public interface OnSimpleDrawerLayoutCallBack { 706 | 707 | void onDrawerStatusChanged(SimpleDrawerLayout drawerLayout, int status, int amount, int state); 708 | } 709 | 710 | } 711 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 24 | 25 | 26 |