1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.widget; 18 19import android.os.Handler; 20import android.os.Looper; 21import android.os.Message; 22 23import java.lang.ref.WeakReference; 24 25/** 26 * Manages {@link Snackbar}s. 27 */ 28class SnackbarManager { 29 30 static final int MSG_TIMEOUT = 0; 31 32 private static final int SHORT_DURATION_MS = 1500; 33 private static final int LONG_DURATION_MS = 2750; 34 35 private static SnackbarManager sSnackbarManager; 36 37 static SnackbarManager getInstance() { 38 if (sSnackbarManager == null) { 39 sSnackbarManager = new SnackbarManager(); 40 } 41 return sSnackbarManager; 42 } 43 44 private final Object mLock; 45 private final Handler mHandler; 46 47 private SnackbarRecord mCurrentSnackbar; 48 private SnackbarRecord mNextSnackbar; 49 50 private SnackbarManager() { 51 mLock = new Object(); 52 mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { 53 @Override 54 public boolean handleMessage(Message message) { 55 switch (message.what) { 56 case MSG_TIMEOUT: 57 handleTimeout((SnackbarRecord) message.obj); 58 return true; 59 } 60 return false; 61 } 62 }); 63 } 64 65 interface Callback { 66 void show(); 67 void dismiss(int event); 68 } 69 70 public void show(int duration, Callback callback) { 71 synchronized (mLock) { 72 if (isCurrentSnackbarLocked(callback)) { 73 // Means that the callback is already in the queue. We'll just update the duration 74 mCurrentSnackbar.duration = duration; 75 76 // If this is the Snackbar currently being shown, call re-schedule it's 77 // timeout 78 mHandler.removeCallbacksAndMessages(mCurrentSnackbar); 79 scheduleTimeoutLocked(mCurrentSnackbar); 80 return; 81 } else if (isNextSnackbarLocked(callback)) { 82 // We'll just update the duration 83 mNextSnackbar.duration = duration; 84 } else { 85 // Else, we need to create a new record and queue it 86 mNextSnackbar = new SnackbarRecord(duration, callback); 87 } 88 89 if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar, 90 Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) { 91 // If we currently have a Snackbar, try and cancel it and wait in line 92 return; 93 } else { 94 // Clear out the current snackbar 95 mCurrentSnackbar = null; 96 // Otherwise, just show it now 97 showNextSnackbarLocked(); 98 } 99 } 100 } 101 102 public void dismiss(Callback callback, int event) { 103 synchronized (mLock) { 104 if (isCurrentSnackbarLocked(callback)) { 105 cancelSnackbarLocked(mCurrentSnackbar, event); 106 } else if (isNextSnackbarLocked(callback)) { 107 cancelSnackbarLocked(mNextSnackbar, event); 108 } 109 } 110 } 111 112 /** 113 * Should be called when a Snackbar is no longer displayed. This is after any exit 114 * animation has finished. 115 */ 116 public void onDismissed(Callback callback) { 117 synchronized (mLock) { 118 if (isCurrentSnackbarLocked(callback)) { 119 // If the callback is from a Snackbar currently show, remove it and show a new one 120 mCurrentSnackbar = null; 121 if (mNextSnackbar != null) { 122 showNextSnackbarLocked(); 123 } 124 } 125 } 126 } 127 128 /** 129 * Should be called when a Snackbar is being shown. This is after any entrance animation has 130 * finished. 131 */ 132 public void onShown(Callback callback) { 133 synchronized (mLock) { 134 if (isCurrentSnackbarLocked(callback)) { 135 scheduleTimeoutLocked(mCurrentSnackbar); 136 } 137 } 138 } 139 140 public void cancelTimeout(Callback callback) { 141 synchronized (mLock) { 142 if (isCurrentSnackbarLocked(callback)) { 143 mHandler.removeCallbacksAndMessages(mCurrentSnackbar); 144 } 145 } 146 } 147 148 public void restoreTimeout(Callback callback) { 149 synchronized (mLock) { 150 if (isCurrentSnackbarLocked(callback)) { 151 scheduleTimeoutLocked(mCurrentSnackbar); 152 } 153 } 154 } 155 156 public boolean isCurrent(Callback callback) { 157 synchronized (mLock) { 158 return isCurrentSnackbarLocked(callback); 159 } 160 } 161 162 public boolean isCurrentOrNext(Callback callback) { 163 synchronized (mLock) { 164 return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback); 165 } 166 } 167 168 private static class SnackbarRecord { 169 final WeakReference<Callback> callback; 170 int duration; 171 172 SnackbarRecord(int duration, Callback callback) { 173 this.callback = new WeakReference<>(callback); 174 this.duration = duration; 175 } 176 177 boolean isSnackbar(Callback callback) { 178 return callback != null && this.callback.get() == callback; 179 } 180 } 181 182 private void showNextSnackbarLocked() { 183 if (mNextSnackbar != null) { 184 mCurrentSnackbar = mNextSnackbar; 185 mNextSnackbar = null; 186 187 final Callback callback = mCurrentSnackbar.callback.get(); 188 if (callback != null) { 189 callback.show(); 190 } else { 191 // The callback doesn't exist any more, clear out the Snackbar 192 mCurrentSnackbar = null; 193 } 194 } 195 } 196 197 private boolean cancelSnackbarLocked(SnackbarRecord record, int event) { 198 final Callback callback = record.callback.get(); 199 if (callback != null) { 200 // Make sure we remove any timeouts for the SnackbarRecord 201 mHandler.removeCallbacksAndMessages(record); 202 callback.dismiss(event); 203 return true; 204 } 205 return false; 206 } 207 208 private boolean isCurrentSnackbarLocked(Callback callback) { 209 return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback); 210 } 211 212 private boolean isNextSnackbarLocked(Callback callback) { 213 return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback); 214 } 215 216 private void scheduleTimeoutLocked(SnackbarRecord r) { 217 if (r.duration == Snackbar.LENGTH_INDEFINITE) { 218 // If we're set to indefinite, we don't want to set a timeout 219 return; 220 } 221 222 int durationMs = LONG_DURATION_MS; 223 if (r.duration > 0) { 224 durationMs = r.duration; 225 } else if (r.duration == Snackbar.LENGTH_SHORT) { 226 durationMs = SHORT_DURATION_MS; 227 } 228 mHandler.removeCallbacksAndMessages(r); 229 mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); 230 } 231 232 void handleTimeout(SnackbarRecord record) { 233 synchronized (mLock) { 234 if (mCurrentSnackbar == record || mNextSnackbar == record) { 235 cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT); 236 } 237 } 238 } 239 240} 241