• Android 高仿微信头像截取 打造不一样的自定义控件


    转载请表明出处:http://blog.csdn.net/lmj623565791/article/details/39761281,本文出自:【张鸿洋的博客】

    1、概述

    前面已经写了关于检测手势识别的文章,如果不了解可以参考:Android 手势检测实战 打造支持缩放平移的图片预览效果(下)。首先本篇文章,将对之前博客的ZoomImageView代码进行些许的修改与改善,然后用到我们的本篇博客中去,实现仿微信的头像截取功能,当然了,个人觉得微信的截取头像功能貌似做得不太好,本篇博客准备去其糟粕,取其精华;最后还会见识到不一样的自定义控件的方式,也是在本人博客中首次出现,如果有兴趣可以读完本篇博客,希望可以启到抛砖引玉的效果。

    2、效果分析

    1、效果图:


    我们来看看妹子的项链,嗯,妹子项链还是不错的~

    2、效果分析

    根据上面的效果,我们目测需要自定义两个控件,一个就是我们的可自由缩放移动的ImageView,一个就是那个白色的边框;然后一起放置到一个RelativeLayout中;最后对外公布一个裁剪的方法,返回一个Bitmap;

    暂时的分析就这样,下面我们来写代码~

    首先是白色框框那个自定义View,我们叫做ClipImageBorderView

    3、ClipImageBorderView

    分析下这个View,其实就是根据在屏幕中绘制一个正方形,正方形区域以外为半透明,绘制这个正方形需要与屏幕左右边距有个边距。

    我们准备按如下图绘制:


    按顺序在View的onDraw里面绘制上图中:1、2、3、4,四个半透明的区域,然后在中间正方形区域绘制一个正方形

    下面看下代码:

    package com.zhy.view;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.Paint.Style;
    import android.util.AttributeSet;
    import android.util.TypedValue;
    import android.view.View;
    /**
     * @author zhy
     *
     */
    public class ClipImageBorderView extends View
    {
    	/**
    	 * 水平方向与View的边距
    	 */
    	private int mHorizontalPadding = 20;
    	/**
    	 * 垂直方向与View的边距
    	 */
    	private int mVerticalPadding;
    	/**
    	 * 绘制的矩形的宽度
    	 */
    	private int mWidth;
    	/**
    	 * 边框的颜色,默认为白色
    	 */
    	private int mBorderColor = Color.parseColor("#FFFFFF");
    	/**
    	 * 边框的宽度 单位dp
    	 */
    	private int mBorderWidth = 1;
    
    	private Paint mPaint;
    
    	public ClipImageBorderView(Context context)
    	{
    		this(context, null);
    	}
    
    	public ClipImageBorderView(Context context, AttributeSet attrs)
    	{
    		this(context, attrs, 0);
    	}
    
    	public ClipImageBorderView(Context context, AttributeSet attrs, int defStyle)
    	{
    		super(context, attrs, defStyle);
    		// 计算padding的px
    		mHorizontalPadding = (int) TypedValue.applyDimension(
    				TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()
    						.getDisplayMetrics());
    		mBorderWidth = (int) TypedValue.applyDimension(
    				TypedValue.COMPLEX_UNIT_DIP, mBorderWidth, getResources()
    						.getDisplayMetrics());
    		mPaint = new Paint();
    		mPaint.setAntiAlias(true);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas)
    	{
    		super.onDraw(canvas);
    		//计算矩形区域的宽度
    		mWidth = getWidth() - 2 * mHorizontalPadding;
    		//计算距离屏幕垂直边界 的边距
    		mVerticalPadding = (getHeight() - mWidth) / 2;
    		mPaint.setColor(Color.parseColor("#aa000000"));
    		mPaint.setStyle(Style.FILL);
    		// 绘制左边1
    		canvas.drawRect(0, 0, mHorizontalPadding, getHeight(), mPaint);
    		// 绘制右边2
    		canvas.drawRect(getWidth() - mHorizontalPadding, 0, getWidth(),
    				getHeight(), mPaint);
    		// 绘制上边3
    		canvas.drawRect(mHorizontalPadding, 0, getWidth() - mHorizontalPadding,
    				mVerticalPadding, mPaint);
    		// 绘制下边4
    		canvas.drawRect(mHorizontalPadding, getHeight() - mVerticalPadding,
    				getWidth() - mHorizontalPadding, getHeight(), mPaint);
    		// 绘制外边框
    		mPaint.setColor(mBorderColor);
    		mPaint.setStrokeWidth(mBorderWidth);
    		mPaint.setStyle(Style.STROKE);
    		canvas.drawRect(mHorizontalPadding, mVerticalPadding, getWidth()
    				- mHorizontalPadding, getHeight() - mVerticalPadding, mPaint);
    
    	}
    
    }
    
    我们直接预设了一个水平方向的边距,根据边距计算出正方形的边长,接下来就是按照上图分别会1、2、3、4四个区域,最后就是绘制我们的正方形~~

    代码还是很简单的~~我们的ClipImageBorderView就搞定了,我们决定来测试一下:

    布局文件:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/a"   >
    
        <com.zhy.view.ClipImageBorderView
            android:id="@+id/id_clipImageLayout"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    
    </RelativeLayout>

    效果图:


    故意放了个背景,没撒用,就是为了能看出效果,可以看到我们的框框绘制的还是蛮不错的~~嗯,这个框框距离屏幕左右两侧的距离应该抽取出来,嗯,后面再说~

    4、ClipZoomImageView

    我们准备对我们原先的ZoomImageView进行简单的修改,修改的地方:
    1、在onGlobalLayout方法中,如果图片的宽或者高只要一个小于我们的正方形的边长,我们会直接把较小的尺寸放大至正方形的边长;如果图片的宽和高都大于我们的正方形的边长,我们仅仅把图片移动到我们屏幕的中央,不做缩放处理;

    2、根据步骤1,我们会获得初始的缩放比例(默认为1.0f),然后SCALE_MID , 与 SCALE_MAX 分别为2倍和4倍的初始化缩放比例。

    3、图片在移动过程中的边界检测完全根据正方形的区域,图片不会在移动过程中与正方形区域产生内边距

    4、对外公布一个裁切的方法

    部分代码:

    /**
    	 * 水平方向与View的边距
    	 */
    	private int mHorizontalPadding = 20;
    	/**
    	 * 垂直方向与View的边距
    	 */
    	private int mVerticalPadding;
    
    	@Override
    	public void onGlobalLayout()
    	{
    		if (once)
    		{
    			Drawable d = getDrawable();
    			if (d == null)
    				return;
    			Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight());
    			// 计算padding的px
    			mHorizontalPadding = (int) TypedValue.applyDimension(
    					TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding,
    					getResources().getDisplayMetrics());
    			// 垂直方向的边距
    			mVerticalPadding = (getHeight() - (getWidth() - 2 * mHorizontalPadding)) / 2;
    
    			int width = getWidth();
    			int height = getHeight();
    			// 拿到图片的宽和高
    			int dw = d.getIntrinsicWidth();
    			int dh = d.getIntrinsicHeight();
    			float scale = 1.0f;
    			if (dw < getWidth() - mHorizontalPadding * 2
    					&& dh > getHeight() - mVerticalPadding * 2)
    			{
    				scale = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw;
    			}
    
    			if (dh < getHeight() - mVerticalPadding * 2
    					&& dw > getWidth() - mHorizontalPadding * 2)
    			{
    				scale = (getHeight() * 1.0f - mVerticalPadding * 2) / dh;
    			}
    
    			if (dw < getWidth() - mHorizontalPadding * 2
    					&& dh < getHeight() - mVerticalPadding * 2)
    			{
    				float scaleW = (getWidth() * 1.0f - mHorizontalPadding * 2)
    						/ dw;
    				float scaleH = (getHeight() * 1.0f - mVerticalPadding * 2) / dh;
    				scale = Math.max(scaleW, scaleH);
    			}
    
    			initScale = scale;
    			SCALE_MID = initScale * 2;
    			SCALE_MAX = initScale * 4;
    			Log.e(TAG, "initScale = " + initScale);
    			mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);
    			mScaleMatrix.postScale(scale, scale, getWidth() / 2,
    					getHeight() / 2);
    			// 图片移动至屏幕中心
    			setImageMatrix(mScaleMatrix);
    			once = false;
    		}
    
    	}
    
    	/**
    	 * 剪切图片,返回剪切后的bitmap对象
    	 * 
    	 * @return
    	 */
    	public Bitmap clip()
    	{
    		Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(),
    				Bitmap.Config.ARGB_8888);
    		Canvas canvas = new Canvas(bitmap);
    		draw(canvas);
    		return Bitmap.createBitmap(bitmap, mHorizontalPadding,
    				mVerticalPadding, getWidth() - 2 * mHorizontalPadding,
    				getWidth() - 2 * mHorizontalPadding);
    	}
    	
    	/**
    	 * 边界检测
    	 */
    	private void checkBorder()
    	{
    
    		RectF rect = getMatrixRectF();
    		float deltaX = 0;
    		float deltaY = 0;
    
    		int width = getWidth();
    		int height = getHeight();
    
    		// 如果宽或高大于屏幕,则控制范围
    		if (rect.width() >= width - 2 * mHorizontalPadding)
    		{
    			if (rect.left > mHorizontalPadding)
    			{
    				deltaX = -rect.left + mHorizontalPadding;
    			}
    			if (rect.right < width - mHorizontalPadding)
    			{
    				deltaX = width - mHorizontalPadding - rect.right;
    			}
    		}
    		if (rect.height() >= height - 2 * mVerticalPadding)
    		{
    			if (rect.top > mVerticalPadding)
    			{
    				deltaY = -rect.top + mVerticalPadding;
    			}
    			if (rect.bottom < height - mVerticalPadding)
    			{
    				deltaY = height - mVerticalPadding - rect.bottom;
    			}
    		}
    		mScaleMatrix.postTranslate(deltaX, deltaY);
    
    	}

    这里贴出了改变的代码,完整的代码就不贴了,太长了,如果大家学习过前面的博客应该也会比较熟悉,若没有也没事,后面会提供源码。

    贴代码的目的,第一让大家看下我们改变了哪些;第二,我想暴露出我们代码中的问题,我们设置了一个这样的变量:mHorizontalPadding = 20;这个是手动和ClipImageBorderView里面的成员变量mHorizontalPadding 写的一致,也就是说这个变量,两个自定义的View都需要使用且需要相同的值,目前我们的做法,写死且每个View各自定义一个。这种做法不用说,肯定不好,即使抽取成自定义属性,两个View都需要进行抽取,且用户在使用的时候,还需要设置为一样的值,总觉得有点强人所难~~

    5、不一样的自定义控件

    现在我们考虑下:易用性。目前为止,其实我们的效果已经实现了,但是需要用户这么写布局文件:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#aaaaaa" >
    
        <com.zhy.view.ZoomImageView
            android:id="@+id/id_zoomImageView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:scaleType="matrix"
            android:src="@drawable/a" />
    
        <com.zhy.view.ClipImageView
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    
    </RelativeLayout>

    然后这两个类中都有一个mHorizontalPadding变量,且值一样,上面也说过,即使抽取成自定义变量,也需要在布局文件中每个View中各写一次。so, we need change . 这样的耦合度太夸张了,且使用起来蹩脚。

    于是乎,我决定把这两个控件想办法整到一起,用户使用时只需要声明一个控件:

    怎么做呢,我们使用组合的思想来自定义控件,我们再声明一个控件,继承子RelativeLayout,然后在这个自定义RelativeLayout中通过代码添加这两个自定义的布局,并且设置一些公用的属性,具体我们就开始行动。

    1、ClipImageLayout

    我们自定义一个RelativeLayout叫做ClipImageLayout,用于放置我们的两个自定义View,并且由ClipImageLayout进行设置边距,然后传给它内部的两个View,这样的话,跟用户交互的就一个ClipImageLayout,用户只需要设置一次边距即可。

    完整的ClipImageLayout代码:

    package com.zhy.view;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.util.AttributeSet;
    import android.util.TypedValue;
    import android.widget.RelativeLayout;
    
    import com.zhy.clippic.R;
    /**
     * zhy
     * @author zhy
     *
     */
    public class ClipImageLayout extends RelativeLayout
    {
    
    	private ClipZoomImageView mZoomImageView;
    	private ClipImageBorderView mClipImageView;
    
    	/**
    	 * 这里测试,直接写死了大小,真正使用过程中,可以提取为自定义属性
    	 */
    	private int mHorizontalPadding = 20;
    
    	public ClipImageLayout(Context context, AttributeSet attrs)
    	{
    		super(context, attrs);
    
    		mZoomImageView = new ClipZoomImageView(context);
    		mClipImageView = new ClipImageBorderView(context);
    
    		android.view.ViewGroup.LayoutParams lp = new LayoutParams(
    				android.view.ViewGroup.LayoutParams.MATCH_PARENT,
    				android.view.ViewGroup.LayoutParams.MATCH_PARENT);
    		
    		/**
    		 * 这里测试,直接写死了图片,真正使用过程中,可以提取为自定义属性
    		 */
    		mZoomImageView.setImageDrawable(getResources().getDrawable(
    				R.drawable.a));
    		
    		this.addView(mZoomImageView, lp);
    		this.addView(mClipImageView, lp);
    
    		
    		// 计算padding的px
    		mHorizontalPadding = (int) TypedValue.applyDimension(
    				TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()
    						.getDisplayMetrics());
    		mZoomImageView.setHorizontalPadding(mHorizontalPadding);
    		mClipImageView.setHorizontalPadding(mHorizontalPadding);
    	}
    
    	/**
    	 * 对外公布设置边距的方法,单位为dp
    	 * 
    	 * @param mHorizontalPadding
    	 */
    	public void setHorizontalPadding(int mHorizontalPadding)
    	{
    		this.mHorizontalPadding = mHorizontalPadding;
    	}
    
    	/**
    	 * 裁切图片
    	 * 
    	 * @return
    	 */
    	public Bitmap clip()
    	{
    		return mZoomImageView.clip();
    	}
    
    }
    

    可以看到,现在用户需要使用头像裁切功能只需要声明下ClipImageLayout即可,完全避免了上述我们描述的问题,我们对用户屏蔽了两个真正实现的类。这个也是自定义控件的一种方式,希望可以借此抛砖引玉,大家能够更加合理的设计出自己的控件~~

    好了,我们的ClipImageLayout搞定以后,下面看下如何使用~

    6、用法

    1、布局文件

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#aaaaaa"   >
    
        <com.zhy.view.ClipImageLayout
            android:id="@+id/id_clipImageLayout"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
    
    </RelativeLayout>

    2、MainActivity

    package com.zhy.clippic;
    
    import java.io.ByteArrayOutputStream;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.graphics.Bitmap;
    import android.os.Bundle;
    import android.view.Menu;
    import android.view.MenuItem;
    
    import com.zhy.view.ClipImageLayout;
    
    public class MainActivity extends Activity
    {
    	private ClipImageLayout mClipImageLayout;
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState)
    	{
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    
    		mClipImageLayout = (ClipImageLayout) findViewById(R.id.id_clipImageLayout);
    
    	}
    
    	@Override
    	public boolean onCreateOptionsMenu(Menu menu)
    	{
    		getMenuInflater().inflate(R.menu.main, menu);
    		return true;
    	}
    
    	@Override
    	public boolean onOptionsItemSelected(MenuItem item)
    	{
    		switch (item.getItemId())
    		{
    		case R.id.id_action_clip:
    			Bitmap bitmap = mClipImageLayout.clip();
    			
    			ByteArrayOutputStream baos = new ByteArrayOutputStream();
    			bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    			byte[] datas = baos.toByteArray();
    			
    			Intent intent = new Intent(this, ShowImageActivity.class);
    			intent.putExtra("bitmap", datas);
    			startActivity(intent);
    
    			break;
    		}
    		return super.onOptionsItemSelected(item);
    	}
    }
    

    我们在menu里面体检了一个裁切的按钮,点击后把裁切好的图片传递给我们的ShowImageActivity

    看一下眼menu的xml

    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
    
        <item
            android:id="@+id/id_action_clip"
            android:icon="@drawable/actionbar_clip_icon"
            android:showAsAction="always|withText"
            android:title="裁切"/>
    
    </menu>

    3、ShowImageActivity

    package com.zhy.clippic;
    
    
    import android.app.Activity;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.os.Bundle;
    import android.widget.ImageView;
    
    
    public class ShowImageActivity extends Activity
    {
    	private ImageView mImageView;
    
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState)
    	{
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.show);
    
    
    		mImageView = (ImageView) findViewById(R.id.id_showImage);
    		byte[] b = getIntent().getByteArrayExtra("bitmap");
    		Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
    		if (bitmap != null)
    		{
    			mImageView.setImageBitmap(bitmap);
    		}
    	}
    }


    layout/show.xml
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff" >
    
        <ImageView
            android:id="@+id/id_showImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:src="@drawable/tbug"
             />
    
    </RelativeLayout>


    好了,到此我们的 高仿微信头像截取功能 就已经结束了~~希望大家可以从本篇博客中可以领悟到something~


    最后我们把ClipImageLayout里面的mHorizontalPadding设置为50,贴个静态效果图~



    ok ~~


    源码点击下载



    ---------------------------------------------------------------------------------------------------------

    建了一个QQ群,方便大家交流。群号:55032675



    ----------------------------------------------------------------------------------------------------------

    博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

    1、高仿微信5.2.1主界面及消息提醒

    2、高仿QQ5.0侧滑

















    版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 相关阅读:
    【z02】选择客栈
    JavaEE(22)
    驱动表问题
    影响子查询展开的情况
    SQL*Net message from client
    等值链接和非等值链接
    ||拼接列关联和直接关联区别
    分页SQL优化
    SQL*Net more data to client
    SQL*Net message to client
  • 原文地址:https://www.cnblogs.com/dingxiaoyue/p/4924882.html
Copyright © 2020-2023  润新知