本篇博文的重点是如何将一张图片切成指定行列的小切片,然后进行显示。为了突出切图逻辑,切了一张正方形图片,切出来的也是宽高相等的小切片。为了增强灵活性,使用了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; } }
为了让切片显示美观一些,切片与切片之间加入了边距,但在代码中通过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); } } }
看完本文,如果想要通过自定义布局的方式显示那些切割下来的切片,可以阅读Android自定义控件系列案例【二】