├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── leo │ │ └── slideexit │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cn │ │ │ └── leo │ │ │ └── slideexit │ │ │ ├── FirstActivity.java │ │ │ ├── LoginActivity.java │ │ │ ├── Main2Activity.java │ │ │ ├── MyApp.java │ │ │ ├── MyRecyclerView.java │ │ │ ├── MyViewPager.java │ │ │ ├── MyVpAdapter.java │ │ │ ├── SecondActivity.java │ │ │ ├── SlideExit.java │ │ │ ├── SwipeBack1.java │ │ │ └── SwipeBack2.java │ └── res │ │ ├── layout │ │ ├── activity_first.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_main2.xml │ │ └── content_first.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── cn │ └── leo │ └── slideexit │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── swipe_back_lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── cn │ └── leo │ └── swipe_back_lib │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── cn │ │ └── leo │ │ └── swipe_back_lib │ │ └── SwipeBack.java └── res │ ├── anim │ ├── swipe_back_in_left.xml │ ├── swipe_back_in_left_normal.xml │ ├── swipe_back_in_right.xml │ ├── swipe_back_out_left.xml │ ├── swipe_back_out_right.xml │ └── swipe_back_out_right_normal.xml │ └── values │ └── strings.xml └── test └── java └── cn └── leo └── swipe_back_lib └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlideExit 2 | ## 侧滑关闭activity控件 3 | 4 | 本库不用设置主题,一句代码使用: 5 | 在application里面初始化即可: 6 | ``` 7 | SwipeBack.init(this,false); 8 | ``` 9 | 第二个参数true表示边缘滑动模式,false表示全屏滑动模式; 10 | 全屏滑动模式下,如果有左右滑动的控件会冲突,请自己解决滑动冲突! 11 | 12 | #### 依赖方法: 13 | 1.在项目的全局build文件里面添加仓库: 14 | ``` 15 | allprojects { 16 | repositories { 17 | ... 18 | maven { url 'https://jitpack.io' } 19 | } 20 | } 21 | ``` 22 | 2.在app的build文件里面添加依赖: 23 | ``` 24 | dependencies { 25 | ... 26 | implementation 'com.github.jarryleo:SlideExit:v1.9' 27 | } 28 | ``` 29 | 30 | #### 注意事项: 31 | > 1.本库自带页面过渡动画,不用再重写activity过渡动画,可能会导致错乱 32 | > 2.如果某个页面滑动冲突不好解决,可以在activity类上打上注解:@SwipeBack.IgnoreSwipeBack关闭当前页面的滑动关闭 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | defaultConfig { 6 | applicationId "cn.leo.slideexit" 7 | minSdkVersion 15 8 | targetSdkVersion 24 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(include: ['*.jar'], dir: 'libs') 23 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 24 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | implementation 'com.android.support:appcompat-v7:24.2.1' 28 | implementation 'com.android.support:design:24.2.1' 29 | testImplementation 'junit:junit:4.12' 30 | implementation project(':swipe_back_lib') 31 | // implementation 'com.github.jarryleo:SlideExit:v1.7' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\Android_Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/cn/leo/slideexit/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 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 | * Instrumentation 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() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("cn.leo.slideexit", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/FirstActivity.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.View; 10 | import android.widget.Toast; 11 | 12 | public class FirstActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_first); 18 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 19 | setSupportActionBar(toolbar); 20 | 21 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 22 | fab.setOnClickListener(new View.OnClickListener() { 23 | @Override 24 | public void onClick(View view) { 25 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 26 | .setAction("Action", null).show(); 27 | } 28 | }); 29 | } 30 | 31 | public void click(View view) { 32 | startActivity(new Intent(this, SecondActivity.class)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.annotation.TargetApi; 6 | import android.content.pm.PackageManager; 7 | import android.support.annotation.NonNull; 8 | import android.support.design.widget.Snackbar; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.app.LoaderManager.LoaderCallbacks; 11 | 12 | import android.content.CursorLoader; 13 | import android.content.Loader; 14 | import android.database.Cursor; 15 | import android.net.Uri; 16 | import android.os.AsyncTask; 17 | 18 | import android.os.Build; 19 | import android.os.Bundle; 20 | import android.provider.ContactsContract; 21 | import android.text.TextUtils; 22 | import android.view.KeyEvent; 23 | import android.view.View; 24 | import android.view.View.OnClickListener; 25 | import android.view.inputmethod.EditorInfo; 26 | import android.widget.ArrayAdapter; 27 | import android.widget.AutoCompleteTextView; 28 | import android.widget.Button; 29 | import android.widget.EditText; 30 | import android.widget.TextView; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | import static android.Manifest.permission.READ_CONTACTS; 36 | 37 | /** 38 | * A login screen that offers login via email/password. 39 | */ 40 | public class LoginActivity extends AppCompatActivity implements LoaderCallbacks { 41 | 42 | /** 43 | * Id to identity READ_CONTACTS permission request. 44 | */ 45 | private static final int REQUEST_READ_CONTACTS = 0; 46 | 47 | /** 48 | * A dummy authentication store containing known user names and passwords. 49 | * TODO: remove after connecting to a real authentication system. 50 | */ 51 | private static final String[] DUMMY_CREDENTIALS = new String[]{ 52 | "foo@example.com:hello", "bar@example.com:world" 53 | }; 54 | /** 55 | * Keep track of the login task to ensure we can cancel it if requested. 56 | */ 57 | private UserLoginTask mAuthTask = null; 58 | 59 | // UI references. 60 | private AutoCompleteTextView mEmailView; 61 | private EditText mPasswordView; 62 | private View mProgressView; 63 | private View mLoginFormView; 64 | 65 | @Override 66 | protected void onCreate(Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | setContentView(R.layout.activity_login); 69 | // Set up the login form. 70 | mEmailView = (AutoCompleteTextView) findViewById(R.id.email); 71 | populateAutoComplete(); 72 | 73 | mPasswordView = (EditText) findViewById(R.id.password); 74 | mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { 75 | @Override 76 | public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { 77 | if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { 78 | attemptLogin(); 79 | return true; 80 | } 81 | return false; 82 | } 83 | }); 84 | 85 | Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); 86 | mEmailSignInButton.setOnClickListener(new OnClickListener() { 87 | @Override 88 | public void onClick(View view) { 89 | attemptLogin(); 90 | } 91 | }); 92 | 93 | mLoginFormView = findViewById(R.id.login_form); 94 | mProgressView = findViewById(R.id.login_progress); 95 | } 96 | 97 | private void populateAutoComplete() { 98 | if (!mayRequestContacts()) { 99 | return; 100 | } 101 | 102 | getLoaderManager().initLoader(0, null, this); 103 | } 104 | 105 | private boolean mayRequestContacts() { 106 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 107 | return true; 108 | } 109 | if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { 110 | return true; 111 | } 112 | if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { 113 | Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) 114 | .setAction(android.R.string.ok, new View.OnClickListener() { 115 | @Override 116 | @TargetApi(Build.VERSION_CODES.M) 117 | public void onClick(View v) { 118 | requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); 119 | } 120 | }); 121 | } else { 122 | requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); 123 | } 124 | return false; 125 | } 126 | 127 | /** 128 | * Callback received when a permissions request has been completed. 129 | */ 130 | @Override 131 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 132 | @NonNull int[] grantResults) { 133 | if (requestCode == REQUEST_READ_CONTACTS) { 134 | if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 135 | populateAutoComplete(); 136 | } 137 | } 138 | } 139 | 140 | 141 | /** 142 | * Attempts to sign in or register the account specified by the login form. 143 | * If there are form errors (invalid email, missing fields, etc.), the 144 | * errors are presented and no actual login attempt is made. 145 | */ 146 | private void attemptLogin() { 147 | if (mAuthTask != null) { 148 | return; 149 | } 150 | 151 | // Reset errors. 152 | mEmailView.setError(null); 153 | mPasswordView.setError(null); 154 | 155 | // Store values at the time of the login attempt. 156 | String email = mEmailView.getText().toString(); 157 | String password = mPasswordView.getText().toString(); 158 | 159 | boolean cancel = false; 160 | View focusView = null; 161 | 162 | // Check for a valid password, if the user entered one. 163 | if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { 164 | mPasswordView.setError(getString(R.string.error_invalid_password)); 165 | focusView = mPasswordView; 166 | cancel = true; 167 | } 168 | 169 | // Check for a valid email address. 170 | if (TextUtils.isEmpty(email)) { 171 | mEmailView.setError(getString(R.string.error_field_required)); 172 | focusView = mEmailView; 173 | cancel = true; 174 | } else if (!isEmailValid(email)) { 175 | mEmailView.setError(getString(R.string.error_invalid_email)); 176 | focusView = mEmailView; 177 | cancel = true; 178 | } 179 | 180 | if (cancel) { 181 | // There was an error; don't attempt login and focus the first 182 | // form field with an error. 183 | focusView.requestFocus(); 184 | } else { 185 | // Show a progress spinner, and kick off a background task to 186 | // perform the user login attempt. 187 | showProgress(true); 188 | mAuthTask = new UserLoginTask(email, password); 189 | mAuthTask.execute((Void) null); 190 | } 191 | } 192 | 193 | private boolean isEmailValid(String email) { 194 | //TODO: Replace this with your own logic 195 | return email.contains("@"); 196 | } 197 | 198 | private boolean isPasswordValid(String password) { 199 | //TODO: Replace this with your own logic 200 | return password.length() > 4; 201 | } 202 | 203 | /** 204 | * Shows the progress UI and hides the login form. 205 | */ 206 | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 207 | private void showProgress(final boolean show) { 208 | // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow 209 | // for very easy animations. If available, use these APIs to fade-in 210 | // the progress spinner. 211 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 212 | int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); 213 | 214 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 215 | mLoginFormView.animate().setDuration(shortAnimTime).alpha( 216 | show ? 0 : 1).setListener(new AnimatorListenerAdapter() { 217 | @Override 218 | public void onAnimationEnd(Animator animation) { 219 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 220 | } 221 | }); 222 | 223 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 224 | mProgressView.animate().setDuration(shortAnimTime).alpha( 225 | show ? 1 : 0).setListener(new AnimatorListenerAdapter() { 226 | @Override 227 | public void onAnimationEnd(Animator animation) { 228 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 229 | } 230 | }); 231 | } else { 232 | // The ViewPropertyAnimator APIs are not available, so simply show 233 | // and hide the relevant UI components. 234 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 235 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 236 | } 237 | } 238 | 239 | @Override 240 | public Loader onCreateLoader(int i, Bundle bundle) { 241 | return new CursorLoader(this, 242 | // Retrieve data rows for the device user's 'profile' contact. 243 | Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, 244 | ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION, 245 | 246 | // Select only email addresses. 247 | ContactsContract.Contacts.Data.MIMETYPE + 248 | " = ?", new String[]{ContactsContract.CommonDataKinds.Email 249 | .CONTENT_ITEM_TYPE}, 250 | 251 | // Show primary email addresses first. Note that there won't be 252 | // a primary email address if the user hasn't specified one. 253 | ContactsContract.Contacts.Data.IS_PRIMARY + " DESC"); 254 | } 255 | 256 | @Override 257 | public void onLoadFinished(Loader cursorLoader, Cursor cursor) { 258 | List emails = new ArrayList<>(); 259 | cursor.moveToFirst(); 260 | while (!cursor.isAfterLast()) { 261 | emails.add(cursor.getString(ProfileQuery.ADDRESS)); 262 | cursor.moveToNext(); 263 | } 264 | 265 | addEmailsToAutoComplete(emails); 266 | } 267 | 268 | @Override 269 | public void onLoaderReset(Loader cursorLoader) { 270 | 271 | } 272 | 273 | private void addEmailsToAutoComplete(List emailAddressCollection) { 274 | //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list. 275 | ArrayAdapter adapter = 276 | new ArrayAdapter<>(LoginActivity.this, 277 | android.R.layout.simple_dropdown_item_1line, emailAddressCollection); 278 | 279 | mEmailView.setAdapter(adapter); 280 | } 281 | 282 | 283 | private interface ProfileQuery { 284 | String[] PROJECTION = { 285 | ContactsContract.CommonDataKinds.Email.ADDRESS, 286 | ContactsContract.CommonDataKinds.Email.IS_PRIMARY, 287 | }; 288 | 289 | int ADDRESS = 0; 290 | int IS_PRIMARY = 1; 291 | } 292 | 293 | /** 294 | * Represents an asynchronous login/registration task used to authenticate 295 | * the user. 296 | */ 297 | public class UserLoginTask extends AsyncTask { 298 | 299 | private final String mEmail; 300 | private final String mPassword; 301 | 302 | UserLoginTask(String email, String password) { 303 | mEmail = email; 304 | mPassword = password; 305 | } 306 | 307 | @Override 308 | protected Boolean doInBackground(Void... params) { 309 | // TODO: attempt authentication against a network service. 310 | 311 | try { 312 | // Simulate network access. 313 | Thread.sleep(2000); 314 | } catch (InterruptedException e) { 315 | return false; 316 | } 317 | 318 | for (String credential : DUMMY_CREDENTIALS) { 319 | String[] pieces = credential.split(":"); 320 | if (pieces[0].equals(mEmail)) { 321 | // Account exists, return true if the password matches. 322 | return pieces[1].equals(mPassword); 323 | } 324 | } 325 | 326 | // TODO: register the new account here. 327 | return true; 328 | } 329 | 330 | @Override 331 | protected void onPostExecute(final Boolean success) { 332 | mAuthTask = null; 333 | showProgress(false); 334 | 335 | if (success) { 336 | finish(); 337 | } else { 338 | mPasswordView.setError(getString(R.string.error_incorrect_password)); 339 | mPasswordView.requestFocus(); 340 | } 341 | } 342 | 343 | @Override 344 | protected void onCancelled() { 345 | mAuthTask = null; 346 | showProgress(false); 347 | } 348 | } 349 | } 350 | 351 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/Main2Activity.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | public class Main2Activity extends AppCompatActivity { 7 | 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | setContentView(R.layout.activity_main2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/MyApp.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.app.Application; 4 | 5 | import cn.leo.swipe_back_lib.SwipeBack; 6 | 7 | /** 8 | * @author : Jarry Leo 9 | * @date : 2019/2/13 11:21 10 | */ 11 | public class MyApp extends Application { 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | SwipeBack.init(this, true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/MyRecyclerView.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | 9 | /** 10 | * Created by Leo on 2018/2/26. 11 | */ 12 | 13 | public class MyRecyclerView extends RecyclerView { 14 | public MyRecyclerView(Context context) { 15 | super(context); 16 | } 17 | 18 | public MyRecyclerView(Context context, @Nullable AttributeSet attrs) { 19 | super(context, attrs); 20 | } 21 | 22 | public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { 23 | super(context, attrs, defStyle); 24 | } 25 | 26 | @Override 27 | protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 28 | super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); 29 | getParent().requestDisallowInterceptTouchEvent(false); 30 | } 31 | 32 | @Override 33 | public boolean onTouchEvent(MotionEvent e) { 34 | return super.onTouchEvent(e); 35 | } 36 | 37 | @Override 38 | public int computeHorizontalScrollOffset() { 39 | return super.computeHorizontalScrollOffset(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/MyViewPager.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * Created by JarryLeo on 2017/5/6. 10 | */ 11 | 12 | public class MyViewPager extends ViewPager { 13 | 14 | private float mDownX; 15 | 16 | public MyViewPager(Context context) { 17 | this(context, null); 18 | } 19 | 20 | public MyViewPager(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | @Override 25 | public boolean dispatchTouchEvent(MotionEvent ev) { 26 | int item = getCurrentItem(); 27 | switch (ev.getAction()) { 28 | case MotionEvent.ACTION_DOWN: 29 | getParent().requestDisallowInterceptTouchEvent(true); 30 | mDownX = ev.getX(); 31 | break; 32 | case MotionEvent.ACTION_MOVE: 33 | float x = ev.getX(); 34 | if ((x > mDownX && item == 0) || 35 | x < mDownX && item == getAdapter().getCount() - 1) { 36 | getParent().requestDisallowInterceptTouchEvent(false); 37 | return false; 38 | } else { 39 | getParent().requestDisallowInterceptTouchEvent(true); 40 | } 41 | break; 42 | default: 43 | break; 44 | } 45 | return super.dispatchTouchEvent(ev); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/MyVpAdapter.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.graphics.Color; 4 | import android.support.v4.view.PagerAdapter; 5 | import android.view.Gravity; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | /** 11 | * Created by JarryLeo on 2017/5/6. 12 | */ 13 | 14 | public class MyVpAdapter extends PagerAdapter { 15 | String[] title = new String[]{"第一页", "第二页", "第三页"}; 16 | 17 | @Override 18 | public int getCount() { 19 | return title.length; 20 | } 21 | 22 | @Override 23 | public boolean isViewFromObject(View view, Object object) { 24 | return view == object; 25 | } 26 | 27 | @Override 28 | public Object instantiateItem(ViewGroup container, int position) { 29 | TextView tv = new TextView(container.getContext()); 30 | tv.setText(title[position]); 31 | tv.setTextColor(Color.BLACK); 32 | tv.setGravity(Gravity.CENTER); 33 | tv.setBackgroundColor(Color.YELLOW); 34 | container.addView(tv); 35 | return tv; 36 | } 37 | 38 | @Override 39 | public void destroyItem(ViewGroup container, int position, Object object) { 40 | container.removeView((View) object); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/SecondActivity.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v4.view.ViewPager; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | 10 | /** 11 | * @author Leo 12 | */ 13 | public class SecondActivity extends Activity { 14 | 15 | private ViewPager mVp; 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | init(); 22 | } 23 | 24 | private void init() { 25 | mVp = (ViewPager) findViewById(R.id.vp); 26 | mVp.setAdapter(new MyVpAdapter()); 27 | findViewById(R.id.tvTitle).setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View view) { 30 | startActivity(new Intent(SecondActivity.this, Main2Activity.class)); 31 | } 32 | }); 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/SlideExit.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.animation.IntEvaluator; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.support.v4.view.ViewCompat; 10 | import android.support.v4.widget.ViewDragHelper; 11 | import android.util.AttributeSet; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.Window; 16 | import android.widget.FrameLayout; 17 | 18 | /** 19 | * Created by JarryLeo on 2017/5/6. 20 | */ 21 | 22 | public class SlideExit extends FrameLayout { 23 | //支持往四边滑动关闭Activity 24 | 25 | public static final int SLIDE_LEFT_EXIT = 1 << 0; 26 | public static final int SLIDE_RIGHT_EXIT = 1 << 1; 27 | public static final int SLIDE_UP_EXIT = 1 << 2; 28 | public static final int SLIDE_DOWN_EXIT = 1 << 3; 29 | private ViewDragHelper mDragHelper; 30 | private View mContentView; 31 | private int mSide; 32 | private Paint mPaint; 33 | private Activity mActivity; 34 | private IntEvaluator mEvaluator; 35 | 36 | public SlideExit(Context context) { 37 | this(context, null); 38 | } 39 | 40 | public SlideExit(Context context, AttributeSet attrs) { 41 | this(context, attrs, 0); 42 | } 43 | 44 | public SlideExit(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | init(); 47 | } 48 | 49 | private void init() { 50 | mPaint = new Paint(); 51 | mEvaluator = new IntEvaluator(); 52 | mDragHelper = ViewDragHelper.create(this, mCallback); 53 | } 54 | 55 | ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() { 56 | 57 | 58 | @Override 59 | public boolean tryCaptureView(View child, int pointerId) { 60 | //捕获子容器 61 | return child == mContentView; 62 | } 63 | 64 | @Override 65 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 66 | //滑动边界1/4即关闭Activity 67 | float xDistance = getMeasuredWidth() / 3; 68 | float yDistance = getMeasuredHeight() / 4; 69 | //左右超过边界处理 70 | if (mContentView.getLeft() != 0) { 71 | if (mContentView.getLeft() < -xDistance) { 72 | mDragHelper.smoothSlideViewTo(mContentView, -getMeasuredWidth(), 0); 73 | } else if (mContentView.getLeft() > xDistance) { 74 | mDragHelper.smoothSlideViewTo(mContentView, getMeasuredWidth(), 0); 75 | } else { 76 | //未超过则回弹 77 | mDragHelper.smoothSlideViewTo(mContentView, 0, 0); 78 | } 79 | } 80 | //上下超过边界 81 | if (mContentView.getTop() != 0) { 82 | if (mContentView.getTop() < -yDistance) { 83 | mDragHelper.smoothSlideViewTo(mContentView, 0, -getMeasuredHeight()); 84 | } else if (mContentView.getTop() > yDistance) { 85 | mDragHelper.smoothSlideViewTo(mContentView, 0, getMeasuredHeight()); 86 | } else { 87 | //未超过则回弹 88 | mDragHelper.smoothSlideViewTo(mContentView, 0, 0); 89 | } 90 | } 91 | //刷新动画 92 | ViewCompat.postInvalidateOnAnimation(SlideExit.this); 93 | } 94 | 95 | @Override 96 | public int clampViewPositionVertical(View child, int top, int dy) { 97 | //上滑 98 | if ((mSide & SLIDE_UP_EXIT) == SLIDE_UP_EXIT && top < 0) { 99 | return top; 100 | } 101 | //下滑 102 | if ((mSide & SLIDE_DOWN_EXIT) == SLIDE_DOWN_EXIT && top > 0) { 103 | return top; 104 | } 105 | return 0; 106 | } 107 | 108 | @Override 109 | public int clampViewPositionHorizontal(View child, int left, int dx) { 110 | //左滑 111 | if ((mSide & SLIDE_LEFT_EXIT) == SLIDE_LEFT_EXIT && left < 0) { 112 | return left; 113 | } 114 | //右滑 115 | if ((mSide & SLIDE_RIGHT_EXIT) == SLIDE_RIGHT_EXIT && left > 0) { 116 | return left; 117 | } 118 | return 0; 119 | } 120 | 121 | @Override 122 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 123 | //绘制阴影刷新显示 124 | ViewCompat.postInvalidateOnAnimation(SlideExit.this); 125 | } 126 | 127 | 128 | @Override 129 | public int getViewHorizontalDragRange(View child) { 130 | return 100; 131 | } 132 | 133 | @Override 134 | public int getViewVerticalDragRange(View child) { 135 | return 100; 136 | } 137 | }; 138 | 139 | @Override 140 | public void computeScroll() { 141 | //滑动动画处理 142 | if (mDragHelper.continueSettling(true)) { 143 | //刷新显示 144 | ViewCompat.postInvalidateOnAnimation(this); 145 | } else { 146 | //滑动结束,关闭Activity 147 | if (Math.abs(mContentView.getLeft()) == getMeasuredWidth() 148 | || Math.abs(mContentView.getTop()) == getMeasuredHeight()) { 149 | mActivity.finish(); 150 | } 151 | } 152 | } 153 | 154 | @Override 155 | public boolean onInterceptTouchEvent(MotionEvent ev) { 156 | //交给ViewDragHelper处理拦截事件 157 | return mDragHelper.shouldInterceptTouchEvent(ev); 158 | } 159 | 160 | @Override 161 | public boolean onTouchEvent(MotionEvent event) { 162 | //交给ViewDragHelper处理滑动事件 163 | mDragHelper.processTouchEvent(event); 164 | return true; 165 | } 166 | 167 | @Override 168 | protected void dispatchDraw(Canvas canvas) { 169 | super.dispatchDraw(canvas); 170 | 171 | //绘制阴影 172 | if (mContentView.getTop() > 0) { 173 | Integer evaluate = mEvaluator.evaluate(mContentView.getTop() / mContentView.getMeasuredHeight() * 1.0f, 174 | 0, 100); 175 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 176 | //上边阴影 177 | canvas.drawRect(0, 0, mContentView.getMeasuredWidth(), 178 | mContentView.getTop(), mPaint); 179 | } else if (mContentView.getTop() < 0) { 180 | Integer evaluate = mEvaluator.evaluate(-mContentView.getTop() / mContentView.getMeasuredHeight() * 1.0f, 181 | 0, 100); 182 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 183 | //下边阴影 184 | canvas.drawRect(0, mContentView.getMeasuredHeight() + mContentView.getTop(), 185 | getMeasuredWidth(), getMeasuredHeight(), mPaint); 186 | } 187 | 188 | if (mContentView.getLeft() > 0) { 189 | Integer evaluate = mEvaluator.evaluate(mContentView.getLeft() * 1.0f / mContentView.getMeasuredWidth(), 190 | 0, 100); 191 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 192 | //左边阴影 193 | canvas.drawRect(0, 0, mContentView.getLeft(), 194 | getMeasuredHeight(), mPaint); 195 | } else if (mContentView.getLeft() < 0) { 196 | Integer evaluate = mEvaluator.evaluate(-mContentView.getLeft() / mContentView.getMeasuredWidth() * 1.0f, 197 | 0, 100); 198 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 199 | //右边阴影 200 | canvas.drawRect(mContentView.getLeft() + getMeasuredWidth(), 0, 201 | getMeasuredWidth(), getMeasuredHeight(), mPaint); 202 | } 203 | } 204 | 205 | public static void bind(Activity activity, int slide_side) { 206 | //创建本类对象并绑定Activity 207 | new SlideExit(activity).attach(activity, slide_side); 208 | } 209 | 210 | 211 | private void attach(Activity activity, int slide_side) { 212 | //滑动关闭方向 213 | mActivity = activity; 214 | mSide = slide_side; 215 | //获取Activity布局的父容器 216 | Window window = activity.getWindow(); 217 | ViewGroup decorView = (ViewGroup) window.getDecorView(); 218 | 219 | if (decorView.getChildCount() > 0) { 220 | //拿到Activity的contentView 221 | mContentView = decorView.getChildAt(0); 222 | decorView.removeAllViews(); 223 | //把contentView添加到本容器 224 | this.removeAllViews(); 225 | this.addView(mContentView); 226 | //把本容器添加到Activity的父容器 227 | decorView.addView(this); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/SwipeBack1.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.animation.IntEvaluator; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.graphics.Bitmap; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.graphics.Rect; 12 | import android.os.Bundle; 13 | import android.support.annotation.NonNull; 14 | import android.support.annotation.Nullable; 15 | import android.support.v4.view.ViewCompat; 16 | import android.support.v4.widget.ViewDragHelper; 17 | import android.util.AttributeSet; 18 | import android.view.MotionEvent; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.view.Window; 22 | import android.widget.FrameLayout; 23 | 24 | import java.lang.annotation.ElementType; 25 | import java.lang.annotation.Retention; 26 | import java.lang.annotation.RetentionPolicy; 27 | import java.lang.annotation.Target; 28 | import java.util.LinkedList; 29 | 30 | /** 31 | * @author : Jarry Leo 32 | * @date : 2019/2/13 9:51 33 | */ 34 | public class SwipeBack1 extends FrameLayout implements Application.ActivityLifecycleCallbacks { 35 | private LinkedList mActivities = new LinkedList<>(); 36 | /** 37 | * 当前展示的activity的内容页面 38 | */ 39 | private View mContentView; 40 | /** 41 | * 底下activity的view作为当前activity的背景 42 | */ 43 | private Bitmap mBackViewBitmap; 44 | private Paint mPaint; 45 | private Paint mBitmapPaint; 46 | private ViewDragHelper mDragHelper; 47 | private IntEvaluator mEvaluator; 48 | private boolean mIsSwipeBack; 49 | 50 | private SwipeBack1(@NonNull Context context) { 51 | this(context, null); 52 | } 53 | 54 | private SwipeBack1(@NonNull Context context, @Nullable AttributeSet attrs) { 55 | this(context, attrs, 0); 56 | } 57 | 58 | private SwipeBack1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 59 | super(context, attrs, defStyleAttr); 60 | initView(); 61 | } 62 | 63 | private void initView() { 64 | mPaint = new Paint(); 65 | mBitmapPaint = new Paint(); 66 | mEvaluator = new IntEvaluator(); 67 | mDragHelper = ViewDragHelper.create(this, mCallback); 68 | setBackgroundColor(Color.WHITE); 69 | } 70 | 71 | 72 | public static void init(Application application) { 73 | SwipeBack1 swipeBack = new SwipeBack1(application); 74 | application.registerActivityLifecycleCallbacks(swipeBack); 75 | } 76 | 77 | ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() { 78 | 79 | @Override 80 | public boolean tryCaptureView(View child, int pointerId) { 81 | return child == mContentView && 82 | mActivities.size() > 1; 83 | } 84 | 85 | @Override 86 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 87 | //滑动边界1/4即关闭Activity 88 | float xDistance = getMeasuredWidth() / 3; 89 | //左右超过边界处理 90 | if (mContentView.getLeft() != 0) { 91 | if (mContentView.getLeft() < -xDistance) { 92 | mDragHelper.smoothSlideViewTo(mContentView, -getMeasuredWidth(), 0); 93 | } else if (mContentView.getLeft() > xDistance) { 94 | mDragHelper.smoothSlideViewTo(mContentView, getMeasuredWidth(), 0); 95 | } else { 96 | //未超过则回弹 97 | mDragHelper.smoothSlideViewTo(mContentView, 0, 0); 98 | } 99 | } 100 | 101 | //刷新动画 102 | ViewCompat.postInvalidateOnAnimation(SwipeBack1.this); 103 | } 104 | 105 | 106 | @Override 107 | public int clampViewPositionHorizontal(View child, int left, int dx) { 108 | if (left <= 0) { 109 | return 0; 110 | } 111 | return left; 112 | } 113 | 114 | @Override 115 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 116 | //绘制阴影刷新显示 117 | ViewCompat.postInvalidateOnAnimation(SwipeBack1.this); 118 | } 119 | 120 | 121 | @Override 122 | public int getViewHorizontalDragRange(View child) { 123 | return 100; 124 | } 125 | 126 | @Override 127 | public int getViewVerticalDragRange(View child) { 128 | return 100; 129 | } 130 | }; 131 | 132 | @Override 133 | public void computeScroll() { 134 | //滑动动画处理 135 | if (mDragHelper.continueSettling(true)) { 136 | //刷新显示 137 | ViewCompat.postInvalidateOnAnimation(this); 138 | } else { 139 | //滑动结束,关闭Activity 140 | int left = mContentView.getLeft(); 141 | if (Math.abs(left) == getMeasuredWidth()) { 142 | Activity last = mActivities.getLast(); 143 | mIsSwipeBack = true; 144 | last.finish(); 145 | } 146 | } 147 | 148 | } 149 | 150 | @Override 151 | public boolean onInterceptTouchEvent(MotionEvent ev) { 152 | //交给ViewDragHelper处理拦截事件 153 | return mDragHelper.shouldInterceptTouchEvent(ev); 154 | } 155 | 156 | @Override 157 | public boolean onTouchEvent(MotionEvent event) { 158 | //交给ViewDragHelper处理滑动事件 159 | mDragHelper.processTouchEvent(event); 160 | return true; 161 | } 162 | 163 | @Override 164 | protected void dispatchDraw(Canvas canvas) { 165 | super.dispatchDraw(canvas); 166 | int left = mContentView.getLeft(); 167 | if (left > 0) { 168 | //绘制背景图 169 | if (mBackViewBitmap != null && !mBackViewBitmap.isRecycled()) { 170 | int width = mContentView.getWidth(); 171 | int i = (width - left) / 4; 172 | Rect src = new Rect(i, 0, i + left, mBackViewBitmap.getHeight()); 173 | Rect dst = new Rect(0, 0, left, mBackViewBitmap.getHeight()); 174 | canvas.drawBitmap(mBackViewBitmap, src, dst, mBitmapPaint); 175 | } 176 | //绘制阴影 177 | Integer evaluate = mEvaluator.evaluate( 178 | mContentView.getLeft() * 1.0f / mContentView.getMeasuredWidth(), 179 | 0, 100); 180 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 181 | //左边阴影 182 | canvas.drawRect(0, 0, mContentView.getLeft(), getMeasuredHeight(), mPaint); 183 | } 184 | 185 | } 186 | 187 | @Override 188 | public void onActivityCreated(Activity activity, Bundle bundle) { 189 | mActivities.addLast(activity); 190 | if (!checkIgnore(activity)) { 191 | getViewToNewActivity(); 192 | } 193 | activity.overridePendingTransition(R.anim.swipe_back_in_right, R.anim.swipe_back_out_left); 194 | } 195 | 196 | @Override 197 | public void onActivityStarted(Activity activity) { 198 | 199 | } 200 | 201 | @Override 202 | public void onActivityResumed(Activity activity) { 203 | } 204 | 205 | @Override 206 | public void onActivityPaused(Activity activity) { 207 | if (mIsSwipeBack) { 208 | activity.overridePendingTransition(R.anim.swipe_back_in_left, R.anim.swipe_back_out_right); 209 | mIsSwipeBack = false; 210 | } else { 211 | activity.overridePendingTransition(R.anim.swipe_back_in_left_normal, R.anim.swipe_back_out_right_normal); 212 | } 213 | } 214 | 215 | @Override 216 | public void onActivityStopped(Activity activity) { 217 | } 218 | 219 | @Override 220 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 221 | 222 | } 223 | 224 | @Override 225 | public void onActivityDestroyed(Activity activity) { 226 | boolean isTop = activity == mActivities.getLast(); 227 | Activity secondLastActivity = mActivities.get(mActivities.size() - 2); 228 | boolean isSecond = activity == secondLastActivity; 229 | mActivities.remove(activity); 230 | if (isTop) { 231 | //从销毁的页面移除自身 232 | mContentView = null; 233 | ViewGroup decorView = getDecorView(activity); 234 | decorView.removeAllViews(); 235 | if (checkIgnore(secondLastActivity)) { 236 | return; 237 | } 238 | //处理底下漏出的新页面 239 | resetViewToSecondActivity(); 240 | } else if (isSecond) { 241 | View backView = getContentView(secondLastActivity); 242 | mBackViewBitmap = getViewBitmap(backView); 243 | } 244 | } 245 | 246 | /** 247 | * 把底部的页面还原回去 248 | * 并且把它下面的view拿上来 249 | */ 250 | private void resetViewToSecondActivity() { 251 | if (mActivities.size() == 0) { 252 | recycleBitmap(); 253 | return; 254 | } 255 | this.removeAllViews(); 256 | Activity lastActivity = mActivities.getLast(); 257 | ViewGroup decorView = getDecorView(lastActivity); 258 | if (mActivities.size() < 2) { 259 | recycleBitmap(); 260 | return; 261 | } 262 | //如果底下有多个页面则把倒数第二个页面添加到它的背景 263 | Activity secondLastActivity = mActivities.get(mActivities.size() - 2); 264 | mContentView = getContentView(lastActivity); 265 | View backView = getContentView(secondLastActivity); 266 | mBackViewBitmap = getViewBitmap(backView); 267 | decorView.removeAllViews(); 268 | this.addView(mContentView); 269 | //把本容器添加到Activity的父容器 270 | decorView.addView(this); 271 | } 272 | 273 | /** 274 | * 把底部页面的布局添加到当前展示的activity的底下; 275 | */ 276 | private void getViewToNewActivity() { 277 | if (mActivities.size() < 2) { 278 | return; 279 | } 280 | Activity lastActivity = mActivities.getLast(); 281 | Activity secondLastActivity = mActivities.get(mActivities.size() - 2); 282 | //处理之前的activity 283 | this.removeAllViews(); 284 | if (mContentView != null) { 285 | ViewGroup preDecorView = getDecorView(secondLastActivity); 286 | preDecorView.removeAllViews(); 287 | preDecorView.addView(mContentView); 288 | } 289 | //处理现在的activity 290 | ViewGroup decorView = getDecorView(lastActivity); 291 | if (decorView.getChildCount() > 0) { 292 | //拿到Activity的contentView 293 | mContentView = getContentView(lastActivity); 294 | View backView = getContentView(secondLastActivity); 295 | mBackViewBitmap = getViewBitmap(backView); 296 | //把contentView添加到本容器 297 | decorView.removeAllViews(); 298 | this.addView(mContentView); 299 | //把本容器添加到Activity的父容器 300 | decorView.addView(this); 301 | } 302 | } 303 | 304 | private ViewGroup getDecorView(Activity activity) { 305 | Window window = activity.getWindow(); 306 | return (ViewGroup) window.getDecorView(); 307 | } 308 | 309 | private View getContentView(Activity activity) { 310 | ViewGroup decorView = getDecorView(activity); 311 | return decorView.getChildAt(0); 312 | } 313 | 314 | private Bitmap getViewBitmap(@NonNull View v) { 315 | recycleBitmap(); 316 | Bitmap bmp = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_4444); 317 | Canvas canvas = new Canvas(bmp); 318 | v.draw(canvas); 319 | return bmp; 320 | } 321 | 322 | private void recycleBitmap() { 323 | if (mBackViewBitmap != null) { 324 | mBackViewBitmap.recycle(); 325 | mBackViewBitmap = null; 326 | } 327 | } 328 | 329 | @Target(ElementType.TYPE) 330 | @Retention(RetentionPolicy.RUNTIME) 331 | public @interface IgnoreSwipeBack { 332 | // 有些自定义view在解绑时会跟本工具冲突(onPause后view空白) 333 | // 可以在activity上打上此注解关闭当前页面的滑动退出 334 | } 335 | 336 | private boolean checkIgnore(Activity activity) { 337 | Class a = activity.getClass(); 338 | return a.isAnnotationPresent(IgnoreSwipeBack.class); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /app/src/main/java/cn/leo/slideexit/SwipeBack2.java: -------------------------------------------------------------------------------- 1 | package cn.leo.slideexit; 2 | 3 | import android.animation.IntEvaluator; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.os.Bundle; 11 | import android.support.annotation.NonNull; 12 | import android.support.annotation.Nullable; 13 | import android.support.v4.view.ViewCompat; 14 | import android.support.v4.widget.ViewDragHelper; 15 | import android.util.AttributeSet; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.view.Window; 20 | import android.widget.FrameLayout; 21 | 22 | import java.util.LinkedList; 23 | 24 | /** 25 | * @author : Jarry Leo 26 | * @date : 2019/2/13 9:51 27 | */ 28 | public class SwipeBack2 extends FrameLayout implements Application.ActivityLifecycleCallbacks { 29 | private LinkedList mActivities = new LinkedList<>(); 30 | /** 31 | * 当前展示的activity的内容页面 32 | */ 33 | private View mContentView; 34 | /** 35 | * 底下activity的view作为当前activity的背景 36 | */ 37 | private View mBackView; 38 | private Paint mPaint; 39 | private ViewDragHelper mDragHelper; 40 | private IntEvaluator mEvaluator; 41 | 42 | private SwipeBack2(@NonNull Context context) { 43 | this(context, null); 44 | } 45 | 46 | private SwipeBack2(@NonNull Context context, @Nullable AttributeSet attrs) { 47 | this(context, attrs, 0); 48 | } 49 | 50 | private SwipeBack2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | initView(); 53 | } 54 | 55 | private void initView() { 56 | mPaint = new Paint(); 57 | mEvaluator = new IntEvaluator(); 58 | mDragHelper = ViewDragHelper.create(this, mCallback); 59 | } 60 | 61 | 62 | public static void init(Application application) { 63 | SwipeBack2 swipeBack = new SwipeBack2(application); 64 | application.registerActivityLifecycleCallbacks(swipeBack); 65 | } 66 | 67 | ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() { 68 | 69 | @Override 70 | public boolean tryCaptureView(View child, int pointerId) { 71 | return child == mContentView && mActivities.size() > 1; 72 | } 73 | 74 | @Override 75 | public void onViewReleased(View releasedChild, float xvel, float yvel) { 76 | //滑动边界1/4即关闭Activity 77 | float xDistance = getMeasuredWidth() / 3; 78 | //左右超过边界处理 79 | if (mContentView.getLeft() != 0) { 80 | if (mContentView.getLeft() < -xDistance) { 81 | mDragHelper.smoothSlideViewTo(mContentView, -getMeasuredWidth(), 0); 82 | } else if (mContentView.getLeft() > xDistance) { 83 | mDragHelper.smoothSlideViewTo(mContentView, getMeasuredWidth(), 0); 84 | } else { 85 | //未超过则回弹 86 | mDragHelper.smoothSlideViewTo(mContentView, 0, 0); 87 | } 88 | } 89 | 90 | //刷新动画 91 | ViewCompat.postInvalidateOnAnimation(SwipeBack2.this); 92 | } 93 | 94 | 95 | @Override 96 | public int clampViewPositionHorizontal(View child, int left, int dx) { 97 | if (left <= 0) { 98 | return 0; 99 | } 100 | return left; 101 | } 102 | 103 | @Override 104 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 105 | //绘制阴影刷新显示 106 | ViewCompat.postInvalidateOnAnimation(SwipeBack2.this); 107 | } 108 | 109 | 110 | @Override 111 | public int getViewHorizontalDragRange(View child) { 112 | return 100; 113 | } 114 | 115 | @Override 116 | public int getViewVerticalDragRange(View child) { 117 | return 100; 118 | } 119 | }; 120 | 121 | @Override 122 | public void computeScroll() { 123 | //滑动动画处理 124 | int left = mContentView.getLeft(); 125 | if (mDragHelper.continueSettling(true)) { 126 | //刷新显示 127 | ViewCompat.postInvalidateOnAnimation(this); 128 | } else { 129 | //滑动结束,关闭Activity 130 | if (Math.abs(left) == getMeasuredWidth()) { 131 | mActivities.getLast().finish(); 132 | } 133 | } 134 | if (mBackView != null) { 135 | int width = mBackView.getWidth(); 136 | int i = (width - left) / 4; 137 | mBackView.setLeft(-i); 138 | } 139 | } 140 | 141 | @Override 142 | public boolean onInterceptTouchEvent(MotionEvent ev) { 143 | //交给ViewDragHelper处理拦截事件 144 | return mDragHelper.shouldInterceptTouchEvent(ev); 145 | } 146 | 147 | @Override 148 | public boolean onTouchEvent(MotionEvent event) { 149 | //交给ViewDragHelper处理滑动事件 150 | mDragHelper.processTouchEvent(event); 151 | return true; 152 | } 153 | 154 | @Override 155 | protected void dispatchDraw(Canvas canvas) { 156 | super.dispatchDraw(canvas); 157 | if (mContentView.getLeft() > 0) { 158 | Integer evaluate = mEvaluator.evaluate(mContentView.getLeft() * 1.0f / mContentView.getMeasuredWidth(), 159 | 0, 100); 160 | mPaint.setColor(Color.argb(100 - evaluate, 0, 0, 0)); 161 | //左边阴影 162 | canvas.drawRect(0, 0, mContentView.getLeft(), 163 | getMeasuredHeight(), mPaint); 164 | } 165 | } 166 | 167 | @Override 168 | public void onActivityCreated(Activity activity, Bundle bundle) { 169 | mActivities.addLast(activity); 170 | getViewToNewActivity(); 171 | } 172 | 173 | @Override 174 | public void onActivityStarted(Activity activity) { 175 | 176 | } 177 | 178 | @Override 179 | public void onActivityResumed(Activity activity) { 180 | } 181 | 182 | @Override 183 | public void onActivityPaused(Activity activity) { 184 | if (mActivities.size() <= 1) { 185 | activity.overridePendingTransition(R.anim.swipe_back_in_right, R.anim.swipe_back_out_left); 186 | } else { 187 | activity.overridePendingTransition(R.anim.swipe_back_in_left, R.anim.swipe_back_out_right); 188 | } 189 | } 190 | 191 | @Override 192 | public void onActivityStopped(Activity activity) { 193 | } 194 | 195 | @Override 196 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 197 | 198 | } 199 | 200 | @Override 201 | public void onActivityDestroyed(Activity activity) { 202 | boolean isTop = activity == mActivities.getLast(); 203 | mActivities.remove(activity); 204 | if (isTop) { 205 | //从销毁的页面移除自身 206 | mContentView = null; 207 | ViewGroup decorView = getDecorView(activity); 208 | decorView.removeAllViews(); 209 | //处理底下漏出的新页面 210 | resetViewToSecondActivity(); 211 | } 212 | } 213 | 214 | /** 215 | * 把底部的页面还原回去 216 | * 并且把它下面的view拿上来 217 | */ 218 | private void resetViewToSecondActivity() { 219 | if (mActivities.size() == 0) { 220 | return; 221 | } 222 | this.removeAllViews(); 223 | Activity lastActivity = mActivities.getLast(); 224 | ViewGroup decorView = getDecorView(lastActivity); 225 | if (mActivities.size() < 2) { 226 | //如果底下只剩一个页面,则把它之前的页面还给它 227 | decorView.addView(mBackView); 228 | return; 229 | } 230 | //如果底下有多个页面则把倒数第二个页面添加到它的背景 231 | Activity secondLastActivity = mActivities.get(mActivities.size() - 2); 232 | mContentView = mBackView; 233 | mBackView = getContentView(secondLastActivity); 234 | this.addView(mBackView); 235 | this.addView(mContentView); 236 | //把本容器添加到Activity的父容器 237 | decorView.addView(this); 238 | } 239 | 240 | /** 241 | * 把底部页面的布局添加到当前展示的activity的底下; 242 | */ 243 | private void getViewToNewActivity() { 244 | if (mActivities.size() < 2) { 245 | return; 246 | } 247 | Activity lastActivity = mActivities.getLast(); 248 | Activity secondLastActivity = mActivities.get(mActivities.size() - 2); 249 | ViewGroup decorView = getDecorView(lastActivity); 250 | if (decorView.getChildCount() > 0) { 251 | //拿到Activity的contentView 252 | mContentView = getContentView(lastActivity); 253 | mBackView = getContentView(secondLastActivity); 254 | //把contentView添加到本容器 255 | this.removeAllViews(); 256 | this.addView(mBackView); 257 | this.addView(mContentView); 258 | //把本容器添加到Activity的父容器 259 | decorView.addView(this); 260 | } 261 | } 262 | 263 | private ViewGroup getDecorView(Activity activity) { 264 | Window window = activity.getWindow(); 265 | return (ViewGroup) window.getDecorView(); 266 | } 267 | 268 | private View getContentView(Activity activity) { 269 | ViewGroup decorView = getDecorView(activity); 270 | View contentView = decorView.getChildAt(0); 271 | decorView.removeAllViews(); 272 | return contentView; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_first.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 22 | 23 | 27 | 28 | 33 | 34 | 37 | 38 | 46 | 47 | 48 | 49 | 52 | 53 | 64 | 65 | 66 | 67 |