今天,我们就来谈下android中图片的变形的特效,在上讲博客中我们谈到android中图片中的色彩特效来实现的。改变它的颜色主要通过ColorMatrix类来实现。
现在今天所讲的图片变形的特效主要就是通过Matrix类来实现,我们通过上篇博客知道,改变色彩特效,主要是通过ColorMatrxi矩阵的系数,以及每个像素点上所对应的颜色偏移量。而今天的图形变换与那个也是非常的类似。它是一个3*3矩阵,而颜色矩阵则是一个4*5的矩阵。在这个3*3矩阵中则表述出了每个像素点的XY坐标信息。然后通过修改这个矩阵,就可达到修改图片中的每个像素点的XY坐标,即改变每个像素点的位置信息,通过对特定的矩阵元素值的修改就可以达到实现图片的中的
图形变换特效如:平移变换特效,旋转变换特效,缩放变换特效,错切变换特效。那么接下来我们就通过从原理的角度来一一分析下每个特效对应的矩阵是怎么样的。
默认的图形变换的初始矩阵是:
                        1  0  0 
                        0  1  0
                        0  0  1
  第一、平移变换特效:
                 我们很容易知道这个,在一个平面中,将一个像素点从位置A(x0,y0)移到另一个位置B(x,y)
很容易得到如下的公式:
                   x=x0+X方向的偏移量(xt);
                   y=y0+Y方向的偏移量(yt);
依据上面的等式我们很容易得到如下矩阵:


      |x|     |1 0 xt|  |x0|
                   |y| =  |0 1 yt| ×    |y0|
                   |1|     |0 0  1|       | 1 |
然后我们通过如上的矩阵乘法计算得到等式(与我们得出的结论一致,所以也就是我们通过改变初始矩阵中那个矩阵元素的值就可以实现图片在X,Y方向上的平移):
                    x=x0+X方向的偏移量(xt);
                    y=y0+Y方向的偏移量(yt);
 第二、旋转变换特效:


                   所谓旋转变换就相当于一个像素点围绕某个中心点O旋转到一个新的点,在高中学过三角函数的都知道,通过从初始点A(x0,y0)旋转到B点(x,y)
初始点与X轴正方向夹角为a,旋转过的角度为t,通过三角函数的计算得出如下公式:
                  设:旋转轴长为:r
                   x0=r*cosa;  y0=r*sina;
                   x=r*cos(a+t)=r*cosa*cost-rsina*sint=x0*cost-y0*sint;
                   y=r*sin(a+t)=r*sina*cost+r*cosa*sint=y0*cost+x0*sint;
从而可以得出如下矩阵:
                    |x|    |cost -sint 0| |x0|
                    |y| = |sint  cost 0| ×|y0|
                    |1|    |0      0     1|    | 1 |
然后我们通过如上的矩阵乘法计算得到等式(与我们得出的结论一致,所以也就是我们通过改变初始矩阵中那个矩阵元素的值就可以实现图片旋转):


                   x=x0*cost-y0*sint;
                                                                  y=y0*cost+x0*sint;
注意:前面所讲的旋转都是以坐标的原点为旋转中心的,我们还可以以任意点O为旋转中心来进行旋转的变换但是通常需要如下三个步骤:
首先第一需要将坐标的原点平移到任意指定的点O,然后再使用上述我们的所讲的旋转方法来进行旋转,最后就需要将我们的原点还原回去。
第三、缩放变换特效:
                 所谓像素点的缩放,实际上并不会对像素点缩放,因为像素点已经够小了,不存在什么缩放的概念,那我们这里所说的缩放是怎么样的呢?
我们是通过将每个像素点所在的XY坐标按一定的比例缩放,然后使得图片整体看起来的有一个缩放的效果。
                    x=K1*x0
                                    y=K2*y0 
通过以上公式反映到我们的变换矩阵中的形式:
                              |x|    |K1 0 0| |x0|
                      |y| = |0 K2 0| ×|y0|
                      |1|    |0 0   1|   | 1 |
通过矩阵乘法验证得到等式(与我们得出的结论一致,所以也就是我们通过改变初始矩阵中那两个矩阵元素的值就可以实现图片在X,Y方向上的缩放)
第四、错切变换特效:
                所谓错切变换的效果很类似数学上的Shear mapping.错切主要分两种形式:
第一水平错切变换:就是在正常图片的基础上,让每个像素点的Y轴坐标保持不变,而让他们的X坐标按一定比例的缩放,第二就是垂直错切变换:就是在原图的基础上,让让每个像素点的X坐标保持不变,让Y坐标按一定比例的缩放。从而可以得到如下的变换公式:
                 x=x0+K1*y0;y=x0+K2*y0;
通过以上的公式反应到我们的变换矩阵中的形式:
                 
      |x|    |1    K1  0| |x0|
                   |y| =  |K2  1   0| ×|y0|
                   |1|    |0     0    1|  | 1 |
通过矩阵乘法验证得到等式(与我们得出的结论一致,所以也就是我们通过改变初始矩阵中那两个矩阵元素的值就可以实现图片在X,Y方向上的错切)
综合上述:将得出我们最后的矩阵变换公式:
                    |Scale_X Skew_X  Trans_X|
                    |Skew_Y  Scale_Y Trans_Y|
                    | 0               0             1        |
也就是我只需要改变矩阵中对应的元素的值,我们就可以实现各种变换的特效。
如果用A,B,C,D,E,F来一次标示那些区的话,A和E决定缩放变换,B和D决定了错切变换,C和F决定了平移变换

下面我们就通过一个Demo来验证一下我们的观点。 

package com.mikyou.matrix;import java.util.ArrayList;import java.util.List;import android.app.Activity;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.Matrix;import android.graphics.Paint;import android.os.Bundle;import android.view.View;import android.widget.EditText;import android.widget.ImageView;public class MainActivity extends Activity {private ImageView iv;private Canvas canvas;private Paint paint;private Bitmap baseBitmap;private Bitmap copyBitmap;private Matrix matrix;private EditText e1,e2,e3,e4,e5,e6,e7,e8,e9;private float t1,t2,t3,t4,t5,t6,t7,t8,t9;private List valueList;public void ok(View view){valueList=new ArrayList();valueList.clear();iv.setImageBitmap(null);t1=Float.valueOf(e1.getText().toString());valueList.add(t1);t2=Float.valueOf(e2.getText().toString());valueList.add(t2);t3=Float.valueOf(e3.getText().toString());valueList.add(t3);t4=Float.valueOf(e4.getText().toString());valueList.add(t4);t5=Float.valueOf(e5.getText().toString());valueList.add(t5);t6=Float.valueOf(e6.getText().toString());valueList.add(t6);t7=Float.valueOf(e7.getText().toString());valueList.add(t7);t8=Float.valueOf(e8.getText().toString());valueList.add(t8);t9=Float.valueOf(e9.getText().toString());valueList.add(t9);float[] imageMatrix=new float[9];for (int i = 0; i 

运行结果:




实际上,除了我们通过底层的方法,直接修改他们的变换矩阵的值来达到某种变换的效果,实际上android已经封装一些变换的API接口
例如:
旋转变换:matrix.setRotate() 平移变换 matrix.setTranslate() 缩放变换 matrix.setScale() 错切变换 matrix.setSkew()
不过android中还提供两个非常重要的方法一个是pre(),一个是post() 提供矩阵的前乘和后乘运算。
注意: 以上的几个set方法都会重新清空重置矩阵中的值,而有时候我需要实现叠加的效果,就单单用set方法是无法完成,因为第二次叠加的变换,会把上一次的
第一次变换的矩阵值给清空,只会保存最近一次的矩阵中的值。所以android 就给我们提供pre和post方法,这两个方法可实现矩阵混合效果,从而实现图形变换的
叠加。例如:
先移动到点(400,400),在旋转45度,最后平移到(200,200)
用以上例子来说明pre(先乘)和post(后乘)运算的区别,因为在矩阵乘法中不支持交换律,所以我们需要用这两个方法来区别实现
pre运算实现:
                                  matrix.setTranslate(200,200) matrix.preRotate(45)
post运算实现:
          matrix.setRotate(45) matrix.postTranslate(200,200)
最后我们通过一个案例来实现一个组合叠加变换的效果

package com.mikyou.dealImage;import android.app.Activity;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.Matrix;import android.graphics.Paint;import android.os.Bundle;import android.view.View;import android.widget.ImageView;public class MainActivity extends Activity {/** *@author zhongqihong *图片的绘制的个人总结: *如何去仿照一张原图去重新绘制一张图片呢?? *需要如下材料: *1、需要参考的原图 *2、画纸(实际上就是一张没有任何内容的图片,空白图片) *3、画布(就是用于固定画纸的) *4、画笔(就是用于绘制图片的工具) *其实一般分为如下几步: *1、首先必须拿到原图构造出一张空画纸(即空白图片),知道原图的大小,分辨率等信息,从而就可以确定我的画纸的大小,及所确定的分辨率(默认使用原图的分辨率)。 *baseBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.img_small_1);//拿到原图对象 *   copyBitmap=Bitmap.createBitmap(baseBitmap.getWidth(),baseBitmap.getHeight(), baseBitmap.getConfig());//根据原图对象的相关信息,得到同大小的画纸,包括原图分辨率 *2、然后,确定了一个空白的画纸后,就需要将我们的画纸固定在我们的画布上 *Canvas canvas=new Canvas(copyBitmap); *3、接着就是构造一个画笔对象 * Paint paint=new Paint(); * 4、接着对图片进行一系列的操作,诸如:缩放、平移、旋转等操作 * Matrix matrix =new Matrix(); * 缩放:(缩放中心点:默认是画纸左上角点的坐标) * matrix.setScale(1.5f,1.5f);//第一个参数为X轴上的缩放比例为1.5(>1表示放大,<1表示缩小)此处就表示在XY轴上分别放大为原图1.5倍 * matrix.setScale(-1f,1f);//表示在X轴反向缩放,Y轴不变,即相当于把原图往X轴负方向反向翻转了一下,即相当于处理后的图片与原图位置(即现在的画纸位置)关于Y负半轴对称 * matrix.setScale(1f,-1f);//表示在X轴不变,Y轴的,即相当于把原图往Y轴负方向反向翻转了一下,即相当于处理后的图片与原图位置(即现在的画纸位置)关于X正半轴对称 * 5、处理完后,就需要把我们处理好的图片绘制在画布上形成最后的图片 * canvas.drawBitmap(baseBitmap,matrix,paint);//第一个参数表示原图的Bitmap对象,表示按照原图的样式内容在原来相同规格空白的纸上画出图片内容iv.setImageBitmap(copyBitmap);//最后将图片的显示在ImageView控件上 * 个人理解:感觉整个过程很像PS中置入一张图片到一张空白画纸上,首先我们 * 需要去设置一张空白的画纸,然后将我们的原图置入,在置入之前我们可以做些对图片的操作 * 包括缩放,平移,旋转,确定一些操作后,点击确定就相当于调用绘制方法,从而就形成一张处理后的图片 *  * */private ImageView iv;private Bitmap baseBitmap;//原图private Bitmap copyBitmap;//画纸private Canvas canvas;//画布private Paint paint;//画笔@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);iv= (ImageView) findViewById(R.id.iv2);}public void btn(View view){//拿到原图:得到原图大小baseBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.img_small_1);//1、拿到一张与原图一样大小的纸,并没有内容copyBitmap=Bitmap.createBitmap(1200,1300, baseBitmap.getConfig());//拿到原图的分辨率和原图的Config//2、将这张纸固定在画布上Canvas canvas=new Canvas(copyBitmap);//3、画笔paint=new Paint();paint.setAlpha(100);//4、加入一些处理的规则Matrix matrix=new Matrix();//1、缩放规则//matrix.setScale(1.5f,1.5f);//2、位移,//   matrix.setTranslate(50f,50f);//分别在x轴和Y轴上位移50,针对图片的左上角为原点来移动//3、旋转//matrix.setRotate(45f);//代表顺时针旋转45度,默认以左上角为原点// matrix.setRotate(45, baseBitmap.getWidth()/2, baseBitmap.getHeight()/2);//第二个参数和第三个参数表示旋转中心的点的X,Y坐标//4、翻转镜面效果// matrix.setScale(-1f, 1f);// matrix.setTranslate(baseBitmap.getWidth(), 0);//  matrix.postTranslate(baseBitmap.getWidth(), 0);//如果要对图片进行多次操作,就要用post的方法来操作//5、倒影效果matrix.setScale(1f, -1f);//先把图片在y轴方向反向缩放matrix.postTranslate(0, baseBitmap.getHeight());//然后再把反向缩放后的图片移到canvas上显示即可 matrix.postTranslate(baseBitmap.getWidth()+160, 0);matrix.postSkew(-1, 0);//5、将处理过的图片画出来canvas.drawBitmap(baseBitmap,matrix,paint);//第一个参数表示原图的Bitmap对象,表示按照原图的样式内容在原来相同规格空白的纸上画出图片内容iv.setImageBitmap(copyBitmap);//最后将图片的显示在ImageView控件上}}

运行效果:


与ColorMatrix颜色矩阵类似,在图形变换中也有一个针对每一个像素块做处理,而在颜色变换是针对每个像素点。针对像素快的处理我们使用
drawBitmapMesh()方法来处理。
drawBitmapMesh()和处理每像素点类似,只不是将图像分成一个一个的小块然后针对每个小块来改变整个图像
drawBitmapMesh()方法很是重要利用可以实现很多的图片的特效。下面我们就重点来介绍一下这个方法:
drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint)
官方源码是这样介绍这个方法的:
    /**
     * Draw the bitmap through the mesh, where mesh vertices are evenly
     * distributed across the bitmap. There are meshWidth+1 vertices across, and
     * meshHeight+1 vertices down. The verts array is accessed in row-major
     * order, so that the first meshWidth+1 vertices are distributed across the
     * top of the bitmap from left to right. A more general version of this
     * method is drawVertices().
     *
     * @param bitmap The bitmap to draw using the mesh
     * @param meshWidth The number of columns in the mesh. Nothing is drawn if
     *                  this is 0
     * @param meshHeight The number of rows in the mesh. Nothing is drawn if
     *                   this is 0
     * @param verts Array of x,y pairs, specifying where the mesh should be
     *              drawn. There must be at least
     *              (meshWidth+1) * (meshHeight+1) * 2 + vertOffset values
     *              in the array
     * @param vertOffset Number of verts elements to skip before drawing
     * @param colors May be null. Specifies a color at each vertex, which is
     *               interpolated across the cell, and whose values are
     *               multiplied by the corresponding bitmap colors. If not null,
     *               there must be at least (meshWidth+1) * (meshHeight+1) +
     *               colorOffset values in the array.
     * @param colorOffset Number of color elements to skip before drawing
     * @param paint  May be null. The paint used to draw the bitmap
     */
它大概的意思的是这样的,通过网格来绘制图片,那么这张图片将会被meshWidth画出横向格子,meshHeight画出纵向的格子
那么在横向上将会产生meshWidth+1个交叉点,在纵向上会产生meshHeight+1个交叉点,最后总的交叉点个数为(meshWidth+1)*(meshHeight+1)
bitmap: 需要绘制在网格上的图像。
meshWidth: 网格的宽度方向的数目(列数),为0时不绘制图像。
meshHeight:网格的高度方向的数目(含数),为0时不绘制图像。
verts: (x,y)对的数组,表示网格顶点的坐标,至少需要有(meshWidth+1) * (meshHeight+1) * 2 + meshOffset 个(x,y)坐标。
vertOffset: verts数组中开始跳过的(x,y)对的数目。
Colors: 可以为空,不为空为没个顶点定义对应的颜色值,至少需要有(meshWidth+1) * (meshHeight+1) * 2 + meshOffset 个(x,y)坐标。
colorOffset: colors数组中开始跳过的(x,y)对的数目。
paint: 可以为空。
下面将通过一个自定义View方法来实现,旗帜飘扬的图片控件。并且添加自定义属性,下次使用可以方便的在XMl中更换图片
修改划分方格数,振幅大小,频率大小
实现整体思路如下:
针对像素块来实现图形扭曲的原理:
它的原理就是通过修改划分后的小方格产生交叉点的坐标,来实现图片的扭曲
本案例实现一个旗帜飘动形状的图片
大致的思路如下:
实现思路整体分两部分,第一部分取得所有扭曲前图片的所有交叉点的坐标
并把这些交叉点坐标保存在orig数组中;第二部分修改原图中方格每个交叉点的坐标,遍历这个数组,然后通过某种算法
使得这些交叉点的坐标,呈某种规律函数曲线变化。这里就以三角函数中的正弦函数
来改变这些交叉点坐标,从而产生每个交叉点新的坐标,然后再将这些新的坐标保存在verts数组中,最后通过drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint)
实现图片的绘制。

自定义属性:attrs.xml

<?xml version="1.0" encoding="utf-8"?>                                                                    

自定义View的MyBannerImageView类:

package com.mikyou.myview;import com.mikyou.piexkuai.R;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.drawable.Drawable;import android.util.AttributeSet;import android.view.View;import android.view.Window;import android.widget.ImageView;/** * @author mikyou * 针对像素块来实现图形扭曲的原理: * 它的原理就是通过修改划分后的小方格产生交叉点的坐标,来实现图片的扭曲 * 本案例实现一个旗帜飘动形状的图片 * 大致的思路如下: * 实现思路整体分两部分,第一部分取得所有扭曲前图片的所有交叉点的坐标 * 并把这些交叉点坐标保存在orig数组中;第二部分修改原图中方格每个交叉点的坐标,遍历这个数组,然后通过某种算法 * 使得这些交叉点的坐标,呈某种规律函数曲线变化。这里就以三角函数中的正弦函数 * 来改变这些交叉点坐标,从而产生每个交叉点新的坐标,然后再将这些新的坐标保存在 * verts数组中,最后通过drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint) * 实现图片的绘制。 * */public class MyBannerImageView extends View{//定义两个常量表示需要将这张图片划分成20*20=400个小方格,//定义两个常量,这两个常量指定该图片横向,纵向上都被划分为20格  private  int WIDTH=40;//横向划分的方格数目private  int HEIGHT=40;//纵向划分的方格数目private float FREQUENCY=0.1f;//三角函数的频率大小private int AMPLITUDE=60;//三角函数的振幅大小//那么将会产生21*21=421个交叉点private  int POINT_COUNT=(WIDTH+1)*(HEIGHT+1);//由于,我要储存一个坐标信息,一个坐标包括x,y两个值的信息,相邻2个值储存为一个坐标点//其实大家应该都认为这样不好吧,还不如直接写一个类来直接保存一个点的信息,但是没办法// 但是在drawBitmapMesh方法中传入的是一个verts数组,该数组就是保存所有点的x,y坐标全都放在一起//所以,我就只能这样去控制定义orig和verts数组了,private Bitmap baseBitmap;private float[] orig=new float[POINT_COUNT*2];//乘以2是因为x,y值是一对的。private float[] verts=new float[POINT_COUNT*2];private float k;public MyBannerImageView(Context context, AttributeSet attrs,int defStyleAttr) {super(context, attrs, defStyleAttr);//接收自定义属性值TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MikyouBannerView);for (int i = 0; i < array.getIndexCount(); i++) {int attr=array.getIndex(i);switch (attr) {case R.styleable.MikyouBannerView_Src:baseBitmap=BitmapFactory.decodeResource(getResources(), array.getResourceId(attr, R.drawable.ic_launcher));break;case R.styleable.MikyouBannerView_ColumnNum:HEIGHT=array.getInt(attr, 40);break;case R.styleable.MikyouBannerView_RowNum:WIDTH=array.getInt(attr, 40);break;case R.styleable.MikyouBannerView_Amplitude:AMPLITUDE=array.getInt(attr, 60);break;case R.styleable.MikyouBannerView_Frequency:FREQUENCY=array.getFloat(attr, 0.1f);break;default:break;}}array.recycle();initData();}public MyBannerImageView(Context context, AttributeSet attrs) {this(context, attrs,0);}public MyBannerImageView(Context context) {this(context,null);}//set,gfanfgpublic int getWIDTH() {return WIDTH;}public void setWIDTH(int wIDTH) {WIDTH = wIDTH;}public int getHEIGHT() {return HEIGHT;}public void setHEIGHT(int hEIGHT) {HEIGHT = hEIGHT;}public float getFREQUENCY() {return FREQUENCY;}public void setFREQUENCY(float fREQUENCY) {FREQUENCY = fREQUENCY;}public int getAMPLITUDE() {return AMPLITUDE;}public void setAMPLITUDE(int aMPLITUDE) {AMPLITUDE = aMPLITUDE;}public Bitmap getBaseBitmap() {return baseBitmap;}public void setBaseBitmap(Bitmap baseBitmap) {this.baseBitmap = baseBitmap;}@Overrideprotected void onDraw(Canvas canvas) {flagWave();k+=FREQUENCY;canvas.drawBitmapMesh(baseBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);invalidate();}private void initData() {float baseBitmapWidth=baseBitmap.getWidth();float baseBitmapHeight=baseBitmap.getHeight();int index=0;//通过遍历所有的划分后得到的像素块,得到原图中每个交叉点的坐标,并把它们保存在orig数组中for (int i = 0; i <= HEIGHT; i++) {//因为这个数组是采取行优先原则储存点的坐标,所以最外层为纵向的格子数,然后一行一行的遍历float fy=baseBitmapHeight*i/HEIGHT;//得到每行中每个交叉点的y坐标,同一行的y坐标一样for (int j = 0; j <= WIDTH; j++) {float fx=baseBitmapHeight*j/WIDTH;//得到每行中的每个交叉点的x坐标,同一列的x坐标一样orig[index*2+0]=verts[index*2+0]=fx;//存储每行中每个交叉点的x坐标,为什么是index*2+0作为数组的序号呢??//因为我们之前也说过这个数组既存储x坐标也存储y坐标,所以每个点就占有2个单位数组空间orig[index*2+1]=verts[index*2+1]=fy+200;//存储每行中每个交叉点的y坐标.为什么需要+1呢?正好取x坐标相邻的下标的元素的值//+200是为了避免等下在正弦函数扭曲下,会把上部分给挡住所以下移200index++;}}}/** * @author mikyou * 加入三角函数正弦函数Sinx的算法,来修改原图数组保存的交叉点的坐标 * 从而得到旗帜飘扬的效果,这里我们只修改y坐标,x坐标保持不变 * */public void flagWave(){for (int i = 0; i <=HEIGHT ; i++) {for (int j = 0; j 

activity_main.xml:

    
运行的结果;

注意:不好意思,因为没有录制GIF所以看不出动态效果,大家可以下载Demo看看.

Demo下载链接

更多相关文章

  1. android 地理位置共享服务
  2. Android(安卓)绘图基础:Bitmap(位图)与Matrix(矩阵)实现图片5种操作(平
  3. Android(安卓)OpenGLES2.0(三)——等腰直角三角形和彩色的三角形
  4. Android分析View的scrollBy()和scrollTo()的参数正负问题原理分
  5. Android(安卓)高级UI解密 (二) :Paint滤镜 与 颜色过滤(矩阵变换)
  6. Android实现控件滑动的几种方法
  7. Android里的动画(补间动画,帧动画,属性动画)
  8. Android(安卓)ColorMatrix类图像颜色处理-黑白老照片、泛黄旧照
  9. Android图形---OpenGL(三)

随机推荐

  1. Android(安卓)View 随手指移动
  2. Android(安卓)BitmapFactory图片压缩处理
  3. AndroidCameraHAL3-MultiCamera-CameraX
  4. android 插件总结
  5. Android官方Toolbar自定义高度最靠谱的解
  6. Android(安卓)Framework中的线程Thread及
  7. Android(安卓)终极屏幕适配方案
  8. 从0开始认识android(十五):点击链接启动APP
  9. Android(安卓)studio无法连接识别检测各
  10. (转)android底部菜单应用