支援GIF動畫的ImageView
網上有很多關於怎麼實現android播放GIF的帖子。但是本人發現,其中多多少少都有些不如人意的地方。因此,花了幾天時間,重寫了ImageView以實現GIF圖片的播放。在此小結一下,也希望可以給後來者一點參考。
大致我們會在網上搜到下面四種解決方法:
【方案一】用外部工具拆分GIF
【方案二】用Android開源專案GifView包
【方案三】手動解碼GIF
【方案四】用系統自帶的類Movie
本文采用方案四,繼承ImageView實現GIF動畫播放,支援ImageView的名稱空間屬性設定,支援ImageView通用介面。
專案原始碼下載地址:
【什麼是GIF】
GIF,就我的理解,就是很多張點陣圖圖片的集合,然後使用了某種編碼方式,使得它可以體積很小但是又夠清晰。由於體積小,不依賴特別的平臺,所以GIF很流行。
好吧,知道的就這麼多,各位看官想了解清楚的話還是請自行百度吧。不過了解了大概概念,我們就可以知道,其實讓GIF播放,實際就是顯示多張圖片而已。
【方案一】用外部工具拆分GIF
大概情況是這樣:
1,首先我們得有一張GIF (提示:選擇賞心悅目的動畫,可以提高學習興趣哦^_^)
2,然後使用工具,千刀萬剮將GIF分成多張圖片 => pic0.png,pic1.png,pic2.png,pic3.png,pic4.png,pic5.png
3,接著編寫android xml資原始檔放在drawable目錄下,說明各個幀圖片以及時間duration
4,然後程式碼裡面使用AnimationDrawable類即可實現
四張圖片按照xml定義的時間,一張張切換,看起來就是動畫了!
動畫資原始檔格式是這樣:(drawable/anim_gif.xml)
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:duration="150" android:drawable="@drawable/pic0" /> <item android:duration="150" android:drawable="@drawable/pic1" /> <item android:duration="150" android:drawable="@drawable/pic2" /> <item android:duration="150" android:drawable="@drawable/pic3" /> <item android:duration="150" android:drawable="@drawable/pic4" /> <item android:duration="150" android:drawable="@drawable/pic5" /> </animation-list>
佈局檔案可以是這樣:
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/anim_gif"
android:id="@+id/imgGif"></ImageView>
程式碼是這樣:
ImageView imageView = (ImageView) findViewById(R.id.imgGif);
Object ob = imageView.getBackground();
AnimationDrawable anim = (AnimationDrawable) ob;
anim.start();
如果我們需要在介面上顯示一個簡單且固定的動畫,單純用於點綴畫面,增強動感,這種方法比較方便。當然,這種方法在某些場合下顯得很不靈活,不能滿足要求效果,那麼請繼續參考後面的方法吧。
【方案二】用Android開源專案GifView包
我們同樣可以在網上搜到這個開源專案的相關應用。有了這個包,我們要讓GIF播放這個事情就變得非常輕鬆。看看它那強大的介面就知道了!使用GifView幾乎就跟ImageView是一樣的。方便!開源專案確實有很多程式碼都有非常好的學習價值,表示有空應該好好拜讀一番!
// 從xml中得到GifView的控制代碼
gif1 = (GifView) findViewById(R.id.gif1);
// 設定Gif圖片源
gif1.setGifImage(R.drawable.gif1);
// 新增監聽器
gif1.setOnClickListener(this);
// 設定顯示的大小,拉伸或者壓縮
gif1.setShowDimension(300, 300);
// 設定載入方式:先載入後顯示、邊載入邊顯示、只顯示第一幀再顯示
gif1.setGifImageType(GifImageType.COVER);
【方案三】手動解碼GIF
用java來解碼,很多人會覺得效率比較低。但是我們目的是學習,完全可以嘗試一下!當然,也可以用native程式碼完成解碼,在java用JNI呼叫。
將GIF檔案解碼後,我們可以得到所有想要的資訊。比如Gif版本GIF87a, GIF89a等等,關鍵是我們可以得到幾張Bitmap圖片,還有各張圖片的顯示延續時間。其實,這裡解碼工作也就大致等同於上面的方案一。不同的是,我們的app可以直接播放GIF,而不需要外部的工具!
有了各幀圖片以及顯示延續時間,我們便可以開始了!新建一個執行緒用於計時,時間一到就重新整理View切換圖片。這就是GIF了!
注意一下在非主執行緒讓View重新整理,應該呼叫postInvalidate() 而不是invalidate()。
下面參考帖子附有解碼源程式,然後按照參考文件來閱讀,很快可以看明白^_^
【方案四】用系統自帶的類Movie
接下來說說具體要講的基於Movie的實現方法吧!
使用Movie類播放GIF很簡單。但是我們的目的是,繼承ImageView,保留它顯示圖片的基本功能,儘量使得介面函式能夠通用簡便。這樣,原先使用ImageView的專案程式碼只要經過少量的修改,即可支援GIF動畫。根據要求,我們至少要過載下面四個通用介面以支援GIF動畫:
public void setImageResource( int resID )
public void setImageURI( Uri uri )
public void setScaleType( ScaleType scaleType )
public void setPadding( int left, int top, int right, int bottom )
說明一下:
// 我們設定了圖片,那麼跟ImageView一樣顯示出圖片
setImageResource( R.drawable.pngtest );
// 我們這次設定了GIF動畫,那麼應該顯示動畫
setImageResource( R.drawable.giftest );
// 支援SD卡中的GIF動畫
setImageURI( Uri.parse( "file://" + Environment.getExternalStorageDirectory().getPath() + "/sdcard_giftest.gif" );
為了實現以上要求,其中遇到很多問題,我們慢慢說吧。
-1- Movie 是啥東西
android.graphics.Movie 在SDK文件中沒有說明,翻看原始碼,發現它只是一個java殼,實際上直接呼叫native程式碼。這樣導致我們沒能快速學習掌握它的用法。
不過幸虧有APIDemo!這真的是一個好東西!開啟其中BitmapDecode我們可以發現程式碼中就用了Movie類!
直接安裝APIDemo到手機中,執行... 發現旗子飄動起來了!
它的原始碼簡單清晰,大概是這樣。Movie物件管理著時間軸上對應的GIF各幀圖片,我們通過傳入時間,便可以取出對應的幀,然後再用draw()方法,將當前的幀畫到畫布canvas上面。如果我們的View不停的重新整理,時間不停地跑,Movie的幀就不停的切換,那麼畫出來的View就動起來了!
-2- Copy APIDemo 原始碼
那麼好了,按照它的程式碼,我們可以很快copy一份出來,然後編譯安裝到手機,我們想GIF似乎就這樣完成了。關鍵程式碼如下。
private static class MovieGifView extends View {
private Movie mMovie;
private long mMovieStart;
public MovieGifView(Context context) {
super(context);
java.io.InputStream is;
is = context.getResources().openRawResource(R.drawable.animated_gif);
mMovie = Movie.decodeStream(is);
}
@Override
protected void onDraw(Canvas canvas) {
long now = android.os.SystemClock.uptimeMillis();
if (mMovieStart == 0) { // first time
mMovieStart = now;
}
if (mMovie != null) {
int dur = mMovie.duration();
if (dur == 0) {
dur = 1000;
}
int relTime = (int) ((now - mMovieStart) % dur);
mMovie.setTime(relTime);
mMovie.draw(canvas, getWidth() - mMovie.width(), getHeight() - mMovie.height());
invalidate();
}
}
}
但是結果卻是那麼不如人意,自己寫的app在一部平板(android 4.3)上執行時,GIF沒有動起來,美女並沒有向我眨眼!
我第一反應便是拿去我的屌絲神機I589(I5830電信版 android 2.3) 上試試。結果反而動起來了!這麼神馬回事?!
難道是android 4.3 版本太新,Movie方法不支援?後來我又找到了一部android 4.1 的手機,安裝發現,GIF同樣沒有動!
奇怪!頭疼!
-3- hardwareAccelerated 惹的禍
為什麼APIDemo的程式碼可以,我的程式碼直接copy,卻不行了?我翻看了很久程式碼,最後找到了唯一不同點,在這裡 -> AndroidManifest.xml
<activity android:hardwareAccelerated="false"
android:name=".graphics.BitmapDecode" android:label="Graphics/BitmapDecode">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
</activity>
BitmapDecode Activity 屬性設定中,有個東東不曾相識→ → android:hardwareAccelerated="false",不使能硬體加速?什麼概念?
於是便開始檢視各種說明,大概意思我是這麼理解的:硬體加速並不是什麼新鮮的東西,已經運用於windows composition 或 OpenGL games等等。而android 在 3.0之後的版本開始支援。但是它現在暫時只支援standard widgets and drawables。一旦使能硬體加速的特性,所有的畫圖工作都交給GPU來做。
但是,我們現在是自定義View類,使用Movie類的draw()方法畫圖,這個方法並沒有在硬體加速支援列表(如下)中找到蹤影。
The following table describes the support level of various operations across API levels:
API level | ||||
< 16 | 16 | 17 | 18 | |
Canvas | ||||
drawBitmapMesh() (colors array) | ✗ | ✗ | ✗ | ✓ |
drawPicture() | ✗ | ✗ | ✗ | ✗ |
drawPosText() | ✗ | ✓ | ✓ | ✓ |
drawTextOnPath() | ✗ | ✓ | ✓ | ✓ |
drawVertices() | ✗ | ✗ | ✗ | ✗ |
setDrawFilter() | ✗ | ✓ | ✓ | ✓ |
clipPath() | ✗ | ✗ | ✗ | ✓ |
clipRegion() | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.XOR) | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.Difference) | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.ReverseDifference) | ✗ | ✗ | ✗ | ✓ |
clipRect() with rotation/perspective | ✗ | ✗ | ✗ | ✓ |
所以,我們認為硬體加速不支援Movie draw()方法。而I589(android 2.3)本身沒有這個特性,所以不出現問題。而android 4.3平板具備硬體加速並預設開啟,而我們沒有關掉,所以出了問題。我準備嘗試關了這個特性再試試,再不行就死給你看!
關閉Hardware Acceleeration可以有幾種方法,針對不同的級別(Application, Activity, Window, View )。具體請詳見官方說明。
為了影響最小,可以使用View級別的 setLayerType(View.LAYER_TYPE_SOFTWARE, null);
關掉了硬體加速,我的GIF終於動起來了!美女開始眨眼,多麼好看的GIF動畫!歡呼吧!\(^o^)/
-4- 繼承ImageView
GIF已經動起來了,感覺事情已經搞定了一大半。但是後面發現,實際上不是這樣的!
再次說說我們的設計目標:設計一個類可以通過設定 Resource ID或者URI 播放圖片和GIF動畫。
我們自然想到繼承ImageView,然後加入GIF功能程式碼。這樣可以節省很多程式碼。但是檢視ImageView原始碼,很多private成員變數, private成員函式,真是讓人望而卻步呀。但是,耐心的啃一啃ImageView的原始碼,大概還是可以看出思路的。
-5- 解讀ImageView原始碼
個人認為ImageView原始碼最關鍵的部分,也是我們繼承它最需要考慮的問題有兩點:
- 控制元件的大小 (onMeasure()回撥方法)
- 圖片的大小與位置 (configureBounds()私有方法)
a) 圖片的大小與位置
先談談圖片的大小與位置吧,因為待會它在onMeasure方法中會用到。
兩個關鍵的成員變數 mDrawable, mDrawMatrix。一個是"圖片",一個是矩陣。mDrawable調節自身大小顏色透明度等等。mDrawMatrix則定義了畫布的縮放平移旋轉等等。在onDraw()方法呼叫之前,我們必須設定好這兩個變數,才能畫出正確的圖形。ImageView對這兩個變數的配置,大部分工作在configureBounds()方法中完成。
configureBounds()私有方法,根據當前View的大小(除去Padding部分)、Drawable實際大小、以及ScaleType引數,設定了圖片最終要顯示的大小以及對齊等屬性。而padding引數的平移效果,在onDraw()中通過平移畫布實現。
// [email protected] :
// configureBounds()方法的產物:mDrawable,mDrawMatrix
// onDraw()方法將使用這裡的mDrawable,mDrawMatrix作畫
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
int dwidth = mDrawableWidth;
int dheight = mDrawableHeight;
int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
int vheight = getHeight() - mPaddingTop - mPaddingBottom;
boolean fits = (dwidth < 0 || vwidth == dwidth) &&
(dheight < 0 || vheight == dheight);
// [email protected] : 以下根據ScaleType設定mDrawMatrix
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
/* If the drawable has no intrinsic size, or we're told to
scaletofit, then we just fill our entire view.
*/
// [email protected]1029 : fitXY的情況較簡單,直接將Drawable縮放至View的大小即可(除去padding)
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
// We need to do the scaling ourself, so have the drawable
// use its native size.
// [email protected] : 獲取圖片固有的大小
// dwidth = mDrawable.getIntrinsicWidth()
// dheight = mDrawable.getIntrinsicHeight()
// 涉及裝置畫素密度(density),圖片存放目錄(drawable-mdpi/drawable-hdpi)
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
} else if (fits) {
// The bitmap fits exactly, no transform needed.
mDrawMatrix = null;
} else if (ScaleType.CENTER == mScaleType) {
// Center bitmap in view, no scaling.
// [email protected] : 按原圖大小居中顯示,超過View長寬則擷取中間部分
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f),
(int) ((vheight - dheight) * 0.5f + 0.5f));
} else if (ScaleType.CENTER_CROP == mScaleType) {
mDrawMatrix = mMatrix;
float scale;
float dx = 0, dy = 0;
// [email protected] : centerCrop,長寬算出比例,然後取比例“大”的
if (dwidth * vheight > vwidth * dheight) {
scale = (float) vheight / (float) dheight;
dx = (vwidth - dwidth * scale) * 0.5f;
} else {
scale = (float) vwidth / (float) dwidth;
dy = (vheight - dheight * scale) * 0.5f;
}
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
} else if (ScaleType.CENTER_INSIDE == mScaleType) {
mDrawMatrix = mMatrix;
float scale;
float dx;
float dy;
// [email protected] : centerCrop,長寬算出比例,然後取比例“小”的
if (dwidth <= vwidth && dheight <= vheight) {
scale = 1.0f;
} else {
scale = Math.min((float) vwidth / (float) dwidth,
(float) vheight / (float) dheight);
}
dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f);
dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f);
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);
} else {
// [email protected] : 剩下fitCenter,fitStart,fitEnd三種
// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
一旦mDrawable或者mDrawMatrix需要改變的時候,configureBounds() 方法就會被呼叫。大概是這樣子的:
只要呼叫invalidate()方法,onDraw()方法就會被呼叫,從而重新整理介面。
我們現在關心的是,播放GIF的時候,我們手上是Movie物件,而不是Drawable物件。因此,用於Drawable的位置計算,不能適用於Movie的場合。ImageView可以通過Drawable setBounds()方法設定大小,Movie卻沒有這種方法,因此我們只能通過縮放畫布(canvas)來實現相同的效果。
至於怎麼用Matrix矩陣來變換畫圖,請移步:
b) 控制元件的大小
控制元件的大小需要在onMeasure()方法中設定,使用setMeasuredDimension()方法。根據輸入引數int widthMeasureSpec, int heightMeasureSpec 可以得到父容器(Layout)提供的Measure模式Mode以及參考大小Size。MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST, MeasureSpec.EXACTLY。
對應這三種模式的不同設定方法,ImageView原始碼註釋得很清楚(resolveAdjustedSize()方法中)。我們到時copy它的程式碼就可以(因為是private方法,子類不可以呼叫)。
重寫onMeasure()方法,我們歸為一點:把裡面的mDrawable替換為mMovie即可。
但是,執行完onMeasure()後,如果立即去getWidth(), getHeight(),我們只會得到舊值!如果想要onMeasure()後,立即計算圖片的縮放移動旋轉引數,那麼需要用getMeasuredWidth()和getMeasuredHeight()代替。
呼叫requestLayout()方法,可以觸發onMeasure()方法。
-6- MovieImageView
程式碼比較長,這裡直接貼上一個測試沒有問題的版本。
這裡只實現新增方法setMovie(),如果想實現setImageResource(), setImageUri(),只要將以下的程式碼稍作修改,將Movie物件的初始化部分放到MovieImageView類裡頭完成,即可實現!
package com.yarkey.giftest2;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Movie;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
public class MovieImageView extends ImageView {
private static final boolean DB = true;
private static final boolean DB_DETAIL = false;
private static final String DB_TAG = "MovieImageView";
/** Feature Support GIF */
private static final boolean FEATURE_IS_GIF_SUPPORTED = true;
/** @see #syncParentParameter() */
private int mSuperPaddingTop;
private int mSuperPaddingLeft;
private int mSuperPaddingRight;
private int mSuperPaddingBottom;
private ScaleType mSuperScaleType;
private Matrix mSuperDrawMatrix;
/** mMovie==null means we work the same as parent(ImageView) */
private Movie mMovie = null;
private Matrix mMatrix;
private Matrix mDrawMatrix;
private long mMovieStartTime = 0;
private long mMovieDuration = 0;
private int mDefLayerType;
// AdjustViewBounds behavior will be in compatibility mode for older apps.
private boolean mAdjustViewBoundsCompat = false;
public MovieImageView(Context context) {
super(context);
initGifAndImageView();
}
public MovieImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MovieImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initGifAndImageView();
}
private void prepareForMovie(boolean isToDo) {
if (FEATURE_IS_GIF_SUPPORTED && isToDo) {
if (getLayerType() != View.LAYER_TYPE_SOFTWARE) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
setWillNotCacheDrawing(false);
mMovieStartTime = 0;
} else if (mDefLayerType != 0 && mDefLayerType != getLayerType()) {
setLayerType(mDefLayerType, null);
mMovie = null;
}
}
/**
* You may open an inputstream of certain GIF file, and then decode by
* Movie.decodeStream.
*
* @param movie
*/
public void setMovie(Movie movie) {
Log("setMovie");
if (FEATURE_IS_GIF_SUPPORTED && mMovie != movie) {
mMovie = movie;
if (mMovie != null) {
prepareForMovie(true);
mMovieDuration = mMovie.duration();
requestLayout();
// configureDrawMatrix();//will get called after onMeasures
} else {
prepareForMovie(false);
}
invalidate();
}
}
private void syncParentParameter() {
Log("syncParentParameter");
mSuperPaddingTop = getPaddingTop();
mSuperPaddingLeft = getPaddingLeft();
mSuperPaddingRight = getPaddingRight();
mSuperPaddingBottom = getPaddingBottom();
mSuperScaleType = getScaleType();
mSuperDrawMatrix = getImageMatrix();
}
@Override
public void setImageBitmap(Bitmap bm) {
prepareForMovie(false);
super.setImageBitmap(bm);
}
@Override
public void setImageDrawable(Drawable drawable) {
prepareForMovie(false);
super.setImageDrawable(drawable);
}
@Override
public void setImageResource(int resId) {
prepareForMovie(false);
super.setImageResource(resId);
}
@Override
public void setImageURI(Uri uri) {
prepareForMovie(false);
super.setImageURI(uri);
}
@Override
public void setScaleType(ScaleType scaleType) {
super.setScaleType(scaleType);
configureDrawMatrix();
}
@Override
public void setImageMatrix(Matrix matrix) {
super.setImageMatrix(matrix);
// We should do the following whether we are in "MovieMode" or not,
// because we can not get the matrix from
// parent later.
if (matrix != null && matrix.isIdentity()) {
matrix = null;
}
if (matrix == null && !mMatrix.isIdentity() || matrix != null && !mMatrix.equals(matrix)) {
mMatrix.set(matrix);
configureDrawMatrix();
invalidate();
}
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
configureDrawMatrix();
}
private void initGifAndImageView() {
Log("initGifAndImageView");
if (FEATURE_IS_GIF_SUPPORTED) {
mMatrix = new Matrix();
mDefLayerType = getLayerType();
}
mAdjustViewBoundsCompat = this.getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
}
private void configureDrawMatrix() {
Log("configureDrawMatrix");
if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) {
return;
}
// getWidth/Height() aren't valid until after a layout
if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0)
return;
syncParentParameter();
int movieWidth = mMovie.width();// 實際畫素
int movieHeight = mMovie.height();
Log("movieWidth = " + movieWidth + ", movieHeight = " + movieHeight);
// in pixels
// int vWidth = getWidth() - mSuperPaddingLeft - mSuperPaddingRight;
// int vHeight = getHeight() - mSuperPaddingTop - mSuperPaddingBottom;
int vWidth = getMeasuredWidth() - mSuperPaddingLeft - mSuperPaddingRight;
int vHeight = getMeasuredHeight() - mSuperPaddingTop - mSuperPaddingBottom;
Log("vWidth = " + vWidth + ", vHeight = " + vHeight);
if (ScaleType.CENTER == mSuperScaleType) {
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate((int) ((vWidth - movieWidth) * 0.5f + 0.5f),
(int) ((vHeight - movieHeight) * 0.5f + 0.5f));
} else if (ScaleType.CENTER_CROP == mSuperScaleType) {
mDrawMatrix = mMatrix;
float scale = Math.max((float) vHeight / (float) movieHeight, (float) vWidth / (float) movieWidth);
float dx = (vWidth - movieWidth * scale) * 0.5f;
float dy = (vHeight - movieHeight * scale) * 0.5f;
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
} else if (ScaleType.CENTER_INSIDE == mSuperScaleType) {
mDrawMatrix = mMatrix;
float scale;
if (movieWidth <= vWidth && movieHeight <= vHeight) {
scale = 1.0f;
} else {
scale = Math.min((float) vWidth / (float) movieWidth, (float) vHeight / (float) movieHeight);
}
float dx = (int) ((vWidth - movieWidth * scale) * 0.5f + 0.5f);
float dy = (int) ((vHeight - movieHeight * scale) * 0.5f + 0.5f);
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);
} else if (ScaleType.FIT_XY == mSuperScaleType) {
mDrawMatrix = mMatrix;
float scaleX = (float) vWidth / (float) movieWidth;
float scaleY = (float) vHeight / (float) movieHeight;
Log("ScaleType.FIT_XY, scaleX = " + scaleX + ", scaleY = " + scaleY);
mDrawMatrix.setScale(scaleX, scaleY);
// mDrawMatrix.postTranslate(mSuperPaddingLeft, mSuperPaddingTop);
} else if (ScaleType.MATRIX == mSuperScaleType) {
mDrawMatrix = mSuperDrawMatrix;
} else { /* fit */
mDrawMatrix = mMatrix;
float scale = Math.min((float) vHeight / (float) movieHeight, (float) vWidth / (float) movieWidth);
float dx = 0.0f;
float dy = 0.0f;
if (ScaleType.FIT_START == mSuperScaleType) {
// dx = 0.0f;
// dy = 0.0f;
} else if (ScaleType.FIT_CENTER == mSuperScaleType) {
dx = (vWidth - movieWidth * scale) * 0.5f + 0.5f;
dy = (vHeight - movieHeight * scale) * 0.5f + 0.5f;
} else {/* ScaleType.FIT_END == mSuperScaleType */
dx = vWidth - movieWidth * scale;
dy = vHeight - movieHeight * scale;
}
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate((int) dx, (int) dy);
}
}
private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) {
Log("resolveAdjustedSize, desiredSize=" + desiredSize + ",maxSize=" + maxSize + ",measureSpec=" + measureSpec);
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
/*
* Parent says we can be as big as we want. Just don't be larger
* than max size imposed on ourselves.
*/
result = Math.min(desiredSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be larger than
// the max size imposed on ourselves.
result = Math.min(Math.min(desiredSize, specSize), maxSize);
break;
case MeasureSpec.EXACTLY:
// "60dp"
// No choice. Do what we are told.
result = specSize;
break;
}
return result;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log("onMeasure");
if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
syncParentParameter();
int w;
int h;
// Desired aspect ratio of the view's contents (not including padding)
float desiredAspect = 0.0f;
// We are allowed to change the view's width
boolean resizeWidth = false;
// We are allowed to change the view's height
boolean resizeHeight = false;
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (mMovie == null) {
w = h = 0;
} else {
w = mMovie.width();
h = mMovie.height();
Log("onMeasure, w = " + w + ", h = " + h);
if (w <= 0)
w = 1;
if (h <= 0)
h = 1;
// We are supposed to adjust view bounds to match the aspect
// ratio of our drawable. See if that is possible.
if (getAdjustViewBounds()) {
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
}
}
int widthSize, heightSize;
Log("onMeasure, resizeWidth=" + resizeWidth + ", resizeHeight=" + resizeHeight);
if (resizeWidth || resizeHeight) {
int maxWidth = getMaxWidth();
int maxHeight = getMaxHeight();
/*
* If we get here, it means we want to resize to match the drawables
* aspect ratio, and we have the freedom to change at least one
* dimension.
*/
// Get the max possible width given our constraints
widthSize = resolveAdjustedSize(w + mSuperPaddingLeft + mSuperPaddingRight, maxWidth, widthMeasureSpec);
// Get the max possible height given our constraints
heightSize = resolveAdjustedSize(h + mSuperPaddingTop + mSuperPaddingBottom, maxHeight, heightMeasureSpec);
if (desiredAspect != 0.0f) {
// See what our actual aspect ratio is
float actualAspect = (float) (widthSize - mSuperPaddingLeft - mSuperPaddingRight)
/ (heightSize - mSuperPaddingTop - mSuperPaddingBottom);
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
// Try adjusting width to be proportional to height
if (resizeWidth) {
int newWidth = (int) (desiredAspect * (heightSize - mSuperPaddingTop - mSuperPaddingBottom))
+ mSuperPaddingLeft + mSuperPaddingRight;
// Allow the width to outgrow its original estimate if
// height is fixed.
if (!resizeHeight && !mAdjustViewBoundsCompat) {
widthSize = resolveAdjustedSize(newWidth, maxWidth, widthMeasureSpec);
}
if (newWidth <= widthSize) {
widthSize = newWidth;
done = true;
}
}
// Try adjusting height to be proportional to width
if (!done && resizeHeight) {
int newHeight = (int) ((widthSize - mSuperPaddingLeft - mSuperPaddingRight) / desiredAspect)
+ mSuperPaddingTop + mSuperPaddingBottom;
// Allow the height to outgrow its original estimate if
// width is fixed.
if (!resizeWidth && !mAdjustViewBoundsCompat) {
heightSize = resolveAdjustedSize(newHeight, maxHeight, heightMeasureSpec);
}
if (newHeight <= heightSize) {
heightSize = newHeight;
}
}
}
}
} else {
/*
* We are either don't want to preserve the drawables aspect ratio,
* or we are not allowed to change view dimensions. Just measure in
* the normal way.
*/
w += mSuperPaddingLeft + mSuperPaddingRight;
h += mSuperPaddingTop + mSuperPaddingBottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
Log("onMeasure, widthSize=" + widthSize + ", heightSize=" + heightSize);
setMeasuredDimension(widthSize, heightSize);
configureDrawMatrix();
}
@Override
protected void onDraw(Canvas canvas) {
if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) {
super.onDraw(canvas);
return;
}
// Movie set time
if (mMovieDuration == 0) {
mMovie.setTime(0);
} else {
long now = android.os.SystemClock.uptimeMillis();
if (mMovieStartTime == 0) {
mMovieStartTime = now;// first time
}
mMovie.setTime((int) ((now - mMovieStartTime) % mMovieDuration));
}
// save the current matrix and clip of canvas
int saveCount = canvas.getSaveCount();
canvas.save();
boolean superCropToPadding = getCropToPadding();
Log("superCropToPadding = " + superCropToPadding, DB_DETAIL);
if (superCropToPadding) {
int superScrollX = getScrollX();
int superScrollY = getScrollY();
int superRight = getRight();
int superLeft = getLeft();
int superBottom = getBottom();
int superTop = getTop();
canvas.clipRect(superScrollX + mSuperPaddingLeft, superScrollY + mSuperPaddingTop, superScrollX
+ superRight - superLeft - mSuperPaddingRight, superScrollY + superBottom - superTop
- mSuperPaddingBottom);
}
if (mDrawMatrix != null && !mDrawMatrix.isIdentity()) {
canvas.concat(mDrawMatrix);
}
mMovie.draw(canvas, mSuperPaddingLeft, mSuperPaddingTop);
canvas.restoreToCount(saveCount);
invalidate();
}
@Override
public void setWillNotCacheDrawing(boolean willNotCacheDrawing) {
if (FEATURE_IS_GIF_SUPPORTED && mMovie != null) {
super.setWillNotCacheDrawing(false);
} else {
super.setWillNotCacheDrawing(willNotCacheDrawing);
}
}
private static void Log(String log) {
if (DB) {
Log.i(DB_TAG, log);
}
}
private static void Log(String log, boolean enable) {
if (enable) {
Log.i(DB_TAG, log);
}
}
}
初始化Movie物件以及測試用程式碼:
package com.yarkey.giftest2;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import android.app.Activity;
import android.graphics.Movie;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
public class MainActivity extends Activity implements OnClickListener {
private static final String TAG = "MainActivity";
ImageView image2;
MovieImageView mView;
Button mBtnA, mBtnB, mBtnC, mBtnD;
static final String ExternalPath = Environment.getExternalStorageDirectory().getPath();
// setVisibility test
private static int sIndexA = 0;
private static int[] sVisibles = new int[] { View.INVISIBLE, View.GONE, View.VISIBLE };
private static String[] sDescrA = new String[] { "INVISIBLE", "GONE", "VISIBLE" };
// setScaleType test
private static int sIndexB = 0;
private static ScaleType[] sScaleTypes = new ScaleType[] { ScaleType.CENTER, ScaleType.CENTER_CROP,
ScaleType.CENTER_INSIDE, ScaleType.FIT_CENTER, ScaleType.FIT_END, ScaleType.FIT_START, ScaleType.FIT_XY };
private static String[] sDescrB = new String[] { "CENTER", "CENTER_CROP", "CENTER_INSIDE", "FIT_CENTER", "FIT_END",
"FIT_START", "FIT_XY" };
// setImageResource test
private static int sIndexC = 0;
private static int[] sResources = new int[] { R.drawable.pngtest1, R.drawable.giftest1, R.drawable.giftest2,
R.drawable.giftest3 };
// setImageUri test
private static int sIndexD = 0;
private static Uri[] sImageUris = new Uri[] { Uri.parse("file://" + ExternalPath + "/giftest1.gif"),
Uri.parse("file://" + ExternalPath + "/giftest2.gif"),
Uri.parse("file://" + ExternalPath + "/giftest3.gif") };
// setMovie test
private static int sIndexE = 0;
private static Movie[] sMovies = new Movie[4];
private static String[] sDescrE = new String[] { "Movie1", "Movie2", "Movie3", "Movie4" };
private static int sIndexF = 0;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
setContentView(R.layout.activity_main);
image2 = (ImageView) this.findViewById(R.id.image2);
mView = (MovieImageView) this.findViewById(R.id.gifView);
mBtnA = (Button) this.findViewById(R.id.btnA);
mBtnB = (Button) this.findViewById(R.id.btnB);
mBtnC = (Button) this.findViewById(R.id.btnC);
mBtnD = (Button) this.findViewById(R.id.btnD);
mBtnA.setOnClickListener(this);
mBtnB.setOnClickListener(this);
mBtnC.setOnClickListener(this);
mBtnD.setOnClickListener(this);
image2 = (ImageView) this.findViewById(R.id.image2);
InputStream uriInputStream = null;
try {
uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[0]));
uriInputStream.mark(uriInputStream.available());
sMovies[0] = Movie.decodeStream(uriInputStream);
uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[1]));
uriInputStream.mark(uriInputStream.available());
sMovies[1] = Movie.decodeStream(uriInputStream);
uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[2]));
uriInputStream.mark(uriInputStream.available());
sMovies[2] = Movie.decodeStream(uriInputStream);
uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(
Uri.parse("android.resource://com.yarkey.giftest2/" + R.raw.largegif)));
uriInputStream.mark(uriInputStream.available());
sMovies[3] = Movie.decodeStream(uriInputStream);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void onClick(View arg0) {
Log.i("MainActivity", "onClick");
switch (arg0.getId()) {
case R.id.btnA:
image2.setVisibility(sVisibles[sIndexA]);
mView.setVisibility(sVisibles[sIndexA]);
mBtnA.setText("Visible: " + sDescrA[sIndexA]);
sIndexA++;
if (sIndexA >= sVisibles.length) {
sIndexA = 0;
}
break;
case R.id.btnB:
image2.setScaleType(sScaleTypes[sIndexB]);
mView.setScaleType(sScaleTypes[sIndexB]);
mBtnB.setText("ScaleType: " + sDescrB[sIndexB]);
sIndexB++;
if (sIndexB >= sScaleTypes.length) {
sIndexB = 0;
}
break;
case R.id.btnC:
image2.setImageResource(R.drawable.ic_launcher);
mView.setMovie(sMovies[sIndexE]);
mBtnC.setText("setMovie: " + sDescrE[sIndexE]);
sIndexE++;
if (sIndexE >= sMovies.length) {
sIndexE = 0;
}
break;
case R.id.btnD:
switch (sIndexF) {
case 0:
image2.setPadding(100, 0, 0, 0);
mView.setPadding(100, 0, 0, 0);
mBtnD.setText("setPadding: 100,0,0,0");
break;
case 1:
image2.setPadding(0, 100, 0, 0);
mView.setPadding(0, 100, 0, 0);
mBtnD.setText("setPadding: 0,100,0,0");
break;
case 2:
image2.setPadding(0, 0, 100, 0);
mView.setPadding(0, 0, 100, 0);
mBtnD.setText("setPadding: 0,0,100,0");
break;
case 3:
image2.setPadding(0, 0, 0, 100);
mView.setPadding(0, 0, 0, 100);
mBtnD.setText("setPadding: 0,0,0,100");
break;
case 4:
image2.setPadding(100, 100, 100, 100);
mView.setPadding(100, 100, 100, 100);
mBtnD.setText("setPadding: 100,100,100,100");
break;
case 5:
image2.setPadding(0, 0, 0, 0);
mView.setPadding(0, 0, 0, 0);
mBtnD.setText("setPadding: no");
break;
}
sIndexF++;
if (sIndexF == 6) {
sIndexF = 0;
}
break;
}
}
}
佈局檔案:
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFBBBBBB"
android:gravity="center"
android:padding="10dp"
android:text="InCallScreen"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageView
android:id="@+id/image2"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#AACCFF"
android:scaleType="center" />
<com.yarkey.giftest2.MovieImageView
android:id="@+id/gifView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#88AADD"
android:scaleType="center" />
<Button
android:id="@+id/btnA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Visible:VISIBLE" />
<Button
android:id="@+id/btnB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="ScaleType:default" />
<Button
android:id="@+id/btnC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="setMovie:null" />
<Button
android:id="@+id/btnD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="setPadding:no" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
</LinearLayout>
</LinearLayout>
</ScrollView>
好吧,整個帖子寫得我自己都覺得非常亂了。
不過,記住一下ImageView流程也好: setImageDrawable -> configureBounds -> requestLayout -> onMeasure -> setFrame -> onDraw
我們計算Movie在View中顯示的位置大小平移等效果,必須在setFrame函式呼叫後,才能執行(此時getWidth(), getHeight() 才會生效)。
寫得太亂了,我的天!就此斷了吧。有空再整理。
Best regards !
相關推薦
支援GIF動畫的ImageView
網上有很多關於怎麼實現android播放GIF的帖子。但是本人發現,其中多多少少都有些不如人意的地方。因此,花了幾天時間,重寫了ImageView以實現GIF圖片的播放。在此小結一下,也希望可以給後來者一點參考。 大致我們會在網上搜到下面四種解決方法:
能夠播放gif動畫的ImageView
ima data googl factor ida nal settime canvas off 一般ImageView並不能播放gif動畫。 此處播放gif動畫的核心是: 1.將gif中的每一幀拿出來,然後使用Movie類的setTime()和draw()這兩
php圖片等比壓縮程式碼,支援jpg,png,gif,驗證gif動畫
<?php /** * Created by PhpStorm. * User: as * Date: 2016-12-01 * Time: 8:38 */ //圖片壓縮 function ImageCondens($filepase){ list(
Ubuntu中的Gif動畫錄制工具
16px ppa bubuko order -c eight name linux win 為了在隨筆中插入gif動態圖Windows系統上可以使用ScreenToGif這個非常好用的小軟件,在Ubuntu系統中選擇也很多(可以參考最下面的鏈接),下面介紹兩款ubunt
Android開源項目:GifView——Android顯示GIF動畫
down pan 常用 ets ole lan parse googl ima 下載:http://code.google.com/p/gifview/downloads/list 簡介:android中現在沒有直接顯示gif的view,只能通過mediaplay來顯示,
怎麽制作GIF動態圖,在線GIF動畫制作工具哪個好
img 技術 工具制作 png 彈出 需要 輕松 watermark 全部 貼吧聊天中,我們經常會使用到GIF動態圖片,看著吧裏那些各式各樣的GIF動態圖,難免都想自己親手制作一張,那麽如何錄制GIF動態圖片呢?其實制作gif動態圖片還是比較容易得,有一款迅捷GIF制作工具
c++builder使用Image顯示gif動畫
cb中,使用TGifimage這個類可以使Image顯示Gif動畫 // Image1:TImage; // Image1.Picture.LoadFromFile(OpenDialog1.FileName); TGIFImage(Image1.Picture.Graphic).Anima
如何擷取視訊轉gif動畫圖片
我每當拍錄有一些搞笑有趣的視訊,就忍不住想發給朋友們分享下。原先我會直接將視訊上傳土豆的,最近發現還有一種好玩的方法,就是擷取視訊中的精華片段做成gif圖。一般縮小在1 MB以內還能當QQ表情,直接在聊天視窗發給朋友看,更直觀,檔案小也便於收藏,有木有?!想學如何擷取視訊轉GIF動畫圖片麼?有興趣的朋友可
用什麼軟體能把flv視訊轉成GIF動畫
其實網上有一部分的GIF動畫來來自於視訊(電影、電視劇、綜藝、動漫),特別是一些炫酷的GIF特效。聊天軟體也偏愛GIF動畫,比方說QQ、微信都有很多GIF動畫為大家提供。並且大家與好友聊天鬥圖應該大部分使用的都是GIF動畫,因為GIF動畫生動形象能夠很好的表達我們的各種心情。那麼用什麼軟體能把視訊轉成GI
視訊轉換器怎麼將視訊轉成GIF動畫
其實網上有一部分的GIF動畫來來自於視訊(電影、電視劇、綜藝、動漫),特別是一些炫酷的GIF特效。聊天軟體也偏愛GIF動畫,比方說QQ、微信都有很多GIF動畫為大家提供。並且大家與好友聊天鬥圖應該大部分使用的都是GIF動畫,因為GIF動畫生動形象能夠很好的表達我們的各種心情。那麼用什麼軟體能把視訊轉成
第二篇-ubuntu18.04下怎麽制作GIF動畫
http inf 點擊 ubunt 坐標 content ubuntu con ont 一、在桌面打開終端 二、接著通過apt安裝byzanz。sudo apt-get install byzanz 三、安裝完成後在終端執行“xwininfo”。xwininfo 四、然後鼠
製作GIF動畫簡單教程,製作GIF動畫簡單方法
GIF動畫在QQ和微信聊天中我們經常需要用到(你們肯定都鬥過圖)。那麼我們如何製作GIF動畫呢?製作GIF動畫又有哪些簡單的方法呢?下面小編就來分享給大家,教大家如何使用迅捷GIF製作工具製作GIF動畫。 迅捷GIF製作工具http://www.xunjieshipin.com/d
使用 byzanz ubuntu16.0.4 下錄製gif動畫
在程式設計師的部落格中,為了減少程式碼的書寫,我們需要實用簡潔的展現方式來展現我們的程式碼和操作, gif 動畫非常符合我們的實際需求,那麼在Ubuntu的桌面系統中有那些好用的工具呢? 筆者之前在別的博文中看到了一篇博文,介紹的軟體包非常好用,名字叫做byzanz 安
進度條製作-GIF動畫
網頁進度條製作 一、為什麼需要網頁進度條 隨著HTML的普及,各種CSS3動畫及特效在網頁中層出不窮,PC端載入資料的速度還算可以,移動端相對要慢很多,如果圖片或指令碼沒有完全載入,使用者在操作中可能發生各種問題,因此我們需要對載入資料進行檢測,以更加人性化的方式給使用者展現。
普通照片新增GIF動畫聖誕燈串閃爍效果PS動作
這是一個PS動作教程哦,利用這個動作即可製作GIF動態圖片效果,需要這個動作可以到陌魚社群搜一下“製作閃爍聖誕燈串GIF動態圖片效果PS動作”下載,下面請看演示教程。 01、編輯-預設-預設管理器,把我們壓縮包內的畫筆(.abr)載入進去。 02、視窗-動作,點
android gif動畫解決 fresco方法 Android Studio 3.0環境
載入和播放順滑流暢,推薦使用 1、引入fresco 編輯 build.gradle 檔案 dependencies { // 其他依賴 compile 'com.facebook.fresco:fresco:0.12.0' / 在 API < 14 上的機
【PS教程】製作逼真下雨GIF動畫效果
有不少小夥伴都想知道該怎麼製作逼真下雨GIF動畫效果,今天小編為大家帶來了用ps製作下雨動態圖片完整版詳細教程,如果你想知道該怎麼製作就快來看看吧!學會之後自己就可以輕鬆製作逼真下雨GIF動畫效果了! Adobe Photoshop CC 2018 for Mac(ps mac破解版)附啟用工具
視訊轉gif 如何將視訊製作gif動畫圖片
在微信聊天的時候,經常使用動態表情包的大家肯定不難發現,很多動圖是用網上的綜藝節目或電視劇電影中的某個搞笑片段製作成的。我們聊天用的很多表情包都存在雷同的現象,要是大家都用相同的表情包那就太沒意思了,想要一款獨特的表情包要怎麼製作呢?既然很多動圖是用視訊片段製作的,那麼我
Java中將將JPG圖片轉GIF動畫和將GIF轉JPG圖片
SEO關鍵字:圖片格式轉換 JPG圖片轉gif動畫 GIF動畫轉JPG圖片 玉念聿輝 吳明輝 深圳市奧捷迅科技 SEO描述:在後臺開發時,我們往往會涉及到圖片的轉換等相關工作,下面來分享兩個簡單而又使用的功能,複製站體即可直接使用。 JPG圖片轉gif動畫 publi
【轉】WPF自定義控制元件與樣式(12)-縮圖ThumbnailImage /gif動畫圖/圖片列表
一.前言 申明:WPF自定義控制元件與樣式是一個系列文章,前後是有些關聯的,但大多是按照由簡到繁的順序逐步釋出的等,若有不明白的地方可以參考本系列前面的文章,文末附有部分文章連結。 本文主要針對WPF專案開發中圖片的各種使用問題,經過總結,把一些經驗分享一下。內容包括: WPF常