本篇主要讲解如何实现一个简易的选择上传图片时的展示控件,该自定义控件继承自ViewGroup,支持网格排列,以及横向排列。最终效果如下图:
网格布局上图中每个ImageView的右上角都有一个删除按钮,我们可以通过组合View或者自定义View的方式去实现,这里选择自定义方式,自定义GridItemView
继承自ImageView
。
我们知道自定义View一般流程为:
编写自定义属性 在构造函数中获取自定义属性 在onMeasure
中测量宽高,如有需要务必考虑支持padding属性和wrap_content,margin不用管,它是由父布局控制的。
重写onDraw
来进行绘制,以达到我们所需要的效果。
由于我们只是想在原图上画一个删除按钮,因此只需重写onDraw
即可。
private void init(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mDelBound=new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int foregroundWidth=30;
int foregroundHeight=30;
int delBoundPadding=5;
//先画灰色背景
mPaint.setColor(0x88000000);
mDelBound.set(getWidth()-getPaddingRight()-foregroundWidth-delBoundPadding*2
,getPaddingTop()
,getWidth()-getPaddingRight()
,getPaddingTop()+foregroundHeight+delBoundPadding*2);
canvas.drawRect(mDelBound,mPaint);
//再画两条交叉线
mPaint.setColor(0xffffffff);
canvas.drawLine(mDelBound.left+delBoundPadding
,mDelBound.top+delBoundPadding
,mDelBound.right-delBoundPadding
,mDelBound.bottom-delBoundPadding,mPaint);
canvas.drawLine(mDelBound.left+delBoundPadding
,mDelBound.bottom-delBoundPadding
,mDelBound.right-delBoundPadding
,mDelBound.top+delBoundPadding,mPaint);
}
上述代码实现了画灰色背景和两条交叉钱,此外,还可以根据实际情况,直接使用drawBitmap
来画按钮。
接下来,我们给这个删除按钮加上点击事件,通过接口的形式对外提供按钮点击功能。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
boolean touchable = event.getX() > mDelBound.left
&& event.getX() mDelBound.top&&event.getY()<mDelBound.bottom;
if (touchable&&mDelClickL!=null) {//点击删除键
mDelClickL.onDelClickL();
return true;
}
}
return super.onTouchEvent(event);
}
自定义ViewGroup
流程一般如下,但由于实际需求不同,并不是每个步骤都需要重写。
编写自定义属性 在构造函数中获取自定义属性 在onMeasure中测量宽高,并测量子view宽高。 重写onLayout来对子View进行布局。 如有需要重写onDraw来进行添加效果,绝大多数并不需要。由于我们需要实现两种排列方式,所以onMeasure
和onLayout
的代码如下:
@Override
protected void (int widthMeasureSpec, int heightMeasureSpec) {//测量
if(mShowStyle==STYLE_HORIZONTAL){
measureHorizontal(widthMeasureSpec,heightMeasureSpec);
}else{
measureVertical(widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(mShowStyle==STYLE_HORIZONTAL){
layoutHorizontal(changed,l,t,r,b);
}else {
layoutVertical(changed,l,t,r,b);
}
}
measure
的计算比较简单,主要是计算一下高度。
private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec){
int width = MeasureSpec.getSize(widthMeasureSpec);
int height;
int totalWidth = width - getPaddingLeft() - getPaddingRight();
mGridSize = (totalWidth - mGap * (mColumnCount - 1)) / mColumnCount; //算出每个条目的大小,以宽度为标准。
height = mGridSize + getPaddingTop() + getPaddingBottom();//计算出高度
setMeasuredDimension(width, height);
}
private void measureVertical(int widthMeasureSpec, int heightMeasureSpec){
int width = MeasureSpec.getSize(widthMeasureSpec);
int height;
int totalWidth = width - getPaddingLeft() - getPaddingRight();
int totalCount=mImgDataList.size()+1;//把mAddView也给算进去
mGridSize = (totalWidth - mGap * (mColumnCount - 1)) / mColumnCount; //算出每个条目的大小,以宽度为标准。
int mRowCount= (int) Math.ceil((totalCount*1.0)/mColumnCount);//算出行数
height = mGridSize * mRowCount + mGap * (mRowCount - 1) + getPaddingTop() + getPaddingBottom();//计算出高度
setMeasuredDimension(width, height);
}
layout
中,横向布局当图片显示超过控件宽度时,我们希望能自动移动到最右边,代码如下,主要是通过scrollTo
方法实现的,当mRightBorder-mLeftBorder>getWidth()
时,即可认为图片显示大于控件宽度。
//水平布局
private void layoutHorizontal(boolean changed, int l, int t, int r, int b) {
int childrenCount = mImgDataList.size();
for (int i = 0; i getWidth()){
scrollTo(mRightBorder - getWidth(),0);
}else{
scrollTo(mLeftBorder,0);
}
}
网格布局的代码如下:
private void layoutVertical(boolean changed, int l, int t, int r, int b) {
int childrenCount = mImgDataList.size();
for (int i = 0; i < childrenCount; i++) {
ImageView childrenView = (ImageView) getChildAt(i);
if (mAdapter != null) {
mAdapter.onDisplayImage(getContext(), childrenView, mImgDataList.get(i));
}
int rowNum = i / mColumnCount;
int columnNum = i % mColumnCount;
int left = (mGridSize + mGap) * columnNum + getPaddingLeft();
int top = (mGridSize + mGap) * rowNum + getPaddingTop();
int right = left + mGridSize;
int bottom = top + mGridSize;
childrenView.layout(left, top, right, bottom);
}
int rowNum = (childrenCount) / mColumnCount;
int columnNum = (childrenCount) % mColumnCount;
int left = (mGridSize + mGap) * columnNum + getPaddingLeft();
int top = (mGridSize + mGap) * rowNum + getPaddingTop();
int right = left + mGridSize;
int bottom = top + mGridSize;
mAddView.layout(left, top, right, bottom);//调整mAddView的位置
}
自定义滑动事件
当图片显示超过ViewGroup的宽度时,为了使交互体验更友好,需要加入滑动功能。在实现之前,我们务必理清楚ViewGroup中dispatchTouchEvent
,onInterceptTouchEvent
,onTouchEvent
中的作用以及事件传递机制。
当检测到水平布局进行了水平滑动时,应当拦截事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
float x = event.getX();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
if(mScroller != null){
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
}
mLastX = x; //记住开始落下的屏幕点
break;
case MotionEvent.ACTION_MOVE:
int detaX = (int) (x-mLastX);
if(Math.abs(detaX)>mTouchSlop&&mShowStyle==STYLE_HORIZONTAL){
return true;
}
break;
}
return super.onInterceptTouchEvent(event);
}
重写onTouchEvent
。在MotionEvent.ACTION_MOVE
中通过scrollBy来实现滑动效果,在MotionEvent.ACTION_UP
中使用Scroller
加入惯性滑动效果以增强交互体验。
private float mLastX;//记录上次滑动的位置
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean isTouch=false;
acquireVelocityTracker(event);
//触摸点
float x = event.getX();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
if(mScroller != null){
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
}
mLastX = x ;
isTouch=false;
break ;
case MotionEvent.ACTION_MOVE:
int detaX = (int)(mLastX-x); //每次滑动屏幕,屏幕应该移动的距离
if (getScrollX() + detaX mRightBorder) {
if(mRightBorder-mLeftBorder>getWidth()){
scrollTo(mRightBorder - getWidth(),0);
}else{
scrollTo(mLeftBorder,0);
}
}else{
scrollBy(detaX, 0);
}
mLastX = x ;
isTouch=true;
break ;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isTouch=false;
mVelocityTracker.computeCurrentVelocity(1000,maxFlingSpeed);
int speed= (int) mVelocityTracker.getXVelocity();
if(Math.abs(speed)>minFlingSpeed){
mScroller.fling(getScrollX(), 0, -speed, 0, mLeftBorder, mRightBorder-getWidth(), 0, 0);
invalidate();
}
releaseVelocityTracker();
break;
}
return isTouch;
}
private void acquireVelocityTracker(MotionEvent event) {
if(null == mVelocityTracker) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
private void releaseVelocityTracker() {
if(null != mVelocityTracker) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
最后,当我们给这个ViewGroup设置图片数据时,刷新布局代码如下:
private void refreshDataSet() {//根据数据,调整ImageView的数量
int oldViewCount = getChildCount()-1;//上次的item个数,由于多了个mAddView,因此要减去1
int newViewCount = mImgDataList.size();//这次的item个数
if (oldViewCount > newViewCount) {
removeViews(newViewCount, oldViewCount - newViewCount);
} else if (oldViewCount < newViewCount) {
for (int i = oldViewCount; i < newViewCount; i++) {
ImageView iv = getImageView(i);
addView(iv, i,generateDefaultLayoutParams());
}
}
requestLayout();
}
最后
完整代码见:https://github.com/maplejaw/GridImageView
使用时直接拷贝以下三个文件到工程中即可: