Android圖片切片控制與顯示案例實戰
本篇博文的重點是如何將一張圖片切成指定行列的小切片,然後進行顯示。為了突出切圖邏輯,切了一張正方形圖片,切出來的也是寬高相等的小切片。為了增強靈活性,使用了SeekBar並指定最大進度值為40來模擬當我們傳入N列時,切圖邏輯就切出N*N張切片,並封裝到List集合中返回。(有密集恐懼症的同學把最大進度值40改小點吧,因為當切片數達到一定數目時,看著確實有感)有了切片的List集合,我們就可以進行顯示了,關於顯示部分不了本博文中重點,所以我決定採用最容易理解的方式來顯示,在XML佈局中定義一個LinearLayout,讓其orientation為垂直排列,然後迴圈行數,建立每一行的LinearLayout,讓其orientation為水平,然後迴圈列數,建立每一列的ImageView,為其設定對應位置的切片,最後每迴圈一列將其新增到對應行的LinearLayout中,而每迴圈完一行,將其新增到XML佈局中定義的那個LinearLayout中,到此就將所有行列的切片顯示到了介面上。當然實際開發中我們完全可以把切片與顯示的邏輯全部放到一個自定義佈局中,但這樣的話可能需要自定義佈局相關的技術加入進來,增加了本博文的複雜度,同時也干擾了我們對本博文重點的介紹,因此關於自定義的實現方式可能會放到升級版博文中。輕裝上陣開始本博文的重點學習。
案例效果:
佈局實現:
佈局這塊首先是一個存放多行多列切片控制元件的LinearLayout容器,具體的切片控制元件需要在程式碼中動態生成並addView到這個容器中,然後就是一個控制圖片被切割的多少行多少列的SeekBar,具體佈局實現如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:id="@+id/ll_pic" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="@android:color/darker_gray" android:orientation="vertical" android:padding="1dp" > </LinearLayout> <SeekBar android:id="@+id/sb" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/ll_pic" android:layout_margin="20dp" android:max="40" /> </RelativeLayout>
邏輯實現:
切片實體:
實體中主要封裝了切片在佈局中的位置和對應的Bitmap,建立它的目的是為了適應將來的程式擴充套件。
package com.slice.slice.mode; import android.graphics.Bitmap; /** * 切片實體類 * * @author 張科勇 * */ public class Slice { private int index;// 切片索引值 private Bitmap bitmap;// 切片圖片物件 public Slice() { } public Slice(int index, Bitmap bitmap) { super(); this.index = index; this.bitmap = bitmap; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public Bitmap getBitmap() { return bitmap; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } @Override public String toString() { return "Slice [index=" + index + ", bitmap=" + bitmap + "]"; } }
切割邏輯工具類:
這個工具類的主要作用就是根據要切的圖片和指定要切的行列數將圖片進行切割,然後將切割後的切片Bitmap的索引值(對應到顯示時的位置)封裝到上一步建立的切片實體中,最後把切片實體儲存到List<Slice>集合中並返回。並了在顯示的時候顯示的看到各個切片,這個工具類提供了兩個讓List集合亂序的方法,使用哪個方法都可以做到亂序,這樣當把亂序後的集合顯示到介面上,我們可以非常清楚的看到各切片的情況。具體實現如下:
package com.slice.slice.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.slice.slice.mode.Slice;
import android.graphics.Bitmap;
/**
* 圖片切片工具類
*
* @author 張科勇
*
*/
public class SliceUtil {
/**
* 切割圖片的方法,切成slices行 *slices列 個圖片,
*
* @param bitmap
* 要切割的圖片物件
* @param slices
* 要切割的列數,
* @return 將切割後的slices行 *slices列 個圖片封裝到List<Slice>中並返回
*/
public static List<Slice> splitPic(Bitmap bitmap, int slices) {
List<Slice> sliceList = new ArrayList<Slice>();
if (slices >= 1) {
// 獲得要切割的圖片的寬高
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 得到每個切片圖片的寬高,這裡讓寬高一樣,意思是切成了正方形
int sliceWH = Math.min(width, height) / slices;
// 開始切割,使用雙迴圈,切割成slices行,slices列
for (int i = 0; i < slices; i++) {
for (int j = 0; j < slices; j++) {
/*
* 把當前行列號作為切片的索引值,假如slices=3
* ==================
* 0+0,0+1,0+2
* 3+0,3+1,3+2
* 6+0,6+1,6+2
* ==================
* 0,1,2
* 3,4,5
* 6,7,8
* ==================
*/
int index = i * slices + j;
// 切片Bitmap對應的x,y座標,x由列決定,y則行決定
int x = j * sliceWH;
int y = i * sliceWH;
Bitmap sliceBitmap = Bitmap.createBitmap(bitmap, x, y, sliceWH, sliceWH);
// 建立切片物件,並把索引值和切片Bitmap封裝到切片物件中
Slice slice = new Slice(index, sliceBitmap);
// 將每個切片物件儲存到List集合中去
sliceList.add(slice);
}
}
}
// 返回切片物件
return sliceList;
}
/**
* 隨機打亂List集合中的物件 Moves every element of the list to a random new position
* in the list.
*
* @param slideList
* 要打亂順序的List集合
* @return 返回一個打亂了順序的List集合
*/
public static List<Slice> shuffleList1(List<Slice> sliceList) {
Collections.shuffle(sliceList);
return sliceList;
}
/**
* 隨機打亂List集合中的物件 Moves every element of the list to a random new position
* in the list.
*
* @param slideList
* 要打亂順序的List集合
* @return 返回一個打亂了順序的List集合
*/
public static List<Slice> shuffleList2(List<Slice> sliceList) {
Collections.sort(sliceList, new Comparator<Slice>() {
@Override
public int compare(Slice s1, Slice s2) {
// 正常的比較是s1>s2 返回1,s1<s2 返回-1,s1=s2返回0
// 這裡我們返回一個不確定的(-1,1,0),這樣就可以把順序打亂
double random = Math.random();
if (random == 0.5) {
return 0;
} else if (random > 0.5) {
return 1;
} else {
return -1;
}
}
});
return sliceList;
}
}
dp與px轉換工具類:
為了讓切片顯示美觀一些,切片與切片之間加入了邊距,但在程式碼中通過setMargin()方法設定邊距時需要傳入的長度單位是畫素,為了能適配不同解析度的螢幕,需要進行指定的dp轉成px,所以設計了這個工具類,其實這個工具類在實際專案中經常使用。具體實現如下:
package com.slice.slice.utils;
import android.content.Context;
/**
* dp與px轉換工具,為螢幕適配
*
* @author 張科勇
*
*/
public class DensityUtil {
/**
* 從 dp轉為px(畫素)
*/
public static int dip2px(Context context, float dp) {
final float density = context.getResources().getDisplayMetrics().density;
return (int) (dp * density + 0.5f);
}
/**
* 從 px(畫素)轉為 dp
*/
public static int px2dip(Context context, float px) {
final float density = context.getResources().getDisplayMetrics().density;
return (int) (px / density + 0.5f);
}
}
切片的生成控制與顯示:
有一上面工具類,我們可以呼叫對應的方法生成切片,但具體在什麼時機去生成切片,在什麼時機顯示這些切片以及如何顯示這些切片,我在這些邏輯都放到了MainActivity中,並且將主要邏輯封裝到了方法中,初始的時候呼叫一次,顯示原圖片,然後在監聽SeekBar拖動的地方把SeekBar當前進度值progress做為行列數傳遞給封裝的方法讓其生成progress*progress行列的切片然後進行顯示,具體實現如下:
package com.slice.slice;
import java.util.List;
import com.slice.slice.mode.Slice;
import com.slice.slice.utils.DensityUtil;
import com.slice.slice.utils.SliceUtil;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
/**
* 控制切片生成與顯示的Activity
* @author 張科勇
*
*/
public class MainActivity extends Activity {
//存放所有行切片的線型佈局容器
private LinearLayout mPicLL;
//控制切割行列數的可拖動進度條
private SeekBar mSlicesSb;
//要切割的圖片物件
private Bitmap mPicBitmap;
//邊距
private int margin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initDatas();
initViews();
initEvents();
splitImage(1);
}
/**
* 初始化資料
*/
private void initDatas() {
mPicBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pic);
margin = DensityUtil.dip2px(this, 1);
}
/**
* 初始化View
*/
private void initViews() {
mPicLL = (LinearLayout) findViewById(R.id.ll_pic);
mSlicesSb = (SeekBar) findViewById(R.id.sb);
}
/**
* 初始化互動事件
*/
private void initEvents() {
// 註冊與處理SeekBar進行改變的事件
mSlicesSb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
/**
* 把progress當作行數,呼叫生成與顯示切片的方法,動態控制切片的生成和顯示
*/
int rowNum = progress;
splitImage(rowNum);
}
});
}
/**
* 切割與顯示切片
*
* @param progress
*/
private void splitImage(int rowNum) {
if (rowNum == 0) {
rowNum = 1;
}
mPicLL.removeAllViews();
//切片返回切片集合
List<Slice> sliceList = SliceUtil.splitPic(mPicBitmap, rowNum);
// 打亂切片在集合中的順序
sliceList = SliceUtil.shuffleList1(sliceList);
//遍歷行
for (int i = 0; i < rowNum; i++) {
//建立每行對應的佈局容器物件
LinearLayout rowLL = new LinearLayout(MainActivity.this);
//每行中的切片水平顯示
rowLL.setOrientation(LinearLayout.HORIZONTAL);
//每行中的切片居中顯示
rowLL.setGravity(Gravity.CENTER);
//遍歷列
for (int j = 0; j < rowNum; j++) {
//獲得對應位置的切片實體物件
Slice slice = sliceList.get(i * rowNum + j);
//建立每列要顯示切片的ImageView控制元件
ImageView iv = new ImageView(MainActivity.this);
iv.setScaleType(ScaleType.FIT_XY);
//顯示切片
iv.setImageBitmap(slice.getBitmap());
// 獲得切片圖片的寬高,作為ImageView的寬高
int ivWH = slice.getBitmap().getWidth();
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ivWH, ivWH);
// 設定切片ImageView的外邊距
params.setMargins(margin, margin, margin, margin);
iv.setLayoutParams(params);
//將每行切片ImageView新增對對應行佈局容器中
rowLL.addView(iv);
}
mPicLL.addView(rowLL);
}
}
}
到此,整個案例的所有工作就做完了,專案中用到了一張600*600本地資源萌寵圖片,大家可根據自己的喜好換成對應的圖片,需要注意的就是圖片本身的寬高不要太小,也不要太大,否則就需要加入對圖片的縮放或壓縮邏輯,甚至需要動態計算佈局寬高了。
看完本文,如果想要通過自定義佈局的方式顯示那些切割下來的切片,可以閱讀