合成是将两幅图像放在一起的动作,它使得我们能够同时看到两幅图像的特征。
在Android SDK 中,可以通过首先在Canvas对象绘制一个位图对象,然后在相同的Canvas对象上绘制第二个位图对象的方式来实现合成。唯一的区别是在绘制第二个图像时,需要在Paint对象上指定一个过渡模式(Xfermode)。
可用作过渡模式的类集合都继承自Xfermode基类,而且其中包括一个称为PorterDuffXfermode的类。PorterDuffXfermode类因Thomas Porter和Tom Duff 而得名,他们于1984年在ACM SIGGRAPH计算机图形学出版物上发表了题为“Compositing digital images(合成数字图像)”的文章,详细介绍了一系列不同的规则,用于彼此重叠地绘制图像。这些规则定义了哪些图像的哪些部分将出现在结果输出中。
在Android的PorterDuff.Mode类中列举了Porter和Duff以及其他更多人制定的规则。
这些规则包括如下:
android.graphics.PorterDuff.Mode.SRC: 此规则意味着只绘制源图像,在当前情况下,他是正在应用此规则的Paint对象。
android.graphics.PorterDuff.Mode.DST:此规则意味着只显示目标图像,即已在画布上的初始图像。
紧跟着SRC和DST规则,有一套与他们一起工作的规则,以确定最终绘制每幅图像的哪些部分。这些规则通常适用于图像具有不同的大小或他们存在透明部分时的情况。
android.graphics.PorterDuff.Mode.DST_OVER:将在源图像的顶部绘制目标图像。
android.graphics.PorterDuff.Mode.DST_IN:将仅仅在源图像和目标图像相交的地方绘制目标图相关。
android.graphics.PorterDuff.Mode.OUT:将仅仅在源图像和目标图像不相交的地方绘制目标图像。
android.graphics.PorterDuff.Mode.ATOP:将在目标图像和源图像相交的地方绘制目标图像;在其他地方绘制源图像。
android.graphics.PorterDuff.Mode.SRC_OVER:将在目标图像的顶部绘制源图像。
android.graphics.PorterDuff.Mode.SRC_IN:将仅仅在目标图像和源图像相交的地方绘制源图像。
android.graphics.PorterDuff.Mode.SRC_OUT:将仅仅在目标图像和源图像不相交的地方绘制源图像。
android.graphics.PorterDuff.Mode.SRC_ATOP:将在源图像与目标图像相交的地方绘制源图像,其他地方绘制目标图像。
android.graphics.PorterDuff.Mode.XOR:将在源图像和目标图像重叠之外的任何地方绘制他们,而在他们重叠的地方不绘制任何内容。
另外4个规则定义了当一幅图像放置在另一幅图像之上时如何合成这两幅图像。
android.graphics.PorterDuff.Mode.LIGHTEN:获得每个位置上两幅图像中最亮的像素并显示。
android.graphics.PorterDuff.Mode.DARKEN:获得每个位置上两幅图像中最暗的像素并显示。
android.graphics.PorterDuff.Mode.MULTIPLY:将每个位置的两个像素相乘,除以255,然后使用该值创建一个新的像素并显示。结果颜色=顶部颜色X底部颜色/255。
android.graphics.PorterDuff.Mode.SCREEN:反转每个颜色,执行相同的操作(向他们相乘并除以255),然后再次反转。结果颜色=255-(((255-顶部颜色)X(255-底部颜色))/255)。
我们在示例应用程序中演示如何使用这些规则。
1 package com.nthm.androidtest; 2 3 import java.io.FileNotFoundException; 4 import android.app.Activity; 5 import android.content.Intent; 6 import android.graphics.Bitmap; 7 import android.graphics.Bitmap.Config; 8 import android.graphics.BitmapFactory; 9 import android.graphics.Canvas; 10 import android.graphics.Paint; 11 import android.graphics.PorterDuffXfermode; 12 import android.net.Uri; 13 import android.os.Bundle; 14 import android.view.Display; 15 import android.view.View; 16 import android.view.View.OnClickListener; 17 import android.widget.Button; 18 import android.widget.ImageView; 19 20 public class ChoosePictureComposite extends Activity implements OnClickListener {
以上代码创建了一个标准的基于活动的应用程序,称之为“选择图片合成”(Choose Picture Composite)。该活动将实现OnClickListener,因此它可以响应Button单击。
由于将合成两幅图像,因此在试图绘制合成版本的图像之前,需要确保用户挑选了两幅图像。为此会使用两个常量,每个按下的按钮对应一个常量;然后使用两个布尔值跟踪是否已经按下一个按钮。当然,同时还要有两个Button对象。
1 private static final int PICKED_ONE=0; 2 private static final int PICKED_TWO=1; 3 4 private boolean onePicked=false; 5 private boolean twoPicked=false; 6 7 private Button choosePicture1; 8 private Button choosePicture2;
我们将有一个ImageView用于显示最终的合成图像。同时还需要有两个位图图像,为每幅选定的图像使用一个该对象。
1 private ImageView compositeImageView; 2 3 private Bitmap bmp1; 4 private Bitmap bmp2;
与前面的示例一样,将需要一个在其上绘制的Canvas对象和一个进行绘制的Paint对象。
1 private Canvas canvas; 2 private Paint paint; 3 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 setContentView(R.layout.choosepicturecomposite); 8 compositeImageView=(ImageView) findViewById(R.id.CompositeImageView); 9 choosePicture1=(Button) findViewById(R.id.ChoosePictureButton1); 10 choosePicture2=(Button) findViewById(R.id.ChoosePictureButton2); 11 12 choosePicture1.setOnClickListener(this); 13 choosePicture2.setOnClickListener(this); 14 }
由于将每个按钮的OnClickListener设置为this类,因此需要实现一个onClick方法进行响应。为了判断点击了哪一个按钮,可以将传入的View对象与每个Button对象进行比较。如果他们相等,那么这就是单击的按钮。
可以设置一个变量witch对应先前定义的常量值,以此来跟踪按下哪个按钮。然后,将这个变量传递给正由ACTION_PICK意图实例化的Gallery应用程序。如同之前示例所显示的那样,这将以允许用户选择一幅图像的模式启动应用程序。
1 @Override 2 public void onClick(View v) { 3 int which=-1; 4 if(v==choosePicture1){ 5 which=PICKED_ONE; 6 }else{ 7 which=PICKED_TWO; 8 } 9 Intent choosePictureIntent=new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 10 startActivityForResult(choosePictureIntent, which); 11 }
在用户选择一幅图像之后,将调用onActivityResult方法。通过startActivityForResult方法传入的变量将在第一个参数(称之为requestCode)中回传给我们。利用它就可以知道用户刚刚选择了哪福图像:第一幅或第二幅。可以通过这个值来决定使用哪一个位图对象加载所选择的图像。
1 @Override 2 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 3 super.onActivityResult(requestCode, resultCode, data); 4 if(resultCode==RESULT_OK){ 5 Uri imageFileUri=data.getData(); 6 if(requestCode==PICKED_ONE){ 7 bmp1=loadBitmap(imageFileUri); 8 onePicked=true; 9 }else if(requestCode==PICKED_TWO){ 10 bmp2=loadBitmap(imageFileUri); 11 twoPicked=true; 12 }
如果已经选择了两幅图像,且两个位图对象均已完成实例化,那么能够继续合成操作。这个过程与本章之前的示例非常类似。首先,创建一个空的可变位图对象,它的大小和配置与第一个位图对象(bmp1)相同。随后,根据它构建一个Canvas对象和一个Paint对象。我们只会在该画布上绘制第一个位图对象(bmp1),因此它变成了合成操作的目标。
现在可以设置Paint对象上的过渡模式。通过传入一个定义操作模式的常量,实例化一个新的PorterDuffXfermode对象。然后,在Canvas对象上绘制第二个位图对象,并且将ImageView设置为新的位图对象。以下版本的代码将使用MULTIPLY模式。
1 if(onePicked&&twoPicked){ 2 Bitmap drawingBitmap=Bitmap.createBitmap(bmp1.getWidth(), bmp1.getHeight(), bmp1.getConfig()); 3 canvas=new Canvas(drawingBitmap); 4 paint=new Paint(); 5 canvas.drawBitmap(bmp1, 0, 0, paint); 6 paint.setXfermode(new PorterDuffXfermode(android.graphics.PorterDuff.Mode.MULTIPLY)); 7 canvas.drawBitmap(bmp2, 0, 0, paint); 8 compositeImageView.setImageBitmap(drawingBitmap); 9 } 10 } 11 }
以下代码是一个辅助类,如同子啊第一章定义的那样,它用于通过URI加载一个位图对象,并将其缩放到不大于屏幕大小。
1 private Bitmap loadBitmap(Uri imageFileUri){ 2 Display currentDisplay=getWindowManager().getDefaultDisplay(); 3 float dw=currentDisplay.getWidth(); 4 float dh=currentDisplay.getHeight(); 5 Bitmap returnBmp=Bitmap.createBitmap((int)dw, (int)dh, Config.ARGB_4444); 6 //加载图像的尺寸而非图像的本身 7 BitmapFactory.Options bmpBitmapFactoryOptions=new BitmapFactory.Options(); 8 bmpBitmapFactoryOptions.inJustDecodeBounds=true; 9 try { 10 returnBmp=BitmapFactory.decodeStream(getContentResolver().openInputStream(imageFileUri), null, bmpBitmapFactoryOptions); 11 int heightRatio=(int) Math.ceil(bmpBitmapFactoryOptions.outHeight/dh); 12 int widthRatio=(int) Math.ceil(bmpBitmapFactoryOptions.outWidth/dw); 13 //如果两个比率都大于1 那么图像的一条边大于屏幕 14 if(heightRatio>1&&widthRatio>1){ 15 if(heightRatio>widthRatio){ 16 //若高度比率比较大,则根据它进行缩放 17 bmpBitmapFactoryOptions.inSampleSize=heightRatio; 18 }else{ 19 //若宽度比率比较大,则根据它进行缩放 20 bmpBitmapFactoryOptions.inSampleSize=widthRatio; 21 } 22 } 23 //对它进行真正的解码 24 bmpBitmapFactoryOptions.inJustDecodeBounds=false; 25 returnBmp=BitmapFactory.decodeStream(getContentResolver().openInputStream(imageFileUri), null, bmpBitmapFactoryOptions); 26 } catch (FileNotFoundException e) { 27 e.printStackTrace(); 28 } 29 return returnBmp; 30 } 31 }
以下代码是用于上述活动的布局XML。
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 android:layout_width="match_parent" 3 android:layout_height="match_parent" 4 android:orientation="vertical" 5 > 6 <Button 7 android:layout_width="fill_parent" 8 android:layout_height="wrap_content" 9 android:text="Choose Picture 1" 10 android:id="@+id/ChoosePictureButton1" /> 11 <Button 12 android:layout_width="fill_parent" 13 android:layout_height="wrap_content" 14 android:text="Choose Picture 2" 15 android:id="@+id/ChoosePictureButton2" /> 16 <ImageView 17 android:layout_width="wrap_content" 18 android:layout_height="wrap_content" 19 android:id="@+id/CompositeImageView" 20 /> 21 </LinearLayout>