自定義控制元件 | 仿《最美有物》點贊效果
前言
最近在跟著Hencode學習自定義控制元件,一直想著自己能夠照著別人寫的demo寫一個好看的View,就科學上網看看了別人的一些demo,看到了一位大神寫的模仿《最美有物》的點贊效果,覺得勝似喜歡,於是也跟著寫了一個demo。主要是看著流程分析自己擼出來的,之前用了自己的寫法,然後看到了大神demo有更好的解決方案,後面也改過來了,所以在程式碼實現方式都差不多。
效果圖
動畫分析
1.向上拉伸是一個屬性動畫;
2.笑臉(不開心臉)動畫是一個幀動畫和屬性動畫組合;
3.背景回收也是一個屬性動畫。
(對動畫不熟的需要可以去看看郭神的部落格)
實現分析
點選觸發 :
1.顏色切換(選中黃,未選中白)
2.按比例拉伸(兩個笑臉同時升高,顯示點贊比例的高度)
3.展示資料文字(百分比資料顯示)
4.顯示選中部分的笑臉動畫(這一步與3文字資料同時進行,在拉伸至最高處停留後)
5.縮回至原始大小,保持選中狀態
通過這個流程的分析,我們可以把主要的功能點劃分為以下兩類:
1.動畫類 : 臉部動畫,拉伸動畫
2.控制類: 顏色切換,資料顯示,拉伸比例
在自定義View的過程中,動畫特效是最吸引人,也是最複雜的部分,雖然現在有高效炫酷的向量動畫庫供我們選擇, 但是基礎動畫的組合也是相當有用的,重點是發揮想象力。
主要的難點就在於如何進行拉伸操作。對於將一個圓形從中間拉伸成長條…
最開始想到的方案是拼接圖形,即通過圓形 + 矩形 + 圓形的方式疊加這個控制元件。通過調節中間矩形的高度,來控制拉伸操作。但是這種方式結構略複雜,需要在一小塊地方擺上三個圖形,還要帶上最外層的笑臉動畫,還沒寫程式碼就感覺應該是效能低下的方案,另外一個致命的問題就是,不能有描邊!大家可以參考餓了麼的實現效果,在有描邊的情況下,形狀拼接的方案明顯不可行。遂放棄。
而最終採用的則是使用圓角矩形作為外層Layout背景,通過控制內部笑臉的marginBottom,來動態的調節Layout的高度。這樣,即可以保持笑臉始終處於控制元件的上部,同時也能控制相對簡單的結構。
拉伸操作的實現
首先我們先簡單的模擬下通過marginBottom控制拉伸的效果。
在佈局裡設定一個LinearLayout ,裡面只有一個ImageView。用一個seekBar來模擬效果。
Layout:
<LinearLayout
android:id="@+id/backGround"
android:layout_width ="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:background="@drawable/yellow_background"
android:layout_above="@+id/sb_smile"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/iv_smile_face"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/like_1"/>
</LinearLayout>
<SeekBar
android:id="@+id/sb_smile"
android:layout_width="match_parent"
android:layout_height="50dp" />
這裡Linearlayout中設定了一個背景,是自定義的圓角矩形shape,通過調大圓角,使其顯示為一個正圓。
Activity:
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) imageView.getLayoutParams();
layoutParams.bottomMargin=progress*3;
Log.i(">>>>>",progress+"");
imageView.setLayoutParams(layoutParams);
}
通過獲取SmileFace的LayoutParams,通過Seekbar設定其下邊距bottomMargin,來控制高度。
效果如下所示:
這樣拉伸的原理就很清楚了。
我們需要在自定義控制元件中完成上述操作,並用屬性動畫替換掉seekBar。
自定義控制元件的封裝
1.子控制元件的佈局
考慮到實現目標裡有兩個並排的笑臉控制元件,這裡採用繼承LinearLayout的方式,可以把兩個控制元件及中間的分割線直接擺放進去。
首先是一個水平排列的線性佈局,裡面又包含了三個子佈局,點贊線性總佈局,中間豎線,差評線性總佈局,點贊線性總佈局是一個垂直排列的的佈局,包含了兩個TextView和一個線性佈局的點贊。
我們先將初始化這些佈局和控制元件。
程式碼如下:
//初始化文字--點贊
likeText=new TextView(getContext());
likeText.setText("喜歡");
likeText.setTextSize(18);
likeText.setTextColor(Color.WHITE);
likeNumText=new TextView(getContext());
likeNumText.setTextSize(14);
likeNumText.setText(likeNum+"%");
likeNumText.setTextColor(Color.WHITE);
//初始化文字--無感
disLikeText=new TextView(getContext());
disLikeText.setText("無感");
disLikeText.setTextSize(18);
disLikeText.setTextColor(Color.WHITE);
disLikeNumText=new TextView(getContext());
disLikeNumText.setTextSize(14);
disLikeNumText.setText(disLikeNum+"%");
disLikeNumText.setTextColor(Color.WHITE);
//差評線性佈局
disLikeImageView=new ImageView(getContext());
disLikeImageView.setImageResource(R.drawable.dislike_1);
disLikeBack=new LinearLayout(getContext());
LayoutParams disLikeBackLP=new LayoutParams(defalutSize, defalutSize);
//disLikeBackLP.gravity=Gravity.BOTTOM;
disLikeBack.addView(disLikeImageView,disLikeBackLP);
disLikeBack.setBackgroundResource(R.drawable.white_background);
disLikeBack.setOrientation(VERTICAL);
//點贊線性佈局
likeImageView=new ImageView(getContext());
likeImageView.setImageResource(R.drawable.like_1);
LayoutParams likeBackLP=new LayoutParams(defalutSize, defalutSize);
//likeBackLP.gravity=Gravity.BOTTOM;
likeBack=new LinearLayout(getContext());
likeBack.addView(likeImageView,likeBackLP);
likeBack.setBackgroundResource(R.drawable.white_background);
likeBack.setOrientation(VERTICAL);
//點贊總佈局
LayoutParams textLP=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
textLP.gravity=Gravity.CENTER_HORIZONTAL;
LayoutParams backLP = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
backLP.gravity = Gravity.CENTER;
likeAll =new LinearLayout(getContext());
likeAll.setOrientation(VERTICAL);
likeAll.addView(likeNumText,textLP);
likeAll.addView(likeText,textLP);
likeAll.addView(likeBack,backLP);
//差評總佈局
disLikeAll=new LinearLayout(getContext());
disLikeAll.setOrientation(VERTICAL);
disLikeAll.addView(disLikeNumText,textLP);
disLikeAll.addView(disLikeText,textLP);
disLikeAll.addView(disLikeBack,backLP);
//中間豎線
ImageView centerLineImageView=new ImageView(getContext());
centerLineImageView.setBackground(new ColorDrawable(Color.GRAY));
LayoutParams centerLineLP=new LayoutParams(3,160);
centerLineLP.setMargins(40, 10, 40, 50);
centerLineLP.gravity= Gravity.BOTTOM;
//初始化總佈局
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.bottomMargin=50;
lp.gravity=Gravity.BOTTOM;
addView(disLikeAll,lp);
addView(centerLineImageView,centerLineLP);
addView(likeAll,lp)
2.背景拉伸動畫
我們點選點贊線性佈局時,佈局背景變成黃色,同時背景開始拉伸,之前用seekbar模擬了背景拉伸動畫,我們這邊直接應用過來。
因為這邊是同時兩個一起拉伸,之前準備建立兩個ValueAnimator物件,分別控制動畫,但是別人用一個物件就能控制,具體實現看程式碼,應該很好理解。
程式碼如下:
/**
*背景拉伸
*/
private void scaleAnim() {
float disLikePercent=disLikeNum/(disLikeNum+likeNum);
float likePercent=(likeNum/(likeNum+disLikeNum));
//計算點贊百分比*300
final int likeMax=Math.round(likePercent*300);
//計算差評百分比*300
final int disLikeMax=Math.round(disLikePercent*300);
int max=Math.max(likeMax,disLikeMax);
/**
* 本來想建立兩個ValueAnimator物件,不過這種方法建立一個物件一樣可以達到目的
*/
animationBack=ValueAnimator.ofInt(0,max);
animationBack.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int margin= (int) animation.getAnimatedValue();
LayoutParams layoutParams= (LayoutParams) likeImageView.getLayoutParams();
layoutParams.bottomMargin=margin;
if (margin<=likeMax)
{
likeImageView.setLayoutParams(layoutParams);
}
if (margin<=disLikeMax)
{
disLikeImageView.setLayoutParams(layoutParams);
}
invalidate();
}
});
likeBack.setClickable(false);
disLikeBack.setClickable(false);
isClose=false;
animationBack.setDuration(1000);
animationBack.start();
animationBack.addListener(this);
}
3.笑臉動畫
笑臉動畫是一個幀動畫和屬性動畫一起組成的。
幀動畫不解釋,比較簡單。屬性動畫是通過設定控制元件的translationX或者translationY屬性從而讓他左右或者上下移動。
程式碼如下:
/**
* 點贊補充屬性動畫
* @param likeImageView
*/
private void translationY(ImageView likeImageView) {
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(likeImageView,"translationY",-5.0f, 0.0f, 5.0f, 0.0f, -5.0f, 0.0f, 5.0f, 0);
objectAnimator.setRepeatMode(ValueAnimator.RESTART);
objectAnimator.setDuration(1500);
objectAnimator.start();
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setBackUpAnimation();
}
});
}
4.背景回收動畫
與拉伸類似,用的屬性動畫:
程式碼如下:
/**
* 背景收回動畫
*/
private void setBackUpAnimation() {
float disLikePercent=disLikeNum/(disLikeNum+likeNum);
float likePercent=(likeNum/(likeNum+disLikeNum));
//計算點贊百分比*300
final int likeMax=Math.round(likePercent*300);
//計算差評百分比*300
final int disLikeMax=Math.round(disLikePercent*300);
int max=Math.max(likeMax,disLikeMax);
animationBack=ValueAnimator.ofInt(max,0);
animationBack.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int margin = (int) animation.getAnimatedValue();
LayoutParams lp= (LayoutParams) disLikeImageView.getLayoutParams();
lp.bottomMargin=margin;
if (margin<likeMax)
{
likeImageView.setLayoutParams(lp);
}
if (margin<disLikeMax)
{
disLikeImageView.setLayoutParams(lp);
}
Log.i(">>>>>>>","margin"+margin);
invalidate();
}
});
animationBack.addListener(this);
animationBack.setDuration(500);
animationBack.start();
}
這樣就完成了自定義控制元件的封裝了,最後在佈局檔案引用這個控制元件就行。
傳送門
最後附上demo地址。喜歡的可以Star OR Fork。