編輯:關於Android編程
對我來說,寫自定義view是一個特麻煩但是寫完之後特有成就感的過程。寫完之後我總是喜歡拿給別人看,去炫耀(當然只是在自己熟悉和關系不錯的人群裡),盡管它們看起來會很簡陋。希望這次寫的東西不會讓大家覺得太過於簡陋,有錯誤的還是得需要各大神的點醒和賜教,有批評建議的也請隨時賜教。
一般的自定義view有三種方式:
繼承具體的view,如textView,button等等; 繼承一個容器view,如linearlayout,relativeLayout等等,然後給該容器添加一些具體的控件來組合成一個新的view; 繼承View。這個方式需要重寫包括onDraw,onMeasure,onLayout,onTouchEvent等等方法來繪制一個新的view對於這三種方式,我在這裡說一下自己的看法:
首先,對於我來說,我很少使用第一種方式。由於繼承的是一個具體的view,這個view的功能一般都是很強大了。我們能做的,希望去做的,大概是改變形狀或者添加一些形狀功能等等。當然由於我目前基本上沒有做過相應的需求,我也無法去說這個方式到底該怎麼弄。因為在我目前認知裡,如果准備大改某個view,我會用第三種方式,如果需要添加一些模塊或者功能我會使用第二種方式。
然後,對於第二種方式,簡單得說,就是控件組合,把一些控件組合起來,形成一個特殊形狀和功能的view。這個是三種方式中我覺得最方便的,只要做好各個控件的擺放,並提供一些參數設置的方法,或者干脆把這些控件實例提供給使用者,讓使用者自己去操作這些實例。這個方法的缺點也是有的,這裡說的特殊形狀,是由有規則的控件布局而成,所能表達的形狀也是有限的。
對於第三種方式,簡單地說,就是自己畫,利用canvas和paint等一點一線的自己來畫。這種方式是最具創造力的,可以畫出幾乎所有的形狀。(我這裡這麼說,是我感覺這種方式最貼近自定義這個詞)。但同時他的缺點也是巨大的,計算繪制的點、線、框、文字等等位置的值,是這個方式最大的挑戰之一。我經常就發現,在我程序的某段裡,全都是成員變量在那裡加減乘除,如果不給成員變量添加/* ……/這樣的注釋,過不了多久就不知道那段程序到底在干什麼。
對於這三種方式,第一種我幾乎不用,在時間不夠充裕的時候我會用第二種方式,在時間充裕的時候和view非常特殊需要自己繪制的時候我會使用第三種方式。
接下來我把我寫過的一個類似seekBar的自定義view拿出來和大家探討一下。這是一個非常簡單的自定義view,就是我們常見的視頻播放控件下邊顯示視頻進度的一個進度條,不同的是他的左右各有一個textView來顯示時間。
我用第二種方式寫的代碼:
自定義view:
package test2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
public class MySeekbarView extends LinearLayout{
private TextView mTvCurrentTime;
private TextView mTvTotalTime;
private SeekBar mSeekbar;
public MySeekbarView(Context context, AttributeSet attrs) {
super(context, attrs);
LinearLayout ll = new LinearLayout(context);
ll.setGravity(Gravity.CENTER_VERTICAL);
ll.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mTvCurrentTime = new TextView(context);
mTvCurrentTime.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
mTvTotalTime = new TextView(context);
mTvCurrentTime.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
mSeekbar = new SeekBar(context);
mSeekbar.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1));
ll.addView(mTvCurrentTime);
ll.addView(mSeekbar);
ll.addView(mTvTotalTime);
this.addView(ll);
}
public TextView getCurrentTimeTextView(){
return mTvCurrentTime;
}
public TextView getTotalTimeTextView(){
return mTvTotalTime;
}
public SeekBar getSeekbar(){
return mSeekbar;
}
}
Activity的布局文件:
Activity:
package test2;
import com.example.test2.R;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.widget.SeekBar;
import android.widget.TextView;
public class Test2_seekbar extends Activity{
private MySeekbarView view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test2_seekbar_layout);
view = (MySeekbarView)findViewById(R.id.MySeekbarView);
TextView tv1 = view.getCurrentTimeTextView();
tv1.setText("01:00");
TextView tv2 = view.getTotalTimeTextView();
tv2.setText("09:00");
SeekBar seekbar = view.getSeekbar();
seekbar.setThumb(ContextCompat.getDrawable(this, R.drawable.video_pb_indicator));
seekbar.setProgress(50);
}
}
結果:
有沒有覺得很方便?在view的數量不多的情況下,直接新建view然後用addView()很方便快捷,但在view的數量有點大或者覺得腦子裡形象不是很直觀的情況下,我們一般會寫一個布局然後通過LayoutInflater的
inflate方法來直接獲得自己想要的這個已經布局好的view。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPtfUtqjS5XZpZXeyvL7WtcSyvL7WzsS8/qO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">
修改之後的自定義view:
package test2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import com.example.test2.R;
public class MySeekbarView extends LinearLayout{
private TextView mTvCurrentTime;
private TextView mTvTotalTime;
private SeekBar mSeekbar;
private View view;
public MySeekbarView(Context context, AttributeSet attrs) {
super(context, attrs);
view = LayoutInflater.from(context).inflate(R.layout.myseekbar_view_layout, this);
mTvCurrentTime = (TextView)view.findViewById(R.id.tv1);
mTvTotalTime = (TextView)view.findViewById(R.id.tv2);
mSeekbar = (SeekBar)view.findViewById(R.id.seekbar);
}
public TextView getCurrentTimeTextView(){
return mTvCurrentTime;
}
public TextView getTotalTimeTextView(){
return mTvTotalTime;
}
public SeekBar getSeekbar(){
return mSeekbar;
}
}
結果是一樣。
有沒有發現,哪裡不一樣。哈哈,發沒發現我都要講了。這是一個關於LayoutInflater的故事。我是在這個地方獲得了很厚重的知識:Android LayoutInflater深度解析 給你帶來全新的認識 。
這裡邊博主告訴我們LayoutInflater的Inflate的三個重構方法的差別:
Inflate(resId , null ):返回temp,沒有執行setLayoutParams和addView
Inflate(resId , root,false ):返回temp,執行setLayoutParams,沒有執行addView
Inflate(resId , root, true ):返回root,執行addView(view,params)
(呃在我那個程序中看到Inflate(resId , root)後沒有addView()方法也可以正常顯示,說明第三個參數默認為true。)
這樣看來在使用Inflate方法的時候有三種選擇:
1.
temp = LayoutInflater.from(context).Inflate(resId , null );
temp.setLayoutParams(…);
this.addView(temp);
2.
temp = LayoutInflater.from(context).Inflate(resId , root,false );
this.addView(temp);
3.
LayoutInflater.from(context).Inflate(resId , root,true );
可想而知他們之間的差別:
3是最簡單的情況,添加單個temp,無特殊params要求;
2其次,可以添加多個temp,無特殊params要求;
1最麻煩,既可以添加多個view又可以添加特殊params要求,唯一不爽的是,需要給每個temp都添加一遍;
這裡我還得提醒一下各位兄弟姐妹們(我最近在自定義的時候犯的錯誤,好不容易才找出來,希望各位兄姐妹們看了以後不要笑,也希望你們不會遇到這樣的問題):
1.一定要盡量把那幾個構造方法的重構都寫上。其實以我目前的經驗,對我有用的重構也就只有public MySeekbarView(Context context)和public MySeekbarView(Context context, AttributeSet attrs)。第一個我覺得主要是在隨時隨地通過context新建view的時候調用,第二個我覺得主要是在利用layout布局來創建view的時候調用。inflate方法中設置的layoutParams是從attrs中得到的,構造函數中沒有傳入AttributeSet attrs或者傳入的為空,inflate中設置的layoutParams為null,然後你就會看見一片空白;
2.盡量利用布局創建view;
3.不通過布局創建view的時候一定不要忘記setLayoutParams,通過布局創建view但沒有設置layoutParams時也要記得添加layoutParams;
4.這些對於linearlayout和relativelayout是有效的,對於viewGroup,呃,我並不是很確定,以後細細研究過後再來探討,不過我認為linearlayout和relativelayout已經包含了很多內容;
是不是有些亂了?我只能說亂的還在後邊。接下來我們來交流一下用第三種方法來自定義view。
package mydemo.myseekbardemo;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
public class MyView extends View{
private Context mContext;
/**
* view的寬
*/
private int mViewWidth;
/**
* view的高
*/
private int mViewHeight;
/**
* 底部進度條的寬
*/
private int mBottomBarWidth;
/**
* 進度條的高
*/
private int mBarHeight = 6;
/**
* 底部進度條左邊位置
*/
private int mBottomBarLeft;
/**
* 底部進度條上邊位置
*/
private int mBottomBarTop;
/**
* 底部進度條右邊位置
*/
private int mBottomBarRight;
/**
* 底部進度條底邊位置
*/
private int mBottomBarBottom;
/**
* 頂部進度條的右邊位置
*/
private int mTopBarRight;
/**
* 文字的寬
*/
private int mTvWidth;
/**
* 文字的高
*/
private int mTvHeight;
/**
* 圖片對象
*/
private Bitmap mBitmapOfCirlcle = null;
/**
* 圖片可見度,在0~255之間
*/
private int mBitmapAlpha = 255;
/**
* 規定圖片的寬
*/
private int mBitmapWidth = 30;
/**
* 規定圖片的高
*/
private int mBitmapHeight = 30;
/**
* 畫筆對象
*/
private Paint mPaint;
/**
* 判斷是否使用用戶的圖片
*/
private boolean hasCirclePic = false;
/**
* 圓圈圓心的x坐標
*/
private int mCircleX;
/**
* 圓圈圓心的y坐標
*/
private int mCircleY;
/**
* 文字大小
*/
private float mTextSize = 35;
/**
* 一些間距
*/
private int mPadding = 10;
/**
* 圓圈的半徑
*/
private int mRadius = 16;
/**
* 頂部進度條的右邊位置同時也是圓圈的x坐標
*/
private int mPosition;
/**
* 頂部進度條初始百分比進度
*/
private float mOriginalPercent = 0f;
/**
* 文本的顏色
*/
private int mTextColor = Color.WHITE;
/**
* 底部進度條的顏色
*/
private int mBottomBarColor = Color.GRAY;
/**
* 頂部進度條的顏色
*/
private int mTopBarColor = Color.BLUE;
/**
* 圓圈的顏色
*/
private int mCircleColor = Color.WHITE;
/**
* 用戶點擊位置狀態
*/
private int mWhereClickedState;
/**
* 用戶點擊在圓圈上
*/
private final int WHERR_CLICKED_CIRCLE = 0;
/**
* 用戶點擊在進度條上
*/
private final int WHERR_CLICKED_BAR = 1;
/**
* 用戶點擊在其他部分
*/
private final int WHERR_CLICKED_VIEW = 2;
/**
* 實時時間文本
*/
private String mCurrentTimeString = "00:00";
/**
* 總時間文本
*/
private String mTotalTimeString = "00:00";
/**
* 手勢管理對象
*/
private GestureDetector mGestureDetector;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.i("tag", "MyView");
mContext = context;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mGestureDetector = new GestureDetector(mContext, new OnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if(mWhereClickedState == WHERR_CLICKED_CIRCLE){
}else if(mWhereClickedState == WHERR_CLICKED_BAR){
mPosition = (int) Math.round(e.getX()+0.5);
if(mPosition >= mBottomBarRight){
mPosition = mBottomBarRight;
}else if(mPosition <= mBottomBarLeft){
mPosition = mBottomBarLeft;
}
MyView.this.invalidate();
}
return false;
}
@Override
public void onShowPress(MotionEvent e) {
// TODO Auto-generated method stub
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if(mWhereClickedState == WHERR_CLICKED_CIRCLE){
mPosition = (int) Math.round(e2.getX()+0.5);
if(mPosition >= mBottomBarRight){
mPosition = mBottomBarRight;
}else if(mPosition <= mBottomBarLeft){
mPosition = mBottomBarLeft;
}
MyView.this.invalidate();
}else if(mWhereClickedState == WHERR_CLICKED_BAR){
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// TODO Auto-generated method stub
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onDown(MotionEvent e) {
float event_x = e.getX();
float event_y = e.getY();
mWhereClickedState = judgeWhereClicked(event_x, event_y);
if(mWhereClickedState == WHERR_CLICKED_VIEW){
return false;
}else{
return true;
}
}
});
}
/**
* 獲得文本的寬高
*/
private void initTvWidthAndHeight() {
Rect rect = new Rect();
mPaint.getTextBounds("00:00", 0, 5, rect);
mTvWidth = rect.width();
mTvHeight = rect.height();
}
@Override
protected void onDraw(Canvas canvas) {
Log.i("tag", "onDraw");
mTopBarRight = mPosition;
mCircleX = mTopBarRight;
mPaint.setColor(mTextColor);
canvas.drawText(mCurrentTimeString, mPadding, mBottomBarBottom+Math.round(mTvHeight/4.0+0.5), mPaint);
canvas.drawText(mTotalTimeString, mViewWidth - mTvWidth - mPadding, mBottomBarBottom+Math.round(mTvHeight/4.0+0.5), mPaint);
mPaint.setColor(mBottomBarColor);
canvas.drawRect(mBottomBarLeft, mBottomBarTop, mBottomBarRight, mBottomBarBottom, mPaint);
mPaint.setColor(mTopBarColor);
canvas.drawRect(mBottomBarLeft, mBottomBarTop, mTopBarRight, mBottomBarBottom, mPaint);
if(mBitmapOfCirlcle != null){
mPaint.setAlpha(mBitmapAlpha);
canvas.drawBitmap(mBitmapOfCirlcle, mCircleX-mBitmapWidth/2, mCircleY - mBitmapHeight/2, mPaint);
}else{
mPaint.setColor(mCircleColor);
canvas.drawCircle(mCircleX, mCircleY, mRadius, mPaint);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mPaint.setTextSize(mTextSize);
//獲得文本的寬高
initTvWidthAndHeight();
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST){
mViewWidth = mTvWidth*4;
}else if(widthMode == MeasureSpec.EXACTLY){
if(mTvWidth*4 >= widthSize){
mViewWidth = mTvWidth*4;
}else{
mViewWidth = widthSize;
}
}
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mViewWidth, MeasureSpec.EXACTLY);
if(heightMode == MeasureSpec.AT_MOST){
mViewHeight = mTvHeight + 4*mPadding;
}else if(heightMode == MeasureSpec.EXACTLY){
if(heightSize <= mTvHeight+4*mPadding ){
mViewHeight = mTvHeight+4*mPadding;
}else{
mViewHeight = heightSize;
}
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mViewHeight, MeasureSpec.EXACTLY);
mBottomBarWidth = mViewWidth - 2*mTvWidth - 6*mPadding;
mBottomBarLeft = mTvWidth + 3*mPadding;
mBottomBarRight = mViewWidth - mTvWidth - 3*mPadding;
mBottomBarBottom = (mViewHeight - mTvHeight)/2+mTvHeight;
mBottomBarTop = mBottomBarBottom-mBarHeight;
mCircleY = mBottomBarBottom - mBarHeight/2;
mPosition = (int) Math.round(mBottomBarWidth * mOriginalPercent+0.5)+mBottomBarLeft;
if(mPosition <= mBottomBarLeft){
mPosition = mBottomBarLeft;
}else if(mPosition >= mBottomBarRight){
mPosition = mBottomBarRight;
}
if(mBitmapOfCirlcle != null){
mBitmapOfCirlcle = Bitmap.createScaledBitmap(mBitmapOfCirlcle, mBitmapWidth, mBitmapHeight, false);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 設置總時間,默認格式"--:--"
* @param strOfTime
*/
public void setTotalTimeString(String strOfTime){
Log.i("tag", "setTotalTimeString");
mTotalTimeString = strOfTime;
}
/**
* 設置實時時間,默認格式"--:--"
* @param strOfTime
*/
public void setCurrentTimeString(String strOfTime){
Log.i("tag", "setCurrentTimeString");
mCurrentTimeString = strOfTime;
}
/**
* 設置圖片的透明度
* @param alpha 0~255
*/
public void setBitmapAlpha(int alpha){
Log.i("tag", "setBitmapAlpha");
if(alpha < 0){
alpha = 0;
}else if(alpha > 255){
alpha = 255;
}
mBitmapAlpha = alpha;
}
/**
* 獲得現在所在位置的百分比進度值
* @return float
*/
public float getPercent(){
float percent = (mBottomBarRight - mPosition)*1.0f/mBottomBarWidth;
if(percent < 0.0){
percent = 0f;
}else if(percent > 1){
percent = 1f;
}
return percent;
}
/**
* 獲得當前進度值
* @return int
*/
public int getProgress(){
float percent = (mBottomBarRight - mPosition)*1.0f/mBottomBarWidth;
if(percent < 0.0){
percent = 0f;
}else if(percent > 1){
percent = 1f;
}
int progress = (int) Math.round(percent*100+0.5);
if(progress < 0){
progress = 0;
}else if(progress >100){
progress = 100;
}
return progress;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mGestureDetector != null){
return mGestureDetector.onTouchEvent(event);
}else{
return super.onTouchEvent(event);
}
}
/**
* 判斷用戶點擊位置狀態
* @param x 用戶點擊的x坐標
* @param y 用戶點擊的y坐標
* @return 返回用戶點擊未知狀態:
* WHERR_CLICKED_CIRCLE
* WHERR_CLICKED_BAR
* WHERR_CLICKED_VIEW
*/
public int judgeWhereClicked(float x, float y){
//s_x,s_y用來界定圓圈或者代替圓圈的圖片的區域,s_x表示x坐標方向,圓心到區域邊界的距離
int s_x = 0;
int s_y = 0;
if(hasCirclePic){
s_x = mBitmapWidth/2;
s_y = mBitmapHeight/2;
}else{
s_x = mRadius;
s_y = mRadius;
}
if( (x>=(mCircleX-s_x) && x<=(mCircleX + s_x)) && (y>=(mCircleY-s_y) && y<=(mCircleY + s_y)) ){
//s_x和s_y界定的區域,即圓圈區域
return WHERR_CLICKED_CIRCLE;
}else if((x>=mBottomBarLeft && x<=mBottomBarRight) || (y <= mBottomBarBottom+5 && y >= mBottomBarTop+5 )){
//進度條上除了圓圈區域的其他可響應點擊部分
return WHERR_CLICKED_BAR;
}else{
//view除了上述兩部分的部分
return WHERR_CLICKED_VIEW;
}
}
/**
* 設置初始百分比進度,0.0~1.0f
* @param percent
*/
public void setOriginalPercent(float percent){
mOriginalPercent = percent;
if(mOriginalPercent<0){
mOriginalPercent = 0.0f;
}else if(mOriginalPercent > 1.0f){
mOriginalPercent = 1.0f;
}
}
/**
* 設置初始進度值,默認進度最大值為100
* @param progress
*/
public void setOriginalProgress(int progress){
mOriginalPercent = progress/100f;
if(mOriginalPercent<0){
mOriginalPercent = 0.0f;
}else if(mOriginalPercent > 1.0f){
mOriginalPercent = 1.0f;
}
}
/**
* 通過傳入進度百分比來更新進度條
* @param percent
*/
public void updatePositionFromPercent(float percent){
Log.i("tag", "updatePositionFromPercent");
if(percent<0){
percent = 0.0f;
}else if(percent > 1.0f){
percent = 1.0f;
}
mPosition = (int) Math.round(mBottomBarWidth * percent+0.5)+mBottomBarLeft;
if(mPosition <= mBottomBarLeft){
mPosition = mBottomBarLeft;
}else if(mPosition >= mBottomBarRight){
mPosition = mBottomBarRight;
}
this.invalidate();
}
/**
* 通過傳入進度值來更新進度條
* @param percent
*/
public void updatePositionFromProgress(int progress){
float percent = progress/100f;
if(percent<0){
percent = 0.0f;
}else if(percent > 1.0f){
percent = 1.0f;
}
mPosition = (int) Math.round(mBottomBarWidth * percent+0.5)+mBottomBarLeft;
if(mPosition <= mBottomBarLeft){
mPosition = mBottomBarLeft;
}else if(mPosition >= mBottomBarRight){
mPosition = mBottomBarRight;
}
this.invalidate();
}
/**
* 設置文字顏色
* @param color
*/
public void setTextColor(int color){
mTextColor = color;
}
/**
* 設置文字大小
* @param size
*/
public void setTextSize(int size){
mTextSize = size;
}
/**
* 設置底部進度條顏色
* @param color
*/
public void setBottomBarColor(int color){
mBottomBarColor = color;
}
/**
* 設置頂部進度條顏色
* @param color
*/
public void setTopBarColor(int color){
mTopBarColor = color;
}
/**
* 獲得實時時間文本
* @return
*/
public String getCurrentTimeString(){
return mCurrentTimeString;
}
/**
* 獲得總時間文本
* @return
*/
public String getTotalTimeString(){
return mTotalTimeString;
}
/**
* 用其他圖片更換進度條上的圓圈
* @param bitmap
*/
public void setThumbBitmap(Bitmap bitmap){
mBitmapOfCirlcle = bitmap;
}
}
哈哈,是不是感覺有點頭大。其實很簡單,一共兩個部分,一是計算位置,二是繪制圖形。其中最看重的就是位置的計算,位置計算好了,繪制就是分分鐘的事了。canvas的繪制功能很強大,一切形狀功能都可以輕松繪制出來。看著圖形從一個點一條線到一個復雜圖形甚至是擁有動畫屬性,這個過程是很有成就感的,也是我最喜歡的過程。
對canvas和paint了解還不深的我就不在這裡露短了,只想告訴大家一些我知道的需要注意的問題:
1.現在的手機基本上都是支持硬件加速的,而並不是所有的canvas的操作都支持硬件加速的,比如canvas.drawPicture()等,所以在使用自定義view特別是自己繪制的自定義view的時候需要注意是否應該開啟硬件加速;
2.最好不要頻繁得創建paint對象,頻繁創建對象本就是編程的大忌,繪制的過程更是如此;
3.想要使得view擁有動畫屬性,只要在靜止的位置上添加偏移量,改變偏移量,就能完成動畫的繪制;
4.盡量給成員變量添加/** ……/這樣的注釋,特別是在計算位置特別復雜的自定義view裡,這樣會更加便於閱讀和回憶;
第一次寫博客,沒想到會花這麼長的時間。以前覺得博客內容很少,寫的應該蠻快的。今天我真真切切的感受到,寫一篇博客真是相當不容易的。也許只是像我這種小新才會有這種感覺把。不過還蠻有成就感的,盡管我覺得真的沒有寫些什麼。最後還是希望有什麼批評建議的請隨時賜教!
Matrix的數學原理在Android中,如果你用Matrix進行過圖像處理,那麼一定知道Matrix這個類。Android中的Matrix是一個3 x 3的矩陣,其內容
本文實例講述了Android編程使用ListView實現數據列表顯示的方法。分享給大家供大家參考,具體如下:要將數據庫中的數據列表顯示在屏幕上,我們要使用ListView
Android注解越來越引領潮流,比如 Dagger2, ButterKnife, EventBus3 等,他們都是注解類型,而且他們都有個共同點就是編譯時生成代碼,而不
一、前言 Android 中解決滑動的方案有2種:外部攔截法 和內部攔截法。 滑動沖突也存在2種場景: 橫豎滑動沖突、同向滑動沖突。 所以我就寫了4個例子來學習如何解決滑