標籤:
最近呢,本人辭職了,在找工作期間,不幸碰到了這個求職淡季,另外還是大學生畢業求職的高峰期,簡曆發了無數份卻都石沉大海,寶寶心裡那是一個苦啊!翻著過去的代碼,本人偶然找到了一個有意思的控制項,那時本人還沒有寫部落格的習慣,現在補上,先看:
然後看用法代碼:
StellarMap stellarMap = (StellarMap) findViewById(R.id.stellar);// 設定資料RecommendAdapter adapter = new RecommendAdapter();stellarMap.setAdapter(adapter);// 首頁選中stellarMap.setGroup(0, true);// 拆分螢幕stellarMap.setRegularity(15, 20);
class RecommendAdapter implements Adapter {/** 預設組數 */public static final intPAGESIZE= 15;@Overridepublic int getGroupCount() {// 資料分組int groupCount = data.size() / PAGESIZE;// 最後一組if (data.size() % PAGESIZE != 0) {return groupCount + 1;}return groupCount;}@Overridepublic int getCount(int group) {// 最後一組if (data.size() % PAGESIZE != 0) {if (group == getGroupCount() - 1) {return data.size() % PAGESIZE;}}return PAGESIZE;}@Overridepublic View getView(int group, int position, View convertView) {TextView tv = new TextView(MainActivity.this);int index = group * PAGESIZE + position;tv.setText(data.get(index));// 隨機大小Random random = new Random();// 14-17int size = random.nextInt(4) + 14;tv.setTextSize(size);// 隨機顏色int alpha = 255;int red = random.nextInt(190) + 30;int green = random.nextInt(190) + 30;int blue = random.nextInt(190) + 30;int argb = Color.argb(alpha, red, green, blue);tv.setTextColor(argb);return tv;}@Overridepublic int getNextGroupOnPan(int group, float degree) {if(group == getGroupCount() - 1){group = -1;}return group + 1;}@Overridepublic int getNextGroupOnZoom(int group, boolean isZoomIn) {if(group == getGroupCount() - 1){group = -1;}return group + 1;}}
代碼都很簡單,我簡單說一下,getGroupCount返回一共有多少組,getCount返回一組有多少個元素,getView就不說了,getNextGroupOnPan返回下一個需要放大動畫的組數,getNextGroupOnZoom返回下一個需要錯小動畫的組數。
接下來才是正餐,我們看看StellarMap的實現,StellarMap繼承於FrameLayout:
/** 構造方法 */public StellarMap(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init();}public StellarMap(Context context, AttributeSet attrs) {super(context, attrs);init();}public StellarMap(Context context) {super(context);init();}
這個大家應該都很熟,自訂View需要實現的三個構造方法。
/** 初始化方法 */private void init() {mGroupCount = 0;mHidenGroupIndex = -1;mShownGroupIndex = -1;mHidenGroup = new RandomLayout(getContext());mShownGroup = new RandomLayout(getContext());addView(mHidenGroup, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));mHidenGroup.setVisibility(View.GONE);addView(mShownGroup, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));mGestureDetector = new GestureDetector(this);setOnTouchListener(this);// 設定動畫mZoomInNearAnim = AnimationUtil.createZoomInNearAnim();mZoomInNearAnim.setAnimationListener(this);mZoomInAwayAnim = AnimationUtil.createZoomInAwayAnim();mZoomInAwayAnim.setAnimationListener(this);mZoomOutNearAnim = AnimationUtil.createZoomOutNearAnim();mZoomOutNearAnim.setAnimationListener(this);mZoomOutAwayAnim = AnimationUtil.createZoomOutAwayAnim();mZoomOutAwayAnim.setAnimationListener(this);}
代碼很清晰,簡單說一下,mGroupCount是組數,mHidenGroupIndex是隱藏的組數角標,mShownGroupIndex是顯示的組數角標,另外建立了兩個RandomLayout,它繼承於ViewGroup,用於實現View的隨機放入,之後建立手勢監聽和觸摸監聽,下面就是四個不同的動畫。
按照代碼執行順序來,下一步是設定Adapter:
/** 設定本Adapter */public void setAdapter(Adapter adapter) {mAdapter = adapter;mGroupCount = mAdapter.getGroupCount();if (mGroupCount > 0) {mShownGroupIndex = 0;}setChildAdapter();}
可見這裡初始化了組數,並調用了setChildAdapter方法:
/** 為子Group設定Adapter */private void setChildAdapter() {if (null == mAdapter) {return;}mHidenGroupAdapter = new RandomLayout.Adapter() {// 取出本Adapter的View對象給HidenGroup的Adapter@Overridepublic View getView(int position, View convertView) {return mAdapter.getView(mHidenGroupIndex, position, convertView);}@Overridepublic int getCount() {return mAdapter.getCount(mHidenGroupIndex);}};mHidenGroup.setAdapter(mHidenGroupAdapter);mShownGroupAdapter = new RandomLayout.Adapter() {// 取出本Adapter的View對象給ShownGroup的Adapter@Overridepublic View getView(int position, View convertView) {return mAdapter.getView(mShownGroupIndex, position, convertView);}@Overridepublic int getCount() {return mAdapter.getCount(mShownGroupIndex);}};mShownGroup.setAdapter(mShownGroupAdapter);}
該方法為子視圖建立Adapter,也就是RandomLayout,我們看看它的實現:
/** 構造方法 */public RandomLayout(Context context) {super(context);init();}
/** 初始化方法 */private void init() {mLayouted = false;mRdm = new Random();setRegularity(1, 1);mFixedViews = new HashSet<View>();mRecycledViews = new LinkedList<View>();}
在init方法中,mLayouted表示該視圖是否已經onlayout,mFixedViews存放已經確定位置的View ,mRecycledViews記錄被回收的View,以便重複利用,setRegularity(1, 1)方法僅僅只是初始化,會被重新調用,我們後面講,setAdapter方法就相當簡單了:
/** 設定資料來源 */public void setAdapter(Adapter adapter) {this.mAdapter = adapter;}
再回到使用代碼上,下一句是stellarMap.setGroup(0, true),我們看看實現:
/** 給指定的Group設定動畫 */public void setGroup(int groupIndex, boolean playAnimation) {switchGroup(groupIndex, playAnimation, mZoomInNearAnim, mZoomInAwayAnim);}
/** 給下一個Group設定進齣動畫 */private void switchGroup(int newGroupIndex, boolean playAnimation, Animation inAnim,Animation outAnim) {if (newGroupIndex < 0 || newGroupIndex >= mGroupCount) {return;}// 把當前顯示Group角標設定為隱藏的mHidenGroupIndex = mShownGroupIndex;// 把下一個Group角標設定為顯示的mShownGroupIndex = newGroupIndex;// 交換兩個GroupRandomLayout temp = mShownGroup;mShownGroup = mHidenGroup;mShownGroup.setAdapter(mShownGroupAdapter);mHidenGroup = temp;mHidenGroup.setAdapter(mHidenGroupAdapter);// 重新整理顯示的GroupmShownGroup.refresh();// 顯示GroupmShownGroup.setVisibility(View.VISIBLE);// 啟動動畫if (playAnimation) {if (mShownGroup.hasLayouted()) {mShownGroup.startAnimation(inAnim);}mHidenGroup.startAnimation(outAnim);} else {mHidenGroup.setVisibility(View.GONE);}}
switchGroup方法正是StellarMap的核心方法,通過交換show和hide的角標與adapter,完成顯示和隱藏的切換,並開啟過度動畫。
最後一行代碼,stellarMap.setRegularity(15, 20)方法:
/** 設定隱藏組和顯示組的x和y的規則 */public void setRegularity(int xRegularity, int yRegularity) {mHidenGroup.setRegularity(xRegularity, yRegularity);mShownGroup.setRegularity(xRegularity, yRegularity);}
用於設定螢幕的分割,再看RandomLayout的setRegularity方法:
/** 設定mXRegularity和mXRegularity,確定地區的個數 */public void setRegularity(int xRegularity, int yRegularity) {if (xRegularity > 1) {this.mXRegularity = xRegularity;} else {this.mXRegularity = 1;}if (yRegularity > 1) {this.mYRegularity = yRegularity;} else {this.mYRegularity = 1;}this.mAreaCount = mXRegularity * mYRegularity;// 個數等於x方向的個數*y方向的個數this.mAreaDensity = new int[mYRegularity][mXRegularity];// 存放地區的二維數組}
這裡儲存了螢幕被分割的快數,並建立了一個二維數組,定位具體的位置,它的onLayout便是地區分布的關鍵:
/** 確定子View的位置,這個就是地區分布的關鍵 */@Overridepublic void onLayout(boolean changed, int l, int t, int r, int b) {final int count = getChildCount();// 確定自身的寬高int thisW = r - l - this.getPaddingLeft() - this.getPaddingRight();int thisH = b - t - this.getPaddingTop() - this.getPaddingBottom();// 自身內容地區的右邊和下邊int contentRight = r - getPaddingRight();int contentBottom = b - getPaddingBottom();// 按照順序存放把地區存放到集合中List<Integer> availAreas = new ArrayList<Integer>(mAreaCount);for (int i = 0; i < mAreaCount; i++) {availAreas.add(i);}int areaCapacity = (count + 1) / mAreaCount + 1; // 地區密度,表示一個地區內可以放幾個View,+1表示至少要放一個int availAreaCount = mAreaCount; // 可用的地區個數for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (child.getVisibility() == View.GONE) { // gone掉的view是不參與布局continue;}if (!mFixedViews.contains(child)) {// mFixedViews用於存放已經確定好位置的View,存到了就沒必要再次存放LayoutParams params = (LayoutParams) child.getLayoutParams();// 先測量子View的大小int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.AT_MOST);// 為子View準備測量的參數int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(this.getMeasuredHeight(), MeasureSpec.AT_MOST);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);// 子View測量之後的寬和高int childW = child.getMeasuredWidth();int childH = child.getMeasuredHeight();// 用自身的高度去除以分配值,可以算出每一個地區的寬和高float colW = thisW / (float) mXRegularity;float rowH = thisH / (float) mYRegularity;while (availAreaCount > 0) { // 如果使用地區大於0,就可以為子View嘗試分配int arrayIdx = mRdm.nextInt(availAreaCount);// 隨機一個list中的位置int areaIdx = availAreas.get(arrayIdx);// 再根據list中的位置擷取一個地區編號int col = areaIdx % mXRegularity;// 計算出在二維數組中的位置int row = areaIdx / mXRegularity;if (mAreaDensity[row][col] < areaCapacity) {// 地區密度未超過限定,將view置入該地區int xOffset = (int) colW - childW; // 地區寬度 和 子View的寬度差值,差值可以用來做地區內的位置隨機if (xOffset <= 0) {// 說明子View的寬比較大xOffset = 1;}int yOffset = (int) rowH - childH;if (yOffset <= 0) {// 說明子View的高比較大yOffset = 1;}// 確定左邊,等於地區寬度*左邊的地區params.mLeft = getPaddingLeft() + (int) (colW * col + mRdm.nextInt(xOffset));int rightEdge = contentRight - childW;if (params.mLeft > rightEdge) {// 加上子View的寬度後不能超出右邊界params.mLeft = rightEdge;}params.mRight = params.mLeft + childW;params.mTop = getPaddingTop() + (int) (rowH * row + mRdm.nextInt(yOffset));int bottomEdge = contentBottom - childH;if (params.mTop > bottomEdge) {// 加上子View的寬度後不能超出右邊界params.mTop = bottomEdge;}params.mBottom = params.mTop + childH;if (!isOverlap(params)) {// 判斷是否和別的View重疊了mAreaDensity[row][col]++;// 沒有重疊,把該地區的密度加1child.layout(params.mLeft, params.mTop, params.mRight, params.mBottom);// 布局子ViewmFixedViews.add(child);// 添加到已經布局的集合中break;} else {// 如果重疊了,把該地區移除,availAreas.remove(arrayIdx);availAreaCount--;}} else {// 地區密度超過限定,將該地區從可選地區中移除availAreas.remove(arrayIdx);availAreaCount--;}}}}mLayouted = true;}
說實在的,這麼長的程式碼分析起來著實有點費勁,必要的部分我加了注釋,這裡就不多說了。
在StellarMap中加入了手勢,用於使用者滑動的時候給與互動:
@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {int centerX = getMeasuredWidth() / 2;int centerY = getMeasuredWidth() / 2;int x1 = (int) e1.getX() - centerX;int y1 = (int) e1.getY() - centerY;int x2 = (int) e2.getX() - centerX;int y2 = (int) e2.getY() - centerY;if ((x1 * x1 + y1 * y1) > (x2 * x2 + y2 * y2)) {zoomOut();} else {zoomIn();}return true;}
/** 給Group設定動畫入 */public void zoomIn() {final int nextGroupIndex = mAdapter.getNextGroupOnZoom(mShownGroupIndex, true);switchGroup(nextGroupIndex, true, mZoomInNearAnim, mZoomInAwayAnim);}/** 給Group設定齣動畫 */public void zoomOut() {final int nextGroupIndex = mAdapter.getNextGroupOnZoom(mShownGroupIndex, false);switchGroup(nextGroupIndex, true, mZoomOutNearAnim, mZoomOutAwayAnim);}
可見最後還是調回了我們的switchGroup方法。
本文最後附上Demo以供參考。
android自訂控制項之飛入飛出控制項