簡(jiǎn)單實(shí)用的Android UI微博動(dòng)態(tài)點(diǎn)贊效果
說(shuō)起空間動(dòng)態(tài)、微博的點(diǎn)贊效果,網(wǎng)上也是很泛濫,各種實(shí)現(xiàn)與效果一大堆。而詳細(xì)實(shí)現(xiàn)的部分,講述的也是參差不齊,另一方面估計(jì)也有很多大俠也不屑一顧,覺(jué)得完全沒(méi)必要單獨(dú)開(kāi)篇來(lái)寫(xiě)和講解吧。畢竟,也就是兩個(gè)view和一些簡(jiǎn)單的動(dòng)畫(huà)效果罷了。
單若是只講這些,我自然也是不愿花這番功夫的。雖然自己很菜,可也不甘于太菜。所以偶爾看到些好東西,可以延伸學(xué)寫(xiě)下,我還是很情愿拿出來(lái)用用,順帶秀一秀逼格什么的。
不扯太多,先說(shuō)說(shuō)今天實(shí)現(xiàn)點(diǎn)贊效果用到的自以為不錯(cuò)的兩個(gè)點(diǎn):
Checkable 用來(lái)擴(kuò)展View實(shí)現(xiàn)選中狀態(tài)切換
AndroidViewAnimations 基于nineoldandroids封裝的android動(dòng)畫(huà)簡(jiǎn)易類(lèi)庫(kù)。究竟有多簡(jiǎn)單呢,就像這樣
AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView);
作用: 在imageView上使用PulseAnimator這個(gè)動(dòng)畫(huà)效果,播放一秒。
當(dāng)然是從實(shí)現(xiàn)角度來(lái)看這個(gè)庫(kù)啦,如果僅僅是使用,google/百度一大堆啦。
結(jié)合前兩篇富文本折疊展開(kāi),加上我們的點(diǎn)贊view 做出的demo整合效果圖:
1.從實(shí)現(xiàn)看門(mén)道
其實(shí)從效果看無(wú)非就是點(diǎn)擊切換圖片,并添加一些簡(jiǎn)單動(dòng)畫(huà)效果而已,確實(shí)沒(méi)什么難度。這里是因?yàn)橐肓藘蓚€(gè)不錯(cuò)的新內(nèi)容,使用下,權(quán)當(dāng)新手嘗鮮。
1.1 Checkable接口實(shí)現(xiàn)CheckedImageView
系統(tǒng)本身提供了android.widget.Checkable這樣一個(gè)接口,方便我們繼承實(shí)現(xiàn)View的選中和取消的狀態(tài)。看下這個(gè)類(lèi):
public interface Checkable { /** * 設(shè)置view的選中狀態(tài) */ void setChecked(boolean checked); /** * 當(dāng)前view是否被選中 */ boolean isChecked(); /** *改變view的選中狀態(tài)到相反的狀態(tài) */ void toggle(); }
通常這個(gè)接口用來(lái)幫助我們快速實(shí)現(xiàn)view的可選效果,增加了選中和取消兩種狀態(tài)和切換方法。另外為了方便View在狀態(tài)改變時(shí)候快速地變看到效果(更背景或圖片),我們可以直接通過(guò)selector控制圖片,而其本身并不會(huì)自動(dòng)改變drawable狀態(tài),因此這里還有必要重寫(xiě)drawableStateChanged方法。我們先以定義一個(gè)通用的CheckedImageView為例:
public class CheckedImageView extends ImageView implements Checkable{ protected boolean isChecked;//選中狀態(tài) protected OnCheckedChangeListener onCheckedChangeListener;//狀態(tài)改變事件監(jiān)聽(tīng) public static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked }; public CheckedImageView(Context context) { super(context); initialize(); } public CheckedImageView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } private void initialize() { isChecked = false; } @Override public boolean isChecked() { return isChecked; } @Override public void setChecked(boolean isChecked) { if (this.isChecked != isChecked) { this.isChecked = isChecked; refreshDrawableState(); if (onCheckedChangeListener != null) { onCheckedChangeListener.onCheckedChanged(this, isChecked); } } } @Override public void toggle() {//改變狀態(tài) setChecked(!isChecked); } //初始DrawableState時(shí)候?yàn)樗砑右粋€(gè)CHECKED_STATE,ImageView本身是沒(méi)有這個(gè)狀態(tài)的 @Override public int[] onCreateDrawableState(int extraSpace) { int[] states = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(states, CHECKED_STATE_SET); } return states; } //當(dāng)view的選中狀態(tài)被改變的時(shí)候通知ImageView改變背景或內(nèi)容,這個(gè)view會(huì)自動(dòng)在drawable狀態(tài)集中選擇與當(dāng)前狀態(tài)匹配的圖片 @Override protected void drawableStateChanged() { super.drawableStateChanged(); Drawable drawable = getDrawable(); if (drawable != null) { int[] myDrawableState = getDrawableState(); drawable.setState(myDrawableState); invalidate(); } } //設(shè)置狀態(tài)改變監(jiān)聽(tīng)事件 public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckedChangeListener) { this.onCheckedChangeListener = onCheckedChangeListener; } //當(dāng)選中狀態(tài)改變時(shí)監(jiān)聽(tīng)接口觸發(fā)該事件 public static interface OnCheckedChangeListener { public void onCheckedChanged(CheckedImageView checkedImgeView, boolean isChecked); } }
這是一個(gè)通用的可被選中ImageView,當(dāng)點(diǎn)擊之后被選中,再次點(diǎn)擊則取消。而其背景/內(nèi)容也會(huì)隨之改變。比如下圖所示效果:
從代碼上看,我們本身并沒(méi)有直接定義當(dāng)view點(diǎn)擊之后,調(diào)用setImage()或者setBackground()來(lái)改變內(nèi)容,而是通過(guò)使用View本身的DrawableState來(lái)繪制和更改,查找與它對(duì)應(yīng)匹配的圖片,而這些狀態(tài)所對(duì)應(yīng)的圖片,都預(yù)先在selector中配置好。關(guān)于selector這里不做介紹,自行查閱學(xué)習(xí)。
既然提到selector,順帶提下之前遇到的坑,關(guān)于他的匹配原則。比如下邊這樣一個(gè)selector:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/icon_pressed"></item> <item android:state_checked="true" android:drawable="@drawable/icon_checked"></item> <item android:drawable="@drawable/icon_normal"></item> </selector>
當(dāng)view同時(shí)有上邊兩個(gè)狀態(tài)(如state_pressed和state_checked)的時(shí)候,view優(yōu)先顯示第一個(gè)狀態(tài)時(shí)候的圖片(icon_pressed)。這是因?yàn)樗怯缮系较掠行虿檎业?,?dāng)找到第一個(gè)狀態(tài)與他定義的所相符所在行時(shí),就優(yōu)先顯示這行的圖片。所以當(dāng)我們將最后一行
< item android:drawable=”@drawable/icon_normal”>< /item>
放在第一行時(shí),無(wú)論是否選中狀態(tài)或按下?tīng)顟B(tài),都顯示的是icon_normal。初學(xué)者一定要注意,我當(dāng)初就因?yàn)檫@個(gè)原因耗費(fèi)了很多時(shí)間查找緣由。
回到我們的點(diǎn)贊實(shí)現(xiàn)。這里實(shí)現(xiàn)的點(diǎn)贊View PraiseView 包含了一個(gè) CheckedImageView 和一個(gè) TextView ,點(diǎn)贊之后,ImageView會(huì)放大回縮并彈出一個(gè)TextView”+1”的動(dòng)畫(huà)效果。
public class PraiseView extends FrameLayout implements Checkable{//同樣繼承Checkable protected OnPraisCheckedListener praiseCheckedListener; protected CheckedImageView imageView; //點(diǎn)贊圖片 protected TextView textView; //+1 protected int padding; public PraiseView(Context context) { super(context); initalize(); } public PraiseView(Context context, AttributeSet attrs) { super(context, attrs); initalize(); } protected void initalize() { setClickable(true); imageView = new CheckedImageView(getContext()); imageView.setImageResource(R.drawable.blog_praise_selector); FrameLayout.LayoutParams flp = new LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,Gravity.CENTER); addView(imageView, flp); textView = new TextView(getContext()); textView.setTextSize(10); textView.setText("+1"); textView.setTextColor(Color.parseColor("#A24040")); textView.setGravity(Gravity.CENTER); addView(textView, flp); textView.setVisibility(View.GONE); } @Override public boolean performClick() { checkChange(); return super.performClick(); } @Override public void toggle() { checkChange(); } public void setChecked(boolean isCheacked) { imageView.setChecked(isCheacked); } public void checkChange() {//點(diǎn)擊切換狀態(tài) if (imageView.isChecked) { imageView.setChecked(false); } else { imageView.setChecked(true); textView.setVisibility(View.VISIBLE); //放大動(dòng)畫(huà) AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView); //飄 “+1”動(dòng)畫(huà) AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView); } //調(diào)用點(diǎn)贊事件 if (praiseCheckedListener != null) { praiseCheckedListener.onPraisChecked(imageView.isChecked); } } public boolean isChecked() { return imageView.isChecked; } public void setOnPraisCheckedListener(OnPraisCheckedListener praiseCheckedListener) { this.praiseCheckedListener = praiseCheckedListener; } public interface OnPraisCheckedListener{ void onPraisChecked(boolean isChecked); } }
過(guò)于自定的View大概就這么多了,Checkable這個(gè)小巧方便的類(lèi),不知道你會(huì)用了沒(méi)。至于上邊用到的兩個(gè)動(dòng)畫(huà)效果集:
AnimHelper.with(new PulseAnimator()).duration(1000).playOn(imageView);
AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView);
感覺(jué)封裝的挺簡(jiǎn)潔實(shí)用,所以很有必要學(xué)習(xí)分析一下。
1.2 動(dòng)畫(huà)庫(kù)的封裝和快速框架
提到動(dòng)畫(huà),Android本身自帶的動(dòng)畫(huà)類(lèi)Animation已經(jīng)做到支持3.0及以上了,雖然也做了很好的封裝,但是做起復(fù)雜動(dòng)畫(huà)來(lái)還是不夠像上邊那樣簡(jiǎn)潔。在關(guān)于動(dòng)畫(huà)兼容方面,github上的大牛Jake Wharton又做了一套動(dòng)畫(huà)開(kāi)源庫(kù)NineOldAndroids,效果很好而且支持3.0級(jí)以前的版本,確實(shí)很值得稱(chēng)贊。而在此基礎(chǔ)上,有很多高手又做了二次封裝,實(shí)現(xiàn)了復(fù)雜動(dòng)畫(huà),同時(shí)保證方便簡(jiǎn)潔,而且通用性和擴(kuò)展性更高。我們這里的動(dòng)畫(huà)使用的就是這樣一個(gè)簡(jiǎn)單的封裝。
比如,要在XXView上時(shí)用XXAnimator這樣的動(dòng)畫(huà),持續(xù)Duration秒。就這么一行代碼:
AnimHelper.with(new SlideOutUpAnimator()).duration(1000).playOn(textView);
來(lái)看一下基于NineOldAndroids的ViewAnimations具體實(shí)現(xiàn)。
1). 首先定義一個(gè)基本動(dòng)畫(huà)效果類(lèi)BaseViewAnimator
這個(gè)BaseViewAnimator動(dòng)畫(huà)類(lèi)使用一個(gè)動(dòng)畫(huà)集合AnimatorSet,包裝成單個(gè)動(dòng)畫(huà)類(lèi)似的用法,并定義了一個(gè)abstract方法prepare():
public abstract class BaseViewAnimator { public static final long DURATION = 1000; private AnimatorSet mAnimatorSet; private long mDuration = DURATION; { mAnimatorSet = new AnimatorSet(); } protected abstract void prepare(View target); public BaseViewAnimator setTarget(View target) { reset(target); prepare(target); return this; } public void animate() { start(); } /** * reset the view to default status * * @param target */ public void reset(View target) { ViewHelper.setAlpha(target, 1); ViewHelper.setScaleX(target, 1); ViewHelper.setScaleY(target, 1); ViewHelper.setTranslationX(target, 0); ViewHelper.setTranslationY(target, 0); ViewHelper.setRotation(target, 0); ViewHelper.setRotationY(target, 0); ViewHelper.setRotationX(target, 0); ViewHelper.setPivotX(target, target.getMeasuredWidth() / 2.0f); ViewHelper.setPivotY(target, target.getMeasuredHeight() / 2.0f); } /** * start to animate */ public void start() { mAnimatorSet.setDuration(mDuration); mAnimatorSet.start(); } public BaseViewAnimator setDuration(long duration) { mDuration = duration; return this; } public BaseViewAnimator setStartDelay(long delay) { getAnimatorAgent().setStartDelay(delay); return this; } public long getStartDelay() { return mAnimatorSet.getStartDelay(); } public BaseViewAnimator addAnimatorListener(AnimatorListener l) { mAnimatorSet.addListener(l); return this; } public void cancel(){ mAnimatorSet.cancel(); } public boolean isRunning(){ return mAnimatorSet.isRunning(); } public boolean isStarted(){ return mAnimatorSet.isStarted(); } public void removeAnimatorListener(AnimatorListener l) { mAnimatorSet.removeListener(l); } public void removeAllListener() { mAnimatorSet.removeAllListeners(); } public BaseViewAnimator setInterpolator(Interpolator interpolator) { mAnimatorSet.setInterpolator(interpolator); return this; } public long getDuration() { return mDuration; } public AnimatorSet getAnimatorAgent() { return mAnimatorSet; } }
復(fù)雜動(dòng)畫(huà)效果基類(lèi)BaseViewAnimator使用一個(gè)AnimatorSet集合來(lái)添加各種動(dòng)畫(huà) ,并綁定到目標(biāo)targetView ,使用 prepare(View target) 的abstract方法供其子類(lèi)實(shí)現(xiàn)具體的動(dòng)畫(huà)效果。
2). 其次基于這個(gè)類(lèi)實(shí)現(xiàn)我們的各種動(dòng)畫(huà)效果XXAnimator
當(dāng)我們要實(shí)現(xiàn)具體的動(dòng)畫(huà)效果時(shí),可以直接繼承這個(gè)類(lèi)并實(shí)現(xiàn)prepaer方法。比如這里定義的上劃消失SlideOutUpAnimator 和放大回縮動(dòng)畫(huà)PulseAnimator
/** *上劃消失(飄+1) */ public class SlideOutUpAnimator extends BaseViewAnimator { @Override public void prepare(View target) { ViewGroup parent = (ViewGroup)target.getParent(); getAnimatorAgent().playTogether( ObjectAnimator.ofFloat(target, "alpha", 1, 0), ObjectAnimator.ofFloat(target,"translationY",0,-parent.getHeight()/2) ); } } /** *放大效果 */ public class PulseAnimator extends BaseViewAnimator { @Override public void prepare(View target) { getAnimatorAgent().playTogether( ObjectAnimator.ofFloat(target, "scaleY", 1, 1.2f, 1), ObjectAnimator.ofFloat(target, "scaleX", 1, 1.2f, 1) ); } }
上邊兩種動(dòng)畫(huà)效果就是對(duì)BaseViewAnimator的兩種實(shí)現(xiàn),動(dòng)畫(huà)本身使用的庫(kù)是NineOldAndroids。
3). 最后封裝一個(gè)動(dòng)畫(huà)管理工具類(lèi)AnimHelper供外部使用
首先定義了一個(gè)靜態(tài)類(lèi),使用helper來(lái)實(shí)例化這個(gè)靜態(tài)類(lèi),并設(shè)置各個(gè)參數(shù)選項(xiàng)。
public class AnimHelper { private static final long DURATION = BaseViewAnimator.DURATION; private static final long NO_DELAY = 0; /** *實(shí)例化得到AnimationComposer的唯一接口 */ public static AnimationComposer with(BaseViewAnimator animator) { return new AnimationComposer(animator); } /** *定義與動(dòng)畫(huà)效果相關(guān)聯(lián)的各種參數(shù), *使用這種方法可以保證對(duì)象的構(gòu)建和他的表示相互隔離開(kāi)來(lái) */ public static final class AnimationComposer { private List<Animator.AnimatorListener> callbacks = new ArrayList<Animator.AnimatorListener>(); private BaseViewAnimator animator; private long duration = DURATION; private long delay = NO_DELAY; private Interpolator interpolator; private View target; private AnimationComposer(BaseViewAnimator animator) { this.animator = animator; } public AnimationComposer duration(long duration) { this.duration = duration; return this; } public AnimationComposer delay(long delay) { this.delay = delay; return this; } public AnimationComposer interpolate(Interpolator interpolator) { this.interpolator = interpolator; return this; } public AnimationComposer withListener(Animator.AnimatorListener listener) { callbacks.add(listener); return this; } public AnimManager playOn(View target) { this.target = target; return new AnimManager(play(), this.target); } private BaseViewAnimator play() { animator.setTarget(target); animator.setDuration(duration) .setInterpolator(interpolator) .setStartDelay(delay); if (callbacks.size() > 0) { for (Animator.AnimatorListener callback : callbacks) { animator.addAnimatorListener(callback); } } animator.animate(); return animator; } } /** *動(dòng)畫(huà)管理類(lèi) */ public static final class AnimManager{ private BaseViewAnimator animator; private View target; private AnimManager(BaseViewAnimator animator, View target){ this.target = target; this.animator = animator; } public boolean isStarted(){ return animator.isStarted(); } public boolean isRunning(){ return animator.isRunning(); } public void stop(boolean reset){ animator.cancel(); if(reset) animator.reset(target); } } }
這段代碼使用了類(lèi)似Dialog的builder模式,感興趣的可以搜一下 JAVA設(shè)計(jì)模式-Builder.晚點(diǎn)會(huì)另開(kāi)一篇講解。
(注: 復(fù)雜動(dòng)畫(huà)這一部分的內(nèi)容這里只是拿出來(lái)展示和使用,包裝和實(shí)現(xiàn)是由代碼家大大原創(chuàng),有想了解更多動(dòng)畫(huà)及效果的請(qǐng)點(diǎn)其名字鏈接)
運(yùn)行一下,就可以看到前面所演示的效果了。點(diǎn)擊第一下,伴隨著圖標(biāo)變大一下并飄出“+1”的效果,圖片切換到選中狀態(tài);再點(diǎn)則恢復(fù)未選中,而且不會(huì)觸發(fā)動(dòng)畫(huà)。
至此,點(diǎn)贊這塊內(nèi)容和關(guān)注點(diǎn)也說(shuō)完了,希望各位能有點(diǎn)兒收獲,另外便于自己也能加深理解。
最后,附上示例源碼地址:Android UI微博動(dòng)態(tài)點(diǎn)贊
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android高級(jí)UI特效仿直播點(diǎn)贊動(dòng)畫(huà)效果
- android實(shí)現(xiàn)直播點(diǎn)贊飄心動(dòng)畫(huà)效果
- Android控件實(shí)現(xiàn)直播App點(diǎn)贊飄心動(dòng)畫(huà)
- Android仿直播特效之點(diǎn)贊飄心效果
- Android貝塞爾曲線(xiàn)實(shí)現(xiàn)直播點(diǎn)贊效果
- Android 仿微信朋友圈點(diǎn)贊和評(píng)論彈出框功能
- Android自定義view實(shí)現(xiàn)仿抖音點(diǎn)贊效果
- Android仿微信朋友圈點(diǎn)贊和評(píng)論功能
- Android自定義View實(shí)現(xiàn)點(diǎn)贊控件
- Android自定義View實(shí)現(xiàn)直播點(diǎn)贊特效
相關(guān)文章
使用Chrome瀏覽器調(diào)試Android App詳解
這篇文章主要介紹了使用Chrome瀏覽器調(diào)試Android App詳解,本網(wǎng)講解了使用Facebook開(kāi)源Stetho實(shí)現(xiàn)在Chrome中調(diào)試Android App中,需要的朋友可以參考下2015-05-05Android Drawerlayout側(cè)拉欄事件傳遞問(wèn)題的解決方法
這篇文章主要為大家詳細(xì)介紹了Android Drawerlayout側(cè)拉欄事件傳遞問(wèn)題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android ADT和SDK Manager無(wú)法更新下載解決方案
這篇文章主要介紹了Android ADT和SDK Manager無(wú)法更新下載解決方案的相關(guān)資料,需要的朋友可以參考下2017-04-04Android開(kāi)發(fā)OkHttp執(zhí)行流程源碼分析
這篇文章主要為大家介紹了Android開(kāi)發(fā)OkHttp執(zhí)行流程源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09Android?Studio使用自定義對(duì)話(huà)框效果
這篇文章主要為大家詳細(xì)介紹了Android?Studio使用自定義對(duì)話(huà)框效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05Android 7.0開(kāi)發(fā)獲取存儲(chǔ)設(shè)備信息的方法
這篇文章主要介紹了Android 7.0開(kāi)發(fā)獲取存儲(chǔ)設(shè)備信息的方法,結(jié)合實(shí)例形式分析了Android7.0針對(duì)存儲(chǔ)設(shè)備信息的獲取、判斷操作方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-11-11Android獲得內(nèi)/外置存儲(chǔ)卡路徑的方法
我們知道Android上一般都有外置的存儲(chǔ)卡,內(nèi)置存儲(chǔ)卡路徑大家都知道怎么獲得的。那么如何獲取外置存儲(chǔ)卡的位置呢?下面小編通過(guò)本文給大家分享下2017-01-01在android開(kāi)發(fā)中盡量不要使用中文路徑的問(wèn)題詳解
本篇文章對(duì)在android開(kāi)發(fā)中盡量不要使用中文路徑的問(wèn)題進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05