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。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。