TextSwitcher 實現 android 公告欄
網上搜索看到自定義的公告欄,採用ViewFlipper實現。之所以採用ViewFlipper,是因為它可以實現:子類有規律的間隔“跳動”。而ViewFlipper和ViewSwitcher都是ViewAnimator的子類。不同的是,ViewSwitcher只能有兩個子view,而ViewFlipper可以有多個子view.
考慮到,公告欄是一些文字在切換,所以採用ViewSwitcher包含兩個textview實現。而恰好,ViewSwitcher有一個子類TextSwitcher。看看TextSwitcher的介紹:
-
A TextSwitcher is useful to animate a label on screen.Whenever setText(CharSequence) is called, TextSwitcher animates the current text out and animates the new text in.
TextSwitcher 與文字型別的公告欄,簡直是絕配。
TextSwitcher ----繼承自---->ViewSwitcher----繼承自----->ViewAnimator
以下是具體實現過程
1.自定義NotiveView,繼承自TextSwitcher ,實現相應構造方法。
2.我們要給NotiveView新增兩個Textview,一個是公告欄進入 時的TextView,一個是公告欄退出 時的Textview.
ViewSwitcher早已為我們鋪墊好了,ViewSwitcher中有一個ViewFactory的介面,負責為ViewSwitcher建立子view.
看一下ViewSwitcher的原始碼:
public interface ViewFactory { View makeView(); } public void setFactory(ViewFactory factory) { mFactory = factory; obtainView(); obtainView(); } private View obtainView() { View child = mFactory.makeView(); LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp == null) { lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } addView(child, lp); return child; }
這也表明,我們在自定義的view中實現ViewFactory 這個介面,在 makeView()方法中返回一個TextView,每呼叫一次obtainView()就會 執行addView(child, lp),將view新增到viewgroup中。也就是說我們在自定義的view中只要呼叫setFactory(ViewFactory factory)就好了。
private void init() { setFactory(this); } @Override public View makeView() { TextView t = new TextView(context); t.setGravity(Gravity.CENTER); t.setTextColor(Color.parseColor("#333333")); t.setMaxLines(1); float textSize=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,context.getResources().getDisplayMetrics()); t.setTextSize(textSize); t.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,240)); return t; }
現在只是沒有進入和退出的動畫,我們可以先在activity中呼叫,看看效果:
在此之前,再提一下TextSwitcher的setText(CharSequence text)方法。
-
Sets the text of the next view and switches to the next view. This can be used to animate the old text out
and animate the next text in.
只要我們呼叫此方法,正在show的TextView就會out,而後面的Textview就會in,並且設定傳遞的text.
而要實現隔幾秒變換一次公告欄內容,我們就需要開啟一個執行緒,在子執行緒中使用handler重複呼叫。
由於重複呼叫,採用取餘的辦法,來確定當前顯示的是哪個String.
程式碼如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); stringList=getStringList(); mNotiveView=findViewById(R.id.auto_view); mNotiveView.setFocusableInTouchMode(true); handler.postDelayed(runnable,2000); mNotiveView.setOnClickListener(v -> Log.e(TAG, "onClick: "+stringList.get(mCount%stringList.size()) )); } 模擬公告欄資料 private List<String> getStringList() { List<String> list=new ArrayList<>(); list.add("hello"); list.add("world"); list.add("i"); list.add("miss"); list.add("you"); list.add("!"); return list; } private Handler handler=new Handler(); private int mCount=0; public Runnable runnable=new Runnable() { @Override public void run() { handler.postDelayed(runnable,3000); if (stringList.size()==0){return;} myAutoView.setText(stringList.get(mCount%stringList.size())); mCount++; } };
注意在onDestroy中把runnable回收掉
@Override protected void onDestroy() { super.onDestroy(); if (runnable!=null){ handler.removeCallbacks(runnable); } }
現在執行後,可以看到公告欄文字每隔3秒切換一次,但並沒有動畫效果,顯得不美,下面在自定義的view中設定進入和退出的動畫。
一切都是那麼巧合,ViewAnimator,也就是textSwitcher的父類的父類,已經定義好了兩個方法:
//Specifies the animation used to animate a View that enters the screen. public void setInAnimation(Animation inAnimation) { mInAnimation = inAnimation; } //Specifies the animation used to animate a View that exit the screen. public void setOutAnimation(Animation outAnimation) { mOutAnimation = outAnimation; }
美中不足的是,他們都需要一個Animation ,而不是Animator.屬性動畫就不能用了,只能設定Tween(補間動畫)。
下面是定義的一個動畫
class Rotate3dAnimation extends Animation { private final float mFromDegrees; private final float mToDegrees; private float mCenterX; private float mCenterY; private final boolean mTurnIn; private final boolean mTurnUp; private Camera mCamera; public Rotate3dAnimation(float fromDegrees, float toDegrees, boolean turnIn, boolean turnUp) { mFromDegrees = fromDegrees; mToDegrees = toDegrees; mTurnIn = turnIn; mTurnUp = turnUp; } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); mCamera = new Camera(); mCenterY = getHeight(); mCenterX = getWidth() / 2; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { Log.e(TAG, "applyTransformation: interpolatedTime"+interpolatedTime ); final float fromDegrees = mFromDegrees; float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime); final float centerX = mCenterX ; final float centerY = mCenterY ; final Camera camera = mCamera; final int derection = mTurnUp ? 1: -1; final Matrix matrix = t.getMatrix(); camera.save(); if (mTurnIn) { camera.translate(0.0f, derection *mCenterY * (interpolatedTime - 1.0f), 0.0f); } else { camera.translate(0.0f, derection *mCenterY * (interpolatedTime), 0.0f); } camera.rotateX(degrees); camera.getMatrix(matrix); camera.restore(); matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY); } }
在init方法中設定動畫
private void init() { setFactory(this); //進入和出去時的動畫 AnimationmInUp = createAnim(0, 0, true, true); AnimationmOutUp = createAnim(0, 0, false, true); setInAnimation(mInUp); setOutAnimation(mOutUp); } private Animation createAnim(float start, float end, boolean turnIn, boolean turnUp) { //平移動畫 final Animation rotation = new Rotate3dAnimation(start, end, turnIn, turnUp); rotation.setDuration(1000); rotation.setFillAfter(true); rotation.setInterpolator(new OvershootInterpolator()); return rotation; }
我也在問自己,當在自定義view中設定動畫後,並沒有指定哪個TextView啟動執行動畫,那動畫是怎麼執行的呢?
結果在ViewAnimator中。
理一下思路:
當在activity中findviewbyId的時候,自定義的view已經初始化,init方法便會呼叫,setFactory(ViewFactory factory)執行之後,我們便在自定義的view裡面添加了兩個TextView.之後設定了進入退出的動畫。
當activity開啟的子執行緒執行後,自定義的view呼叫了setText(CharSequence text)方法
public void setText(CharSequence text) { final TextView t = (TextView) getNextView(); t.setText(text); showNext();------------------------>1 }
showNext()方法很關鍵。
@android.view.RemotableViewMethod public void showNext() { setDisplayedChild(mWhichChild + 1);-------->2 }
繼續看setDisplayedChild(mWhichChild + 1)方法。
@android.view.RemotableViewMethod public void setDisplayedChild(int whichChild) { mWhichChild = whichChild; if (whichChild >= getChildCount()) { mWhichChild = 0; } else if (whichChild < 0) { mWhichChild = getChildCount() - 1; } boolean hasFocus = getFocusedChild() != null; // This will clear old focus if we had it showOnly(mWhichChild);--------------------》3 if (hasFocus) { // Try to retake focus if we had it requestFocus(FOCUS_FORWARD); } }
我大膽猜測一下,whichChild 是int型別的,作用是判斷目前的子類是哪個,而hasFocus 是判斷焦點,那麼showOnly(mWhichChild),就是接近真相的地方了。
void showOnly(int childIndex) { final boolean animate = (!mFirstTime || mAnimateFirstTime); showOnly(childIndex, animate); }
接著看 showOnly(childIndex, animate);
void showOnly(int childIndex, boolean animate) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (i == childIndex) { if (animate && mInAnimation != null) { child.startAnimation(mInAnimation); } child.setVisibility(View.VISIBLE); mFirstTime = false; } else { if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) { child.startAnimation(mOutAnimation); } else if (child.getAnimation() == mInAnimation) child.clearAnimation(); child.setVisibility(View.GONE); } } }
終於在這裡,我看到了我想看到的東西。child.startAnimation(mInAnimation);這就是啟動動畫的地方。
總結:雖然盡力想弄明白一些東西,但又豈是一朝一夕。還好TextSwitcher 原始碼簡單,能看的下去。最後我在想,原始碼為什麼這樣封裝繼承,拐回頭又看了一遍ViewAnimator----這個上游父類的註釋:
- Base class for aFrameLayout container that will perform animations when switching between its views.