[置顶] android平台TextView使用ImageSpan展示GIF图片

 

android-gif-drawable(https://github.com/koral--/android-gif-drawable/releases)开源项目---是一个蛮不错的android gif显示实现.本文在android-gif-drawable基础上介绍如何实现TextView、EditText上展示Gif动态图。

网上有蛮多介绍这个框架使用的文章,比如http://www.open-open.com/lib/view/open1404888098200.html。



核心类GifDrawable间隔一定时间读取下一帧数据,然后执行invalidateSelf()----》CallBack::invalidateDrawable()---》View::verifyDrawable()和View::invalidate(),该帧数据刷新流程就执行结束。
而android-gif-drawable框架目前已支持GifImageView、GifImageButton、GifTextView三个android widget,且GifImageView、GifImageButton支持对src和backgroud设置Gif,而GifTextView对支持backgroud和CompoundDrawables设置Gif。

现在很多app都支持Gif表情,但貌似还没有一个app对输入框(等)支持GIF。而基本所有的表情图片(包括Emoji)都是使用ImageSpan实现的。但默认的ImageSpan是无法支持GIF的。
参考android-gif-drawable框架中gif帧数据刷新流程,要支持GIF需要考虑并完成下面三个操作:
1)对ImageSpan中的GifDrawable,何时设置其Callback,又何时清空该Callback,目前TextView、ImageSpan和Spaned都没有设置Callback的地方,我们需要找一个合适的地方将TextView设置为GifDrawable的Callback;
2)在TextView::invalidateDrawable()中实现对GifDrawable的校验,即验证该GifDrawable是TextView的内容,需要刷新;
3)在TextView::invalidateDrawable()中实现如何刷新TextView显示;

首先对于1),我们参考下ImageView和TextView实现。ImageView的src drawable对应实现如下:
	/**
     * Sets a drawable as the content of this ImageView.
     * 
     * @param drawable The drawable to set
     */
    public void setImageDrawable(Drawable drawable) {
        if (mDrawable != drawable) {
            ...
            updateDrawable(drawable);
			...
        }
    }
	
	private void updateDrawable(Drawable d) {
        if (mDrawable != null) {
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
        }
        mDrawable = d;
        if (d != null) {
            d.setCallback(this);
            if (d.isStateful()) {
                d.setState(getDrawableState());
            }
            d.setLevel(mLevel);
            d.setLayoutDirection(getLayoutDirection());
            d.setVisible(getVisibility() == VISIBLE, true);
            mDrawableWidth = d.getIntrinsicWidth();
            mDrawableHeight = d.getIntrinsicHeight();
            applyColorMod();
            configureBounds();
        } else {
            mDrawableWidth = mDrawableHeight = -1;
        }
    }
也就是说,ImageView在设置其src时,清空旧mDrawable的callback,然后将新设置的src drawable的callback设置为ImageView本身。
同理,TextView对于CompoundDrawables的callback处理也是在setCompoundDrawables()时。
而ImageSpan需要在什么时机设置GifDrawable的callback呢,
public class GifImageSpan extends ImageSpan{

	private Drawable mDrawable = null;
	
	public GifImageSpan(Drawable d) {
		super(d);
		mDrawable = d;
	}
	
	public GifImageSpan(Drawable d, int verticalAlignment) {
		super(d, verticalAlignment);
		mDrawable = d;
	}

	@Override
	public Drawable getDrawable() {
		return mDrawable;
	}
}

public class GifEditText extends EditText {
	
	private GifSpanChangeWatcher mGifSpanChangeWatcher;
	public GifEditText(Context context) {
		super(context);
		initGifSpanChangeWatcher();
	}

	public GifEditText(Context context, AttributeSet attrs) {
		super(context, attrs);
		initGifSpanChangeWatcher();
	}

	public GifEditText(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initGifSpanChangeWatcher();
	}

	private void initGifSpanChangeWatcher() {
		mGifSpanChangeWatcher = new GifSpanChangeWatcher(this);
		addTextChangedListener(mGifSpanChangeWatcher);
	}
	
	@Override
	public void setText(CharSequence text, BufferType type) {

		CharSequence oldText = null;
		try {
			//EditText的默认mText为"",是一个String,但getText()强转为Editable,尼玛,只能try/catch了
			oldText = getText();
			//首先清空所有旧GifImageSpan的callback和oldText上的GifSpanChangeWatcher
			if (!TextUtils.isEmpty(oldText) && oldText instanceof Spannable) {
				Spannable sp = (Spannable) oldText;
				final GifImageSpan[] spans = sp.getSpans(0, sp.length(), GifImageSpan.class);
		        final int count = spans.length;
		        for (int i = 0; i < count; i++) {
		        	spans[i].getDrawable().setCallback(null);
		        }
		        
		        final GifSpanChangeWatcher[] watchers = sp.getSpans(0, sp.length(), GifSpanChangeWatcher.class);
	            final int count1 = watchers.length;
	            for (int i = 0; i < count1; i++) {
	                sp.removeSpan(watchers[i]);
	            }
			}
		} catch (Exception e) {
			
		}
		
		
		if (!TextUtils.isEmpty(text)) {
			if (!(text instanceof Editable)) {
				text = new SpannableStringBuilder(text);
			}
		}
		
		if (!TextUtils.isEmpty(text) && text instanceof Spannable) {
			Spannable sp = (Spannable) text;
			//设置新text中所有GifImageSpan的callback为当前EditText
			final GifImageSpan[] spans = sp.getSpans(0, sp.length(), GifImageSpan.class);
	        final int count = spans.length;
	        for (int i = 0; i < count; i++) {
	        	spans[i].getDrawable().setCallback(this);
	        }
	        
	        //清空新text上的GifSpanChangeWatcher
	        final GifSpanChangeWatcher[] watchers = sp.getSpans(0, sp.length(), GifSpanChangeWatcher.class);
            final int count1 = watchers.length;
            for (int i = 0; i < count1; i++) {
                sp.removeSpan(watchers[i]);
            }
            
	        if (mGifSpanChangeWatcher == null) {
				mGifSpanChangeWatcher = new GifSpanChangeWatcher(this);
			}
			
	        //设置新text上的GifSpanChangeWatcher
			sp.setSpan(mGifSpanChangeWatcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE | (100 << Spanned.SPAN_PRIORITY_SHIFT));
		}

		super.setText(text, type);
	}
}



public class GifSpanChangeWatcher implements SpanWatcher, TextWatcher{

	private Drawable.Callback mCallback;
	
	public GifSpanChangeWatcher(Drawable.Callback callback) {
		mCallback = callback;
	}
    public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
        //do nothing
    }

    public void onSpanAdded(Spannable buf, Object what, int s, int e) {
        //设置callback
    	if (what instanceof GifImageSpan) {
    		((GifImageSpan)what).getDrawable().setCallback(mCallback);
    	}
    }

    public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
    	//清空callback
    	if (what instanceof GifImageSpan) {
    		((GifImageSpan)what).getDrawable().setCallback(null);
    	}
    }
    
	@Override
	public void afterTextChanged(Editable s) {
		if (s != null) {
			s.setSpan(this, 0, s.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE | (100 << Spanned.SPAN_PRIORITY_SHIFT));
		}
	}
	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {
		// TODO Auto-generated method stub
		
	}
	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {
		// TODO Auto-generated method stub
		
	}

}


也就是,在setText()和onSpanAdded()、onSpanRemoved()中执行操作(1)

然后,对于2),同样参考ImageView和TextView
@Override
    protected boolean verifyDrawable(Drawable dr) {
        return mDrawable == dr || super.verifyDrawable(dr);
    }

@Override
    protected boolean verifyDrawable(Drawable who) {
        final boolean verified = super.verifyDrawable(who);
        if (!verified && mDrawables != null) {
            return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
                    who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom ||
                    who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
        }
        return verified;
    }
直接上代码
public class GifEditText extends EditText {

	private GifImageSpan getImageSpan(Drawable drawable) {
		GifImageSpan imageSpan = null;
		CharSequence text = getText();
		if (!TextUtils.isEmpty(text)) {
			if (text instanceof Spanned) {
				Spanned spanned = (Spanned) text;
				GifImageSpan[] spans = spanned.getSpans(0, text.length(), GifImageSpan.class);
				if (spans != null && spans.length > 0) {
					for (GifImageSpan span : spans) {
						if (drawable == span.getDrawable()) {
							imageSpan = span;
						}
					}
				}
			}
		}

		return imageSpan;
	}
}
getImageSpan()方法通过getSpans()获取所有的GifImageSpan,然后对比drawable,返回相应的GifImageSpan。

最后,操作3)更新View显示。同样参考下TextView
@Override
    public void invalidateDrawable(Drawable drawable) {
        if (verifyDrawable(drawable)) {
            final Rect dirty = drawable.getBounds();
            int scrollX = mScrollX;
            int scrollY = mScrollY;

            // IMPORTANT: The coordinates below are based on the coordinates computed
            // for each compound drawable in onDraw(). Make sure to update each section
            // accordingly.
            final TextView.Drawables drawables = mDrawables;
            if (drawables != null) {
                if (drawable == drawables.mDrawableLeft) {
                    final int compoundPaddingTop = getCompoundPaddingTop();
                    final int compoundPaddingBottom = getCompoundPaddingBottom();
                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;

                    scrollX += mPaddingLeft;
                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
                } else if (drawable == drawables.mDrawableRight) {
                    final int compoundPaddingTop = getCompoundPaddingTop();
                    final int compoundPaddingBottom = getCompoundPaddingBottom();
                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;

                    scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
                } else if (drawable == drawables.mDrawableTop) {
                    final int compoundPaddingLeft = getCompoundPaddingLeft();
                    final int compoundPaddingRight = getCompoundPaddingRight();
                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;

                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
                    scrollY += mPaddingTop;
                } else if (drawable == drawables.mDrawableBottom) {
                    final int compoundPaddingLeft = getCompoundPaddingLeft();
                    final int compoundPaddingRight = getCompoundPaddingRight();
                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;

                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
                    scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
                }
            }

            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
        }
    }
计算compoundDrawable位置区域,然后执行invalidate。对于GifEditText貌似也可以类似操作,根据GifImageSpan的start、end计算其位置区域,然后执行invalidate()。不过计算过程太过复杂了。不过android4.4的TextView提供这个方法void invalidateRegion(int start, int end, boolean invalidateCursor) 方法用于刷新start和end之间的区域,但还是蛮复杂的看的人眼花缭乱。研究了下这个方法最终是由谁调用的。

invalidateRegion()<<---invalidateCursor()<<---spanChange()<<---ChangeWatcher::onSpanChanged()、ChangeWatcher::onSpanAdded()、ChangeWatcher::onSpanRemoved()

也就是说,只要TextView内容中span发生变化都会触发invalidateRegion()来刷新对应区域和cursor。
@Override
	public void invalidateDrawable(Drawable drawable) {
		GifImageSpan imageSpan = getImageSpan(drawable);
		Log.e("", "invalidateDrawable imageSpan:" + imageSpan);
		if (imageSpan != null) {
			CharSequence text = getText();
			if (!TextUtils.isEmpty(text)) {
				if (text instanceof Editable) {
					Log.e("", "invalidateDrawable Editable:");
					Editable editable = (Editable)text;
					int start = editable.getSpanStart(imageSpan);
					int end = editable.getSpanEnd(imageSpan);
					int flags = editable.getSpanFlags(imageSpan);

					editable.setSpan(imageSpan, start, end, flags);
				}
			}
			
		} else {
			super.invalidateDrawable(drawable);
		}
	}
直接重新设置该ImageSpan即可触发ChangeWatcher::onSpanChanged()回调,也就会立即刷新其区域和cursor。

大功告成,运行ok。
上面是对EditText的实现,针对TextView实现稍微有点区别
public class GifSpanTextView extends GifTextView {

	private GifSpanChangeWatcher mGifSpanChangeWatcher;
	public GifSpanTextView(Context context) {
		super(context);
		initGifSpanChangeWatcher();
	}

	public GifSpanTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initGifSpanChangeWatcher();
	}

	public GifSpanTextView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initGifSpanChangeWatcher();
	}

	private void initGifSpanChangeWatcher() {
		mGifSpanChangeWatcher = new GifSpanChangeWatcher(this);
		addTextChangedListener(mGifSpanChangeWatcher);
	}

	@Override
	public void setText(CharSequence text, BufferType type) {
		type = BufferType.EDITABLE;
		CharSequence oldText = getText();
		if (!TextUtils.isEmpty(oldText) && oldText instanceof Spannable) {
			Spannable sp = (Spannable) oldText;
			final GifImageSpan[] spans = sp.getSpans(0, sp.length(), GifImageSpan.class);
	        final int count = spans.length;
	        for (int i = 0; i < count; i++) {
	        	spans[i].getDrawable().setCallback(null);
	        }
	        
	        final GifSpanChangeWatcher[] watchers = sp.getSpans(0, sp.length(), GifSpanChangeWatcher.class);
            final int count1 = watchers.length;
            for (int i = 0; i < count1; i++) {
                sp.removeSpan(watchers[i]);
            }
		}
		
		if (!TextUtils.isEmpty(text) && text instanceof Spannable) {
			Spannable sp = (Spannable) text;
			final GifImageSpan[] spans = sp.getSpans(0, sp.length(), GifImageSpan.class);
	        final int count = spans.length;
	        for (int i = 0; i < count; i++) {
	        	spans[i].getDrawable().setCallback(this);
	        }
	        
	        final GifSpanChangeWatcher[] watchers = sp.getSpans(0, sp.length(), GifSpanChangeWatcher.class);
            final int count1 = watchers.length;
            for (int i = 0; i < count1; i++) {
                sp.removeSpan(watchers[i]);
            }
            
	        if (mGifSpanChangeWatcher == null) {
				mGifSpanChangeWatcher = new GifSpanChangeWatcher(this);;
			}
			
			sp.setSpan(mGifSpanChangeWatcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE | (100 << Spanned.SPAN_PRIORITY_SHIFT));
		}
		
		
		super.setText(text, type);
	}

	private GifImageSpan getImageSpan(Drawable drawable) {
		GifImageSpan imageSpan = null;
		CharSequence text = getText();
		if (!TextUtils.isEmpty(text)) {
			if (text instanceof Spanned) {
				Spanned spanned = (Spanned) text;
				GifImageSpan[] spans = spanned.getSpans(0, text.length(), GifImageSpan.class);
				if (spans != null && spans.length > 0) {
					for (GifImageSpan span : spans) {
						if (drawable == span.getDrawable()) {
							imageSpan = span;
						}
					}
				}
			}
		}

		return imageSpan;
	}

	@Override
	public void invalidateDrawable(Drawable drawable) {
		GifImageSpan imageSpan = getImageSpan(drawable);
		if (imageSpan != null) {
			CharSequence text = getText();
			if (!TextUtils.isEmpty(text)) {
				if (text instanceof Editable) {
					Editable editable = (Editable)text;
					int start = editable.getSpanStart(imageSpan);
					int end = editable.getSpanEnd(imageSpan);
					int flags = editable.getSpanFlags(imageSpan);

					editable.removeSpan(imageSpan);
					editable.setSpan(imageSpan, start, end, flags);
				}
			}
			
		} else {
			super.invalidateDrawable(drawable);
		}
	}

}


设置其android:editable="true"或者在如上直接在setText(CharSequence text, BufferType type)将type置为BufferType.EDITABLE。


本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。
从中午开始,花了两三个小时,现在下午四点,终于将个人账号升级为公司账号。本来以为一下午都搞不定呢,没想到这么顺利,不错不错。 1、电话了解,到底能不能转? 苹果客服(4006701855)的妹子说,能转,但需要是个人账号的所有者是联合创办人或公司法人。 本人不是公司法人,只好硬充联合创办人了,就那么骗她,感觉好心亏。 然后个人账号一封邮件,是苹果的客服妹子发来的。 2、回复邮件 邮件主要说需要回答几个问题: - 确认您符合上面列出的账户转换要求 - 提供贵公司的英文全称 - 若是贵公司已有 DUNS 号码
一、Context继承体系 与 Context是如何创建的 1. Context继承体系 只用记住一句:Activity 、 Service 与Application 都是继承自ContextWrapper,而ContextWrapper implements Context。每个:Activity 、 Service 与Application都是一个Context实例。 2. Context 何时创建、怎样创建的 - 查看源码 Android应用程序窗口(Activity)的运行上下文环境(Contex
1、大规模削减个税,刺激消费,消化产能过剩; 2、以阿里、腾讯开办银行为契机,打破央企行政垄断,让民间投资突破玻璃门、弹簧门; 3、强力推进改革,给民企投资、融资松绑; 4、加快金融市场化,消除利率双轨制,降低民间融资成本; 5、加快国企民营化,提升经济效率

cocos2dx之如何优化内存使用 - 2015-03-21 14:03:08

内存优化原则 为了优化应用内存,你应该知道是什么消耗了你应用的大部分内存,答案就是Texture(纹理)!它几乎占据了90%的应用内存。那么我们应该尽力去减小我们应用的纹理内存使用,否则我们的应用进程可能会被系统杀死。 为了减少内存警告,这里我们给出两个普遍的关于cocos2dx游戏内存优化的指导原则。1)了解瓶颈,然后解决掉 什么样的纹理消耗了大部分应用的内存呢?或者说这些纹理消耗了多少内存呢?你不用去手工计算或者猜测。 这里我们正好有一个工具。它就是苹果的开发工具- Allocations Leaks
随着移动互联网时代的到来,BAT公司加速在移动互联网的布局,特别在手机浏览器上,分别讲述自己公司产的内核有如此如此的快,性能如此的好。 在这里,将要讲诉用什么工具来调试开发BAT公司的这三款产品。 目前市场上,功能最强大,使用率最高的,肯定是属于 DebugGap 了。 首先,DebugGap是一款跨平台的开发工具,可以在Linux,Mac和Windows下运行,免安装,可以copy给同事或者朋友。 其次,可以完好的调试各个平台和深度定制化的webkit(比如U3,T5,X5等国产内核),还可以同时调试多
要点: 随着手指的滑动更新位置 drawText的时候,如何计算开始的位置,使str居中 1.CallSliderEndView.java package net.mobctrl.callendview; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import an
看过网上的很多对于frame  和 bounds的 区别的,大多都是千篇一律的! 大多数的说法是: frame  参考坐标系 是其父视图, bounds 的参考坐标系是其本身? 虽然大体上是对的,但几乎没有人能说的更具体更透彻一些. 根据我的研究: frame : 是指的子视图的左上角顶点在父视图中的坐标bounds: 是 其本身作为父视图时,子视图的左上角在该坐标系中的位置坐标!bounds的这个属性是实现 UIScrollView的基石,通过改变 view的bounds可以 模拟出类似的滚动的效果,不
前言 上一节,学会了Paint,Canvas的基本用法后,这一节,学习Paint的高级用法。还没看过上一节的请点击这里: Android_2D绘图的学习Paint,Canvas(一) 。 一,文字的绘制 在做UI的时候,常常会绘制文字,Canvas绘制文字时,主要考虑到字体的宽度和高度问题。字体的宽度比较好理解,这里我们主要考虑一下字体的高度。 先看一张图,网上搜的: 这里说明了在安卓中绘制字体时对于高度的划分:top,ascent,baseLine,descent,bottom.有点类似我们刚开始学英语

我的2014-转折中前行 - 2015-03-21 11:03:07

文章地址: http://ryantang.me/blog/2015/02/17/my-2014/
最近在用ListView+CheckBox搞一个item选中的项目,我将CheckBox的focus设置为false,另我大喜的是,CheckBox竟然可以选中(窃喜中),这么简单就搞定了,因为数据量较小,也没有发现什么问题。 后来数据多了, 页面需要滑动了, 发现了一个奇怪的问题,前面明明选中了,而再次滑动回去的时候竟然变成未选中状态! 这是我刚开始写的那段错误的代码: @Overridepublic View getView(int position, View convertView, ViewGr