├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── renny │ │ └── gifview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── renny │ │ │ └── gifview │ │ │ ├── GifImageView.java │ │ │ ├── MainActivity.java │ │ │ └── toast │ │ │ ├── AppToast.java │ │ │ ├── CustomToast.java │ │ │ ├── IToast.java │ │ │ ├── SystemToast.java │ │ │ ├── ToastCompat.java │ │ │ └── ToastHelper.java │ └── res │ │ ├── drawable-xxhdpi │ │ ├── meizhi.gif │ │ └── sky.gif │ │ ├── layout │ │ ├── activity_main.xml │ │ └── dialog_toast.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 │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── renny │ └── gifview │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scroll.gif └── settings.gradle /.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 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 37 | 38 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 86 | 96 | 97 | 98 | 99 | 100 | 101 | 103 | 104 | 105 | 106 | 107 | 1.8 108 | 109 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GifView 2 | Gif图片自定义显示 3 | ![](scroll.gif) 4 | 5 | 博客地址: 6 | [Gif图片自定义显示](https://juejin.im/post/59e95e09f265da430405bf33) 7 | ```html 8 | 16 | ``` 17 | 18 | ```html 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "26.0.2" 6 | defaultConfig { 7 | applicationId "com.renny.gifview" 8 | minSdkVersion 17 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:26.+' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | compile 'com.sdsmdg.tastytoast:tastytoast:0.1.1' 30 | testCompile 'junit:junit:4.12' 31 | } 32 | -------------------------------------------------------------------------------- /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 F:\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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/renny/gifview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview; 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("com.renny.gifview", 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/renny/gifview/GifImageView.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Canvas; 8 | import android.graphics.Movie; 9 | import android.os.SystemClock; 10 | import android.support.annotation.FloatRange; 11 | import android.support.v7.widget.AppCompatImageView; 12 | import android.util.AttributeSet; 13 | import android.view.View; 14 | 15 | import java.math.BigDecimal; 16 | 17 | 18 | /** 19 | * Created by LuckyCrystal on 2017/3/9. 20 | */ 21 | 22 | 23 | public class GifImageView extends AppCompatImageView { 24 | 25 | private static final int DEFAULT_DURATION = 1000; 26 | private float mScaleW = 1.0f; 27 | private float mScaleH = 1.0f; 28 | private float mScale = 1.0f; 29 | private Movie movie; 30 | //播放开始时间点 31 | private long mMovieStart; 32 | //播放暂停时间点 33 | private long mMoviePauseTime; 34 | //播放暂停时间 35 | private long offsetTime; 36 | //播放完成进度 37 | @FloatRange(from = 0, to = 1.0f) 38 | float percent; 39 | //播放次数,-1为循环播放 40 | private int counts = -1; 41 | 42 | private volatile boolean reverse = false; 43 | private volatile boolean mPaused; 44 | private volatile boolean hasStart; 45 | 46 | private boolean mVisible = true; 47 | 48 | private OnPlayListener mOnPlayListener; 49 | private int movieDuration; 50 | private boolean endLastFrame = false; 51 | 52 | public GifImageView(Context context) { 53 | this(context, null); 54 | } 55 | 56 | public GifImageView(Context context, AttributeSet attrs) { 57 | this(context, attrs, -1); 58 | } 59 | 60 | public GifImageView(Context context, AttributeSet attrs, int defStyle) { 61 | super(context, attrs, defStyle); 62 | setViewAttributes(context, attrs, defStyle); 63 | } 64 | 65 | private void setViewAttributes(Context context, AttributeSet attrs, int defStyle) { 66 | TypedArray a = context.obtainStyledAttributes(attrs, 67 | R.styleable.GifImageView, defStyle, 0); 68 | 69 | int srcID = a.getResourceId(R.styleable.GifImageView_gif_src, 0); 70 | boolean authPlay = a.getBoolean(R.styleable.GifImageView_auth_play, true); 71 | counts = a.getInt(R.styleable.GifImageView_play_count, -1); 72 | endLastFrame = a.getBoolean(R.styleable.GifImageView_end_last_frame, false); 73 | if (srcID > 0) { 74 | setGifResource(srcID, null); 75 | if (authPlay) play(counts); 76 | } 77 | a.recycle(); 78 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 79 | } 80 | 81 | public void setGifResource(int movieResourceId, OnPlayListener onPlayListener) { 82 | if (onPlayListener != null) { 83 | mOnPlayListener = onPlayListener; 84 | } 85 | reset(); 86 | movie = Movie.decodeStream(getResources().openRawResource(movieResourceId)); 87 | if (movie == null) { 88 | //如果movie为空,那么就不是gif文件,尝试转换为bitmap显示 89 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), movieResourceId); 90 | if (bitmap != null) { 91 | setImageBitmap(bitmap); 92 | return; 93 | } 94 | } 95 | movieDuration = movie.duration() == 0 ? DEFAULT_DURATION : movie.duration(); 96 | requestLayout(); 97 | } 98 | 99 | public void setGifResource(int movieResourceId) { 100 | setGifResource(movieResourceId, null); 101 | } 102 | 103 | public void setGifResource(final String path, OnPlayListener onPlayListener) { 104 | movie = Movie.decodeFile(path); 105 | mOnPlayListener = onPlayListener; 106 | reset(); 107 | if (movie == null) { 108 | Bitmap bitmap = BitmapFactory.decodeFile(path); 109 | if (bitmap != null) { 110 | setImageBitmap(bitmap); 111 | return; 112 | } 113 | } 114 | movieDuration = movie.duration() == 0 ? DEFAULT_DURATION : movie.duration(); 115 | requestLayout(); 116 | if (mOnPlayListener != null) { 117 | mOnPlayListener.onPlayStart(); 118 | } 119 | } 120 | 121 | //从新开始播放 122 | public void playOver() { 123 | if (movie != null) { 124 | play(-1); 125 | } 126 | } 127 | 128 | //倒叙播放 129 | public void playReverse() { 130 | if (movie != null) { 131 | reset(); 132 | reverse = true; 133 | if (mOnPlayListener != null) { 134 | mOnPlayListener.onPlayStart(); 135 | } 136 | invalidate(); 137 | } 138 | } 139 | 140 | public void play(int counts) { 141 | this.counts = counts; 142 | reset(); 143 | if (mOnPlayListener != null) { 144 | mOnPlayListener.onPlayStart(); 145 | } 146 | invalidate(); 147 | } 148 | 149 | private void reset() { 150 | reverse = false; 151 | mMovieStart = SystemClock.uptimeMillis(); 152 | mPaused = false; 153 | hasStart = true; 154 | mMoviePauseTime = 0; 155 | offsetTime = 0; 156 | } 157 | 158 | public void play() { 159 | if (movie == null) 160 | return; 161 | if (hasStart) { 162 | if (mPaused && mMoviePauseTime > 0) { 163 | mPaused = false; 164 | offsetTime = offsetTime + SystemClock.uptimeMillis() - mMoviePauseTime; 165 | invalidate(); 166 | if (mOnPlayListener != null) { 167 | mOnPlayListener.onPlayRestart(); 168 | } 169 | } 170 | } else { 171 | play(-1); 172 | } 173 | } 174 | 175 | public void pause() { 176 | if (movie != null && !mPaused && hasStart) { 177 | mPaused = true; 178 | invalidate(); 179 | mMoviePauseTime = SystemClock.uptimeMillis(); 180 | if (mOnPlayListener != null) { 181 | mOnPlayListener.onPlayPause(true); 182 | } 183 | } else { 184 | if (mOnPlayListener != null) { 185 | mOnPlayListener.onPlayPause(false); 186 | } 187 | } 188 | } 189 | 190 | private int getCurrentFrameTime() { 191 | if (movieDuration == 0) 192 | return 0; 193 | long now = SystemClock.uptimeMillis() - offsetTime; 194 | int nowCount = (int) ((now - mMovieStart) / movieDuration); 195 | if (counts != -1 && nowCount >= counts) { 196 | hasStart = false; 197 | if (mOnPlayListener != null) { 198 | mOnPlayListener.onPlayEnd(); 199 | } 200 | return endLastFrame ? movieDuration : 0; 201 | } 202 | float currentTime = (now - mMovieStart) % movieDuration; 203 | percent = currentTime / movieDuration; 204 | if (mOnPlayListener != null && hasStart) { 205 | BigDecimal mData = new BigDecimal(percent).setScale(2, BigDecimal.ROUND_HALF_UP); 206 | double f1 = mData.doubleValue(); 207 | f1 = f1 == 0.99 ? 1.0 : f1; 208 | mOnPlayListener.onPlaying((float) f1); 209 | } 210 | return (int) currentTime; 211 | } 212 | 213 | public void setPercent(float percent) { 214 | if (movie != null && movieDuration > 0) { 215 | this.percent = percent; 216 | movie.setTime((int) (movieDuration * percent)); 217 | invalidateView(); 218 | if (mOnPlayListener != null) { 219 | mOnPlayListener.onPlaying(percent); 220 | } 221 | } 222 | 223 | } 224 | 225 | public boolean isPaused() { 226 | return this.mPaused; 227 | } 228 | 229 | public boolean isPlaying() { 230 | return !this.mPaused && hasStart; 231 | } 232 | 233 | 234 | @Override 235 | protected void onDraw(Canvas canvas) { 236 | super.onDraw(canvas); 237 | if (movie != null) { 238 | if (!mPaused && hasStart) { 239 | if (reverse) { 240 | movie.setTime(movieDuration - getCurrentFrameTime()); 241 | } else { 242 | movie.setTime(getCurrentFrameTime()); 243 | } 244 | drawMovieFrame(canvas); 245 | invalidateView(); 246 | } else { 247 | drawMovieFrame(canvas); 248 | } 249 | } 250 | } 251 | 252 | /** 253 | * 画出gif帧 254 | */ 255 | private void drawMovieFrame(Canvas canvas) { 256 | canvas.save(); 257 | canvas.scale(1 / mScale, 1 / mScale); 258 | movie.draw(canvas, 0.0f, 0.0f); 259 | canvas.restore(); 260 | } 261 | 262 | @Override 263 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 264 | 265 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 266 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 267 | int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); 268 | int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); 269 | 270 | if (movie != null) { 271 | int movieWidth = movie.width(); 272 | int movieHeight = movie.height(); 273 | if (widthMode == MeasureSpec.EXACTLY) { 274 | mScaleW = ((float) movieWidth) / sizeWidth; 275 | } 276 | if (heightMode == MeasureSpec.EXACTLY) { 277 | mScaleH = ((float) movieHeight) / sizeHeight; 278 | } 279 | mScale = Math.max(mScaleW, mScaleH); 280 | setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth 281 | : movieWidth, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight 282 | : movieHeight); 283 | } else { 284 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 285 | } 286 | } 287 | 288 | 289 | private void invalidateView() { 290 | if (mVisible) { 291 | postInvalidateOnAnimation(); 292 | } 293 | } 294 | 295 | public int getDuration() { 296 | if (movie != null) { 297 | return movie.duration(); 298 | } else return 0; 299 | } 300 | 301 | @Override 302 | public void onScreenStateChanged(int screenState) { 303 | super.onScreenStateChanged(screenState); 304 | mVisible = screenState == SCREEN_STATE_ON; 305 | invalidateView(); 306 | } 307 | 308 | 309 | @Override 310 | protected void onVisibilityChanged(View changedView, int visibility) { 311 | super.onVisibilityChanged(changedView, visibility); 312 | mVisible = visibility == View.VISIBLE; 313 | invalidateView(); 314 | } 315 | 316 | @Override 317 | protected void onWindowVisibilityChanged(int visibility) { 318 | super.onWindowVisibilityChanged(visibility); 319 | mVisible = visibility == View.VISIBLE; 320 | invalidateView(); 321 | } 322 | 323 | 324 | public interface OnPlayListener { 325 | void onPlayStart(); 326 | 327 | void onPlaying(@FloatRange(from = 0f, to = 1.0f) float percent); 328 | 329 | void onPlayPause(boolean pauseSuccess); 330 | 331 | void onPlayRestart(); 332 | 333 | void onPlayEnd(); 334 | } 335 | 336 | } -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.FloatRange; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.SeekBar; 10 | import android.widget.TextView; 11 | 12 | import com.renny.gifview.toast.ToastHelper; 13 | 14 | public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener { 15 | GifImageView gifImageView; 16 | Button pauseBtn; 17 | boolean hasPaused = true; 18 | TextView percentTv; 19 | SeekBar mSeekBar; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | pauseBtn = (Button) findViewById(R.id.pause); 26 | pauseBtn.setVisibility(View.GONE); 27 | mSeekBar = (SeekBar) findViewById(R.id.seek); 28 | mSeekBar.setOnSeekBarChangeListener(this); 29 | percentTv = (TextView) findViewById(R.id.percent); 30 | gifImageView = (GifImageView) findViewById(R.id.gif); 31 | gifImageView.setGifResource(R.drawable.meizhi, new GifImageView.OnPlayListener() { 32 | @Override 33 | public void onPlayStart() { 34 | ToastHelper.getInstance(MainActivity.this.getApplication()).makeToast("开始"); 35 | } 36 | 37 | @Override 38 | public void onPlaying(@FloatRange(from = 0f, to = 1.0f) float percent) { 39 | int per = Math.round(percent * 100); 40 | mSeekBar.setProgress(per); 41 | percentTv.setText("播放进度: " + per + "%"); 42 | } 43 | 44 | @Override 45 | public void onPlayPause(boolean pauseSuccess) { 46 | if (pauseSuccess) 47 | ToastHelper.getInstance(MainActivity.this.getApplication()).makeToast("暂停成功"); 48 | else { 49 | ToastHelper.getInstance(MainActivity.this.getApplication()).makeToast("暂停失败"); 50 | } 51 | } 52 | 53 | @Override 54 | public void onPlayRestart() { 55 | ToastHelper.getInstance(MainActivity.this.getApplication()).makeToast("继续"); 56 | } 57 | 58 | @Override 59 | public void onPlayEnd() { 60 | ToastHelper.getInstance(MainActivity.this.getApplication()).makeToast("结束"); 61 | pauseBtn.setVisibility(View.GONE); 62 | } 63 | }); 64 | 65 | } 66 | 67 | public void palycycle(View view) { 68 | hasPaused = false; 69 | pauseBtn.setText("暂停"); 70 | gifImageView.play(-1); 71 | pauseBtn.setVisibility(View.VISIBLE); 72 | } 73 | 74 | public void palyreverse(View view) { 75 | hasPaused = false; 76 | pauseBtn.setText("暂停"); 77 | gifImageView.playReverse(); 78 | pauseBtn.setVisibility(View.VISIBLE); 79 | } 80 | 81 | public void palyone(View view) { 82 | hasPaused = false; 83 | pauseBtn.setText("暂停"); 84 | gifImageView.play(1); 85 | pauseBtn.setVisibility(View.VISIBLE); 86 | } 87 | 88 | public void pause(View view) { 89 | if (hasPaused) { 90 | pauseBtn.setText("继续"); 91 | gifImageView.play(); 92 | } else { 93 | pauseBtn.setText("暂停"); 94 | gifImageView.pause(); 95 | } 96 | hasPaused = !hasPaused; 97 | } 98 | 99 | @Override 100 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 101 | Log.d("onProgressChanged", " " + progress); 102 | float per = progress; 103 | gifImageView.setPercent(per / 100f); 104 | } 105 | 106 | @Override 107 | public void onStartTrackingTouch(SeekBar seekBar) { 108 | 109 | } 110 | 111 | @Override 112 | public void onStopTrackingTouch(SeekBar seekBar) { 113 | 114 | } 115 | 116 | 117 | public void load(View view) { 118 | gifImageView.setGifResource(R.drawable.sky); 119 | palyone(null); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/AppToast.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.content.Context; 4 | import android.view.Gravity; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.WindowManager; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | import com.renny.gifview.R; 12 | 13 | 14 | public class AppToast { 15 | private boolean isNotificationEnabled; 16 | 17 | public AppToast(Context context) { 18 | this.context = context.getApplicationContext(); 19 | this.windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 20 | isNotificationEnabled = ToastCompat.isNotificationEnabled(context); 21 | } 22 | 23 | /** 24 | * application context 25 | */ 26 | private Context context; 27 | private IToast toast = null; 28 | private IToast imgToast = null; 29 | private View textLayout = null; 30 | private View textImgLayout = null; 31 | private WindowManager windowManager; 32 | 33 | private IToast getToast() { 34 | cancelToast(); 35 | if (!isNotificationEnabled) { 36 | toast = new CustomToast(context); 37 | } else { 38 | toast = new SystemToast(context); 39 | } 40 | return toast; 41 | } 42 | 43 | private IToast getImgToast() { 44 | cancelImgToast(); 45 | if (!isNotificationEnabled) { 46 | imgToast = new CustomToast(context); 47 | } else { 48 | imgToast = new SystemToast(context); 49 | } 50 | return imgToast; 51 | } 52 | 53 | private synchronized View makeTextView(String text) { 54 | if (textLayout == null) { 55 | textLayout = LayoutInflater.from(context).inflate(R.layout.dialog_toast, null); 56 | } 57 | 58 | if (textLayout.getParent() != null) { 59 | windowManager.removeView(textLayout); 60 | } 61 | 62 | TextView mText = (TextView) textLayout.findViewById(R.id.toast_text); 63 | mText.setText(text); 64 | return textLayout; 65 | } 66 | 67 | 68 | public IToast makeToast_(String text) { 69 | IToast toast = getToast(); 70 | 71 | View layout = makeTextView(text); 72 | toast.setView(layout); 73 | toast.setGravity(Gravity.CENTER, 0, 0); 74 | toast.setDuration(Toast.LENGTH_SHORT); 75 | 76 | toast.show(); 77 | return toast; 78 | } 79 | 80 | public void makeToast(final String text) { 81 | 82 | makeToast_(text); 83 | 84 | } 85 | 86 | public void makeToast(int text) { 87 | makeToast(context.getString(text)); 88 | } 89 | 90 | 91 | public void cancelToast() { 92 | if (toast != null) { 93 | toast.cancel(); 94 | toast = null; 95 | } 96 | textLayout = null; 97 | } 98 | 99 | public void cancelImgToast() { 100 | if (imgToast != null) { 101 | imgToast.cancel(); 102 | imgToast = null; 103 | } 104 | textImgLayout = null; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/CustomToast.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.graphics.PixelFormat; 7 | import android.os.Build; 8 | import android.os.Handler; 9 | import android.view.Gravity; 10 | import android.view.View; 11 | import android.view.WindowManager; 12 | import android.widget.TextView; 13 | import android.widget.Toast; 14 | 15 | import java.util.concurrent.BlockingQueue; 16 | import java.util.concurrent.LinkedBlockingDeque; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | 19 | /** 20 | * Created by ttt on 2016/7/5. 21 | */ 22 | public class CustomToast implements IToast { 23 | 24 | private static Handler mHandler = new Handler(); 25 | 26 | /** 27 | * 维护toast的队列 28 | */ 29 | private static BlockingQueue mQueue = new LinkedBlockingDeque<>(); 30 | 31 | /** 32 | * 原子操作:判断当前是否在读取{@linkplain #mQueue 队列}来显示toast 33 | */ 34 | private static AtomicInteger mAtomicInteger = new AtomicInteger(0); 35 | 36 | private WindowManager mWindowManager; 37 | 38 | private long mDurationMillis; 39 | 40 | private View mView; 41 | 42 | private WindowManager.LayoutParams mParams; 43 | 44 | private Context mContext; 45 | 46 | public static IToast makeText(Context context, String text, long duration){ 47 | return new CustomToast(context) 48 | .setText(text) 49 | .setDuration(duration) 50 | .setGravity(Gravity.CENTER, 0, 0); 51 | } 52 | 53 | /** 54 | * 参照Toast源码TN()写 55 | * @param context 56 | */ 57 | public CustomToast(Context context){ 58 | mContext = context; 59 | mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 60 | mParams = new WindowManager.LayoutParams(); 61 | mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 62 | mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 63 | mParams.format = PixelFormat.TRANSLUCENT; 64 | mParams.windowAnimations = android.R.style.Animation_Toast; 65 | mParams.type = WindowManager.LayoutParams.TYPE_TOAST; 66 | mParams.setTitle("Toast"); 67 | mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 68 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 69 | // 默认居中 70 | mParams.gravity = Gravity.CENTER; 71 | } 72 | 73 | /** 74 | * Set the location at which the notification should appear on the screen. 75 | * 76 | * @param gravity 77 | * @param xOffset 78 | * @param yOffset 79 | */ 80 | // @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 81 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 82 | @Override 83 | public IToast setGravity(int gravity, int xOffset, int yOffset) { 84 | // We can resolve the Gravity here by using the Locale for getting 85 | // the layout direction 86 | final int finalGravity; 87 | if (Build.VERSION.SDK_INT >= 17){ 88 | final Configuration config = mView.getContext().getResources().getConfiguration(); 89 | finalGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection()); 90 | }else { 91 | finalGravity = gravity; 92 | } 93 | mParams.gravity = finalGravity; 94 | if ((finalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { 95 | mParams.horizontalWeight = 1.0f; 96 | } 97 | if ((finalGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { 98 | mParams.verticalWeight = 1.0f; 99 | } 100 | mParams.y = yOffset; 101 | mParams.x = xOffset; 102 | return this; 103 | } 104 | 105 | @Override 106 | public IToast setDuration(long durationMillis) { 107 | if (durationMillis < 0) { 108 | mDurationMillis = 0; 109 | } 110 | if (durationMillis == Toast.LENGTH_SHORT) { 111 | mDurationMillis = 2000; 112 | } else if (durationMillis == Toast.LENGTH_LONG) { 113 | mDurationMillis = 3500; 114 | } else { 115 | mDurationMillis = durationMillis; 116 | } 117 | return this; 118 | } 119 | 120 | /** 121 | * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 122 | * 123 | * @param view 传入view 124 | * 125 | * @return 自身对象 126 | */ 127 | @Override 128 | public IToast setView(View view) { 129 | mView = view; 130 | return this; 131 | } 132 | 133 | @Override 134 | public IToast setMargin(float horizontalMargin, float verticalMargin) { 135 | mParams.horizontalMargin = horizontalMargin; 136 | mParams.verticalMargin = verticalMargin; 137 | return this; 138 | } 139 | 140 | /** 141 | * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 142 | * 143 | * @param text 字符串 144 | * 145 | * @return 自身对象 146 | */ 147 | public IToast setText(String text) { 148 | // 模拟Toast的布局文件 com.android.internal.R.layout.transient_notification 149 | // 虽然可以手动用java写,但是不同厂商系统,这个布局的设置好像是不同的,因此我们自己获取原生Toast的view进行配置 150 | View view = Toast.makeText(mContext, text, Toast.LENGTH_SHORT).getView(); 151 | if (view != null){ 152 | TextView tv = (TextView) view.findViewById(android.R.id.message); 153 | tv.setText(text); 154 | setView(view); 155 | } 156 | return this; 157 | } 158 | 159 | @Override 160 | public void show() { 161 | // 1. 将本次需要显示的toast加入到队列中 162 | mQueue.offer(this); 163 | 164 | // 2. 如果队列还没有激活,就激活队列,依次展示队列中的toast 165 | if (0 == mAtomicInteger.get()){ 166 | mAtomicInteger.incrementAndGet(); 167 | mHandler.post(mActivite); 168 | } 169 | } 170 | 171 | @Override 172 | public void cancel() { 173 | // 1. 如果队列已经处于非激活状态或者队列没有toast了,就表示队列没有toast正在展示了,直接return 174 | if (0 == mAtomicInteger.get() && mQueue.isEmpty()) return; 175 | 176 | // 2. 当前显示的toast是否为本次要取消的toast,如果是的话 177 | // 2.1 先移除之前的队列逻辑 178 | // 2.2 立即暂停当前显示的toast 179 | // 2.3 重新激活队列 180 | if (this.equals(mQueue.peek())){ 181 | mHandler.removeCallbacks(mActivite); 182 | mHandler.post(mHide); 183 | mHandler.post(mActivite); 184 | } 185 | } 186 | 187 | private void handleShow() { 188 | if (mView != null) { 189 | if (mView.getParent() != null) { 190 | mWindowManager.removeView(mView); 191 | } 192 | mWindowManager.addView(mView, mParams); 193 | } 194 | } 195 | 196 | private void handleHide() { 197 | if (mView != null) { 198 | // note: checking parent() just to make sure the view has 199 | // been added... i have seen cases where we get here when 200 | // the view isn't yet added, so let's try not to crash. 201 | if (mView.getParent() != null) { 202 | mWindowManager.removeView(mView); 203 | // 同时从队列中移除这个toast 204 | mQueue.poll(); 205 | } 206 | mView = null; 207 | } 208 | } 209 | 210 | private static void activeQueue() { 211 | CustomToast toast = mQueue.peek(); 212 | if (toast == null){ 213 | // 如果不能从队列中获取到toast的话,那么就表示已经暂时完所有的toast了 214 | // 这个时候需要标记队列状态为:非激活读取 215 | mAtomicInteger.decrementAndGet(); 216 | }else { 217 | // 如果还能从队列中获取到toast的话,那么就表示还有toast没有展示 218 | // 1. 展示队首的toast 219 | // 2. 设置一定时间后主动采取toast消失措施 220 | // 3. 设置展示完毕之后再次执行本逻辑,以展示下一个toast 221 | mHandler.post(toast.mShow); 222 | mHandler.postDelayed(toast.mHide, toast.mDurationMillis); 223 | mHandler.postDelayed(mActivite, toast.mDurationMillis); 224 | } 225 | 226 | } 227 | 228 | private final Runnable mShow = new Runnable() { 229 | @Override 230 | public void run() { 231 | handleShow(); 232 | } 233 | }; 234 | 235 | private final Runnable mHide = new Runnable() { 236 | @Override 237 | public void run() { 238 | handleHide(); 239 | } 240 | }; 241 | 242 | private final static Runnable mActivite = new Runnable() { 243 | @Override 244 | public void run() { 245 | activeQueue(); 246 | } 247 | }; 248 | } 249 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/IToast.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * Created by ttt on 2016/7/5. 7 | */ 8 | public interface IToast { 9 | 10 | IToast setGravity(int gravity, int xOffset, int yOffset); 11 | 12 | IToast setDuration(long durationMillis); 13 | 14 | /** 15 | * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setText(String)} 16 | */ 17 | IToast setView(View view); 18 | 19 | IToast setMargin(float horizontalMargin, float verticalMargin); 20 | 21 | /** 22 | * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setText(String)} 23 | */ 24 | IToast setText(String text); 25 | 26 | void show(); 27 | 28 | void cancel(); 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/SystemToast.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.content.Context; 4 | import android.view.Gravity; 5 | import android.view.View; 6 | import android.widget.Toast; 7 | 8 | /** 9 | * Created by ttt on 2016/7/5. 10 | */ 11 | public class SystemToast implements IToast { 12 | private Toast mToast; 13 | 14 | // private Context mContext; 15 | 16 | public static IToast makeText(Context context, String text, long duration) { 17 | return new SystemToast(context) 18 | .setText(text) 19 | .setDuration(duration) 20 | .setGravity(Gravity.CENTER, 0, 0); 21 | } 22 | 23 | public SystemToast(Context context) { 24 | // mContext = context; 25 | mToast = new Toast(context); 26 | } 27 | 28 | @Override 29 | public IToast setGravity(int gravity, int xOffset, int yOffset) { 30 | mToast.setGravity(gravity, xOffset, yOffset); 31 | return this; 32 | } 33 | 34 | @Override 35 | public IToast setDuration(long durationMillis) { 36 | mToast.setDuration((int) durationMillis); 37 | return this; 38 | } 39 | 40 | /** 41 | * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 42 | * 43 | * @param view 传入view 44 | * @return 自身对象 45 | */ 46 | @Override 47 | public IToast setView(View view) { 48 | mToast.setView(view); 49 | return this; 50 | } 51 | 52 | @Override 53 | public IToast setMargin(float horizontalMargin, float verticalMargin) { 54 | mToast.setMargin(horizontalMargin, verticalMargin); 55 | return this; 56 | } 57 | 58 | /** 59 | * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 60 | * 61 | * @param text 传入字符串 62 | * @return 自身对象 63 | */ 64 | @Override 65 | public IToast setText(String text) { 66 | mToast.setText(text); 67 | return this; 68 | } 69 | 70 | @Override 71 | public void show() { 72 | if (mToast != null) { 73 | mToast.show(); 74 | } 75 | } 76 | 77 | @Override 78 | public void cancel() { 79 | if (mToast != null) { 80 | mToast.cancel(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/ToastCompat.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.app.AppOpsManager; 4 | import android.content.Context; 5 | import android.content.pm.ApplicationInfo; 6 | import android.view.View; 7 | 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.Method; 10 | 11 | /** 12 | * Created by ttt on 2016/7/5. 13 | */ 14 | public class ToastCompat implements IToast { 15 | 16 | private static final String CHECK_OP_NO_THROW = "checkOpNoThrow"; 17 | private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION"; 18 | private IToast mIToast; 19 | 20 | public ToastCompat(Context context) { 21 | this(context, null, -1); 22 | } 23 | 24 | ToastCompat(Context context, String text, int duration) { 25 | 26 | if (!(isNotificationEnabled(context))) { 27 | mIToast = CustomToast.makeText(context, text, duration); 28 | } else { 29 | mIToast = SystemToast.makeText(context, text, duration); 30 | } 31 | } 32 | 33 | public static IToast makeText(Context context, String text, int duration) { 34 | return new ToastCompat(context, text, duration); 35 | } 36 | 37 | @Override 38 | public IToast setGravity(int gravity, int xOffset, int yOffset) { 39 | return mIToast.setGravity(gravity, xOffset, yOffset); 40 | } 41 | 42 | @Override 43 | public IToast setDuration(long durationMillis) { 44 | return mIToast.setDuration(durationMillis); 45 | } 46 | 47 | /** 48 | * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 49 | * 50 | * @param view 51 | */ 52 | @Override 53 | public IToast setView(View view) { 54 | return mIToast.setView(view); 55 | } 56 | 57 | @Override 58 | public IToast setMargin(float horizontalMargin, float verticalMargin) { 59 | return mIToast.setMargin(horizontalMargin, verticalMargin); 60 | } 61 | 62 | /** 63 | * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} 64 | * 65 | * @param text 66 | */ 67 | @Override 68 | public IToast setText(String text) { 69 | return mIToast.setText(text); 70 | } 71 | 72 | @Override 73 | public void show() { 74 | mIToast.show(); 75 | } 76 | 77 | @Override 78 | public void cancel() { 79 | mIToast.cancel(); 80 | } 81 | 82 | /** 83 | * 判断维护Toast队列的NotificationManagerService通知权限是否被关闭 84 | * 85 | * @param context 86 | * @return 87 | */ 88 | public static boolean isNotificationEnabled(Context context) { 89 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) { 90 | return true; 91 | } 92 | AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 93 | ApplicationInfo appInfo = context.getApplicationInfo(); 94 | 95 | String pkg = context.getApplicationContext().getPackageName(); 96 | 97 | int uid = appInfo.uid; 98 | 99 | Class appOpsClass = null; /* Context.APP_OPS_MANAGER */ 100 | 101 | try { 102 | 103 | appOpsClass = Class.forName(AppOpsManager.class.getName()); 104 | 105 | Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class); 106 | 107 | Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION); 108 | int value = (int) opPostNotificationValue.get(Integer.class); 109 | return ((int) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED); 110 | 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | return true; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/renny/gifview/toast/ToastHelper.java: -------------------------------------------------------------------------------- 1 | package com.renny.gifview.toast; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * toast帮助类 7 | */ 8 | public class ToastHelper { 9 | private static AppToast appToast; 10 | 11 | //静态工厂方法 12 | public static AppToast getInstance(Context context) { 13 | if (appToast == null) { 14 | appToast = new AppToast(context); 15 | } 16 | return appToast; 17 | } 18 | 19 | public static void makeToast(int textId) { 20 | appToast.makeToast(textId); 21 | } 22 | 23 | public static void makeToast(String text) { 24 | appToast.makeToast(text); 25 | } 26 | 27 | 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/meizhi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ren93/GifView/6d6c6cafb9617e419105497d5b68406122c07921/app/src/main/res/drawable-xxhdpi/meizhi.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/sky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ren93/GifView/6d6c6cafb9617e419105497d5b68406122c07921/app/src/main/res/drawable-xxhdpi/sky.gif -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 25 | 26 | 32 | 33 |