自定义View实战--实现一个清新美观的加载按钮
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
在 Dribble 上偶然看到了一组交互如下:
当时在心里问自己能不能做,答案肯定是能做的,不过我比较懒,觉得中间那个伸缩变化要编写很多代码,所以懒得理。后来,为了不让自己那么浮躁,也为了锻炼自己的耐心程度,还是坚持实现它了。这个过程,觉得自己还是有所收获,把握了一些想当然的细节,输理了对于自定义 View 的流程。
我将这个自定义 View,起了一个名字叫做 LoadButton。
这篇文章涉及到的知识点有如下:
1. 自定义 View 时的基本流程,包含 attrs.xml 中属性的编写,构造方法中属性的获取,onMeasure() 中尺寸的测量。onDraw() 中界面的实现。
2. 可以让 Android 初学者再次感受一次回调机制的美妙。
3. 属性动画的基本使用。
第一步,先确定尺寸
先观察 LoadView 的形态。
上面的显示的是两种形状,一个是圆角矩形,另外一个就是圆。两个形态尺寸区别是,高相同,宽度不一致。
我们再进一步分析形态 1。
形态 1 可以看成是左右两个半圆和中间一个矩形。再回顾下示例图片中的动画表现。
圆角矩形最终变成了一个圆。我们可以用线框图来渐进表现它。
当进行动画时,中间的矩形部分不停地缩小,当它缩小为 0 时,形态 1 就转变成了形态 2。
上面的能够说明什么呢?说明 LoadButton 由 3 个部分组成,左右的半圆和中间的矩形,即使是形态 2 也可以看做是左右半圆和中间宽度为 0 的矩形组成。
细化尺寸
我们进一步讨论尺寸相关的情况。
我们知道对于普通开发者而言,自定义一个 View 测量尺寸的时候我们通常要关注的测量模式是 MeasureSpec.EXACTLY 和 MeasureSpec.AT_MOST 两种。要了解更多详细的信息可以阅读我写的这篇博文《长谈:关于 View Measure 测量机制,让我一次把话说完》。接下来,我们详细讨论一下这两种情况。
MeasureSpec.EXACTLY
当一个 View 的 layout_width 或者 layout_height 的取值为 match_parent 或 30dp 这样具体的数值时,这就表明它的测量模式是 MeasureSpec.EXACTLY。它已经获得了精确的数值了,按照常理我们是不应该再去干涉它,parent 给出的建议尺寸是什么,我们就把尺寸设置成什么,但是结合开发的实际情况来看,我们有一个底线,为了保证 LoadView 的完整性,也就是再差的情况下,parent 给出来的建议尺寸也不能小于形态 2。否则如下图情况就不是我们想要的了
MeasureSpec.AT_MOST
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST,这个时候我们需要自己根据内容计算尺寸。而 LoadButton 的内容是什么呢?它的内容有 text 还有 加载成功或者加载失败的图片。因为图片大小在形态 2 中的圆形内可以确认。所以问题的关键就在于 LoadButton 文字内容宽高的尺寸测量。
text 内容自然是居中显示,然后它距离中间的 rect 上下左右间距也要考虑。这个时候的 rect 尺寸就是相对应的文字尺寸加上相对应方向上的 padding 值,这些 padding 值通过在 attrs.xml 中自定义属性然后在布局文件中赋予。
最后整体 LoadButton 尺寸自然是中间 rect 加上左右两个半圆的半径,但是这还不是最终的尺寸,最终的尺寸还是要和 parent 给的建议尺寸比较,不能大于它。
上面分析了尺寸测量相关,所以顺着思路进行的话,编码也只是水到渠成的事情了。
public class LoadButton extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//用于保存最终尺寸
int resultW = widthSize;
int resultH = heightSize;
// contentW contentH 用于确定中间矩形的尺寸
int contentW = 0;
int contentH = 0;
if ( widthMode == MeasureSpec.AT_MOST ) {
mTextWidth = (int) mTextPaint.measureText(mText);
contentW += mTextWidth + mLeftRightPadding * 2 + mRadiu * 2;
resultW = contentW < widthSize ? contentW : widthSize;
}
if ( heightMode == MeasureSpec.AT_MOST ) {
contentH += mTopBottomPadding * 2 + mTextSize;
resultH = contentH < heightSize ? contentH : heightSize;
}
resultW = resultW < 2 * mRadiu ? 2 * mRadiu : resultW;
resultH = resultH < 2 * mRadiu ? 2 * mRadiu : resultH;
// 修整圆形的半径
mRadiu = resultH / 2;
// 记录中间矩形的宽度值
rectWidth = resultW - 2 * mRadiu;
setMeasuredDimension(resultW,resultH);
Log.d(TAG,"onMeasure: w:"+resultW+" h:"+resultH);
}
}
第二步,绘制
测量是在 onMeasure() 方法中进行,而绘制就是在 onDraw() 方法中进行的,这是 Android 开发者都知道的事情。所以这一节的重点在于 onDraw() 这个方法。
为了不给读者造成困扰,我先张贴自定的属性,及在构造方法中获取属性值的代码。其它的细节应该看名字就大概知道了。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoadButton">
<attr name="android:text" />
<attr name="android:textSize" />
<attr name="stroke_color" format="color|reference" />
<attr name="content_color" format="color|reference" />
<attr name="radiu" format="dimension|reference" />
<attr name="rectwidth" format="dimension|reference" />
<attr name="contentPaddingLR" format="dimension|reference" />
<attr name="contentPaddingTB" format="dimension|reference" />
<attr name="progressedWidth" format="dimension|reference" />
<attr name="backColor" format="color|reference" />
<attr name="progressColor" format="color|reference" />
<attr name="progressSecondColor" format="color|reference" />
<attr name="loadSuccessDrawable" format="reference" />
<attr name="loadErrorDrawable" format="reference" />
<attr name="loadPauseDrawable" format="reference" />
</declare-styleable>
</resources>
然后在 LoadButton 的构造方法中获取这些值。
public LoadButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDefaultRadiu = 40;
mDefaultTextSize = 24;
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.LoadButton);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.LoadButton_android_textSize,
mDefaultTextSize);
mStrokeColor = typedArray.getColor(R.styleable.LoadButton_stroke_color, Color.RED);
mTextColor = typedArray.getColor(R.styleable.LoadButton_content_color, Color.WHITE);
mText = typedArray.getString(R.styleable.LoadButton_android_text);
mRadiu = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_radiu,mDefaultRadiu);
mTopBottomPadding = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_contentPaddingTB,10);
mLeftRightPadding = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_contentPaddingLR,10);
mBackgroundColor = typedArray.getColor(R.styleable.LoadButton_backColor,Color.WHITE);
mProgressColor = typedArray.getColor(R.styleable.LoadButton_progressColor,Color.WHITE);
mProgressSecondColor = typedArray.getColor(R.styleable.LoadButton_progressSecondColor,Color.parseColor("#c3c3c3"));
mProgressWidth = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_progressedWidth,2);
mSuccessedDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadSuccessDrawable);
mErrorDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadErrorDrawable);
mPauseDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadPauseDrawable);
typedArray.recycle();
......
}
形态 1 的绘制,借助于 Path 的力量
Android 绘制图形离不开 Canvas,Canvas 可以直接绘制 直线、矩形、圆、椭圆,但是 LoadButton 的形态 1 怎么绘制呢?它是一个不规则的闭合图形,直接用 Canvas 的话肯定不行,所以得借助另外一个类 Path,Path 中文译做路径,可以专门处理这种情况,而且可以处理比这复杂的情况,具体情况请读者们自己查阅相应资料与教程。
我们再来观察 形态 1 到形态 2 的转变过程。
这是个中间矩形从初始值变为 0 的过程,我们用 rectWidth 表示这个矩形的宽度值,因为在 onDraw() 方法中,LoadButton 尺寸确定,所以我们很容易得到它的中心点,所以我们可以中心点坐标为参考坐标,然后以 rectWidth 为变量创建一个 path,这个 path 实现了 LoadButton 的轮廓。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = getWidth() / 2;
int cy = getHeight() / 2;
drawPath(canvas,cx,cy);
.....
}
private void drawPath(Canvas canvas,int cx,int cy) {
if (mPath == null) {
mPath = new Path();
}
mPath.reset();
left = cx - rectWidth / 2 - mRadiu;
top = 0;
right = cx + rectWidth / 2 + mRadiu;
bottom = getHeight();
leftRect.set(left,top,left + mRadiu * 2,bottom);
rightRect.set(right - mRadiu * 2,top,right,bottom);
contentRect.set(cx-rectWidth/2,top,cx + rectWidth/2,bottom);
//path 起始位置
mPath.moveTo(cx - rectWidth /2,bottom);
// 左边半圆
mPath.arcTo(leftRect,
90.0f,180f);
//连接到右边半圆
mPath.lineTo(cx + rectWidth/2,top);
// 右边半圆
mPath.arcTo(rightRect,
270.0f,180f);
// path 闭合
mPath.close();
// 以填充的方向将图形填充为指定的背景色
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBackgroundColor);
canvas.drawPath(mPath,mPaint);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mStrokeColor);
}
以 rectWidth 为变量建立 path 的好处时,当从形态 1 到 形态 2 转变的过程,肯定是 rectWidth 数值变化的过程,而对于其它数值是不变的,所以重绘的时候 LoadButton 能够很轻松地处理这种情况。
我们到这一步的时候已经能够准确地绘制了 LoadButton 的轮廓。现在需要精确地绘制它的内容,只有这样才是完整的 LoadButton。
我们先需要给 LoadButton 定义一些状态。
LoadButton 的状态
enum State {
INITIAL,// 初始状态
FOLDING,// 正在伸缩
LOADING, // 正在加载
ERROR,// 加载失败
SUCCESSED,// 加载成功
PAUSED // 加载暂停
}
它们的状态转换如下:
LoadButton 的状态转换由用户点击按钮触发。所以 LoadButton 需要在内部设置一个 OnClickListenner。
1. 当在 Initial 状态下点击时,它会转换到 Folding 状态下。
2. Foding 状态结束后,由形态 1 转变成形态 2。自然就进入了 Loading 状态。
3. Loading 状态有 3 个走向,加载成功后,用户通过相应 API 设置状态为 Successed。加载失败后,用户可以设置状态为 Error。如果在 Loading 状态下点击按钮,会进入 Paused 状态。
4. 在 Paused 状态下点击按钮,LoadButton 重新进入 Loading 状态。
5. 在 Successed 或者 Error 状态下点击按钮,将通过回调对象,通知调用者点击事件的发生。
我们在 LoadButton 的构造方法中设置这样的内部的 OnClickListenner。
public LoadButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
......
isUnfold = true;
mListenner = new OnClickListener() {
@Override
public void onClick(View v) {
if ( mCurrentState == State.FODDING) {
return;
}
if ( mCurrentState == State.INITIAL ) {
if ( isUnfold ) {
shringk();
}
} else if ( mCurrentState == State.ERROR) {
if (mLoadListenner != null ) {
mLoadListenner.onClick(false);
}
} else if ( mCurrentState == State.SUCCESSED ) {
if (mLoadListenner != null ) {
mLoadListenner.onClick(true);
}
} else if ( mCurrentState == State.PAUSED) {
if (mLoadListenner != null ) {
mLoadListenner.needLoading();
load();
}
} else if ( mCurrentState == State.LOADDING) {
mCurrentState = State.PAUSED;
cancelAnimation();
invaidateSelft();
}
}
};
setOnClickListener(mListenner);
mCurrentState = State.INITIAL;
......
}
状态的绘制
Initial 状态下其实就是中间一个 text 文本居中显示,相关代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
int textDescent = (int) mTextPaint.getFontMetrics().descent;
int textAscent = (int) mTextPaint.getFontMetrics().ascent;
int delta = Math.abs(textAscent) - textDescent;
if ( mCurrentState == State.INITIAL) {
canvas.drawText(mText,cx,cy + delta / 2,mTextPaint);
}
.....
}
Folding 状态其实就是不显示文字的 Inital 状态,不同的还有它的 rectwidth 每次重绘时会变小,最终会由 Initial 的形态 1 过渡到 Loading 状态下的形态 2。在 Initial 状态下点击按钮会调用一个动画,这个动画用于展示形态 1 到形态 2 的过程。
if ( mCurrentState == State.INITIAL ) {
if ( isUnfold ) {
shringk();
}
}
public void shringk() {
if (shrinkAnim == null) {
shrinkAnim = ObjectAnimator.ofInt(this,"rectWidth", rectWidth,0);
}
shrinkAnim.addListener(this);
shrinkAnim.setDuration(500);
shrinkAnim.start();
mCurrentState = State.FOLDING;
}
public void setRectWidth (int width) {
rectWidth = width;
invaidateSelft();
}
private void invaidateSelft() {
if (Looper.myLooper() == Looper.getMainLooper()) {
invalidate();
} else {
postInvalidate();
}
}
这里是一个典型的属性动画应用场景,通过不断改变属性 rectWidth 的值来进行重绘,而对于绘制这一方面,文章前面部分有说过 LoadButton 通过以中心坐标为参考,以 mRectWidth 为变量建立了一个 Path 来绘制轮廓。
另外,大家可以注意到,shrinkAnim 有一个监听器,我设置为了 LoadButton 本身。
public class LoadButton extends View implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isUnfold = false;
load();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
在收缩动画结束的时候,我调用了 load() 方法用来将状态设置为 Loading,并进行加载动画。
我们先看看 Loading 状态下的绘制,它是形态 2 ,也就是在一个圆形内有一个正在加载无限循环的动画。思路也很简单,用进度条的背景色画一个圆圈,然后用进度条的前景色绘制相应角度的弧,并且这个弧的半径和进度条的半径一样。
if ( mCurrentState == State.LOADING) {
if ( progressRect == null ) {
progressRect = new RectF();
}
progressRect.set(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
mPaint.setColor(mProgressSecondColor);
//先绘制背景圆
canvas.drawCircle(cx,cy,circleR,mPaint);
mPaint.setColor(mProgressColor);
Log.d(TAG,"onDraw() pro:"+progressReverse+" swpeep:"+circleSweep);
if ( circleSweep != 360 ) {
mProgressStartAngel = progressReverse ? 270 : (int) (270 + circleSweep);
//绘制弧线
canvas.drawArc(progressRect
,mProgressStartAngel,progressReverse ? circleSweep : (int) (360 - circleSweep),
false,mPaint);
}
mPaint.setColor(mBackgroundColor);
}
上面有两个关键的变量 progressReverse 和 circleSweep。progressReverse 用来表示动画是否需要翻转,circleSweep 表示每次绘制的时候从起始角度扫描的角度。
正常情况下,起始角度是 270 度不变,如果动画翻转时,它是 270 + circleSweep 的值,具体为什么这样做,大家可以观看之前的图像来思考一下。
加载的动画自然也是属性动画控制的,这个动画让 circleSweep 从 0 到 360 之间不停地变化。并且在每次循环的时候,将 progressReverse 变量置反。
public void load() {
if (loadAnimator == null) {
loadAnimator = ObjectAnimator.ofFloat(this,"circleSweep",0,360);
}
loadAnimator.setDuration(1000);
loadAnimator.setRepeatMode(ValueAnimator.RESTART);
loadAnimator.setRepeatCount(ValueAnimator.INFINITE);
loadAnimator.removeAllListeners();
loadAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
// Log.d(TAG,"onAnimationRepeat:"+progressReverse);
progressReverse = !progressReverse;
}
});
loadAnimator.start();
mCurrentState = State.LOADING;
}
Paused 状态是当 LoadButton 在 Loading 状态下,用户点击了按钮,这个时候按钮会显示一个暂停图标。
if ( mCurrentState == State.LOADING) {
mCurrentState = State.PAUSED;
cancelAnimation();
invaidateSelft();
}
至于显示方面,非常简单就是给一个 drawable 设置好 bound 范围然后显示。稍后我会给出代码。
Successed 状态和 Error 状态实现过程基本上是一致的。但是它们被点击的时候,需要通知点击者。所以我们需要定义一个回调接口。
if ( mCurrentState == State.ERROR) {
if (mLoadListenner != null ) {
mLoadListenner.onClick(false);
}
} else if ( mCurrentState == State.SUCCESSED ) {
if (mLoadListenner != null ) {
mLoadListenner.onClick(true);
}
} else if ( mCurrentState == State.PAUSED) {
if (mLoadListenner != null ) {
mLoadListenner.needLoading();
load();
}
}else if ( mCurrentState == State.PAUSED) {
if (mLoadListenner != null ) {
mLoadListenner.needLoading();
load();
}
}
public interface LoadListenner {
void onClick(boolean isSuccessed);
void needLoading();
}
LoadListenner.onClick() 方法中的参数,isSuccessed 为真告诉点击者加载成功了的信息。否则提示加载失败。needLoading() 方法用来告诉点击者当在 Paused 状态下点击按钮时,调用者应该重新加载了。
它们的显示代码如下:
if ( mCurrentState == State.ERROR) {
mErrorDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
mErrorDrawable.draw(canvas);
} else if (mCurrentState == State.SUCCESSED) {
mSuccessedDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
mSuccessedDrawable.draw(canvas);
} else if (mCurrentState == State.PAUSED) {
mPauseDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
mPauseDrawable.draw(canvas);
}
另外,需要注意的是 Successed 和 Error 状态,需要开发者根据实际情况决定调用。
public void loadSuccessed() {
mCurrentState = State.SUCCESSED;
cancelAnimation();
invaidateSelft();
}
public void loadFailed() {
mCurrentState = State.ERROR;
cancelAnimation();
invaidateSelft();
}
将 LoadButton 重置为 Initial 状态用 reset() 方法。
public void reset(){
mCurrentState = State.INITIAL;
rectWidth = getWidth() - mRadiu * 2;
isUnfold = true;
cancelAnimation();
invaidateSelft();
}
到此,整个 LoadButton 实现逻辑已经完成。接下来我们可以编写代码测试。
测试
我们添加一个 LoadButton 到布局文件,然后用 3 个 Button 来测试它成功、失败、重置的情况。
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:orientation="vertical"
tools:context="com.frank.statusbuttondemo.MainActivity">
<com.frank.statusbuttondemo.LoadButton
android:id="@+id/btn_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backColor="#009966"
app:contentPaddingLR="20dp"
app:contentPaddingTB="20dp"
app:content_color="@android:color/white"
app:progressedWidth="4dp"
android:textSize="36sp"
android:text="点击加载" />
<Button
android:id="@+id/btn_test_successed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="加载成功"/>
<Button
android:id="@+id/btn_test_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="加载失败"/>
<Button
android:id="@+id/btn_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="重置按钮"/>
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
LoadButton mLoadButton;
Button mBtnSuccessed,mBtnError,mBtnReset;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLoadButton = (LoadButton) findViewById(R.id.btn_status);
mBtnSuccessed = (Button) findViewById(R.id.btn_test_successed);
mBtnError = (Button) findViewById(R.id.btn_test_error);
mBtnReset = (Button) findViewById(R.id.btn_reset);
mBtnError.setOnClickListener(this);
mBtnSuccessed.setOnClickListener(this);
mBtnReset.setOnClickListener(this);
mLoadButton.setListenner(new LoadButton.LoadListenner() {
@Override
public void onClick(boolean isSuccessed) {
if ( isSuccessed ) {
Toast.makeText(MainActivity.this,"加载成功",Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this,"加载失败",Toast.LENGTH_LONG).show();
}
}
@Override
public void needLoading() {
Toast.makeText(MainActivity.this,"重新下载",Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onClick(View v) {
switch (v.getId())
{
case R.id.btn_test_successed:
mLoadButton.loadSuccessed();
break;
case R.id.btn_test_error:
mLoadButton.loadFailed();
break;
case R.id.btn_reset:
mLoadButton.reset();
break;
default:
break;
}
}
}
测试结果:
总结
本文的主题并不难,但是如果要实现它也需要细心。关键是编码的时候,要先设计分析,之后就是一气呵成、水到渠成的事情了。
通过演练这个项目,我觉得自己还是有些收获。
- 复习了自定义 View 的基本流程。特别是对 onMeasure() 这一块有更深的理解。
- 复习了属性动画的使用。
- 复习了 Canvas 和 Path 的基本用法。
- 演练了状态模式下的编程。
- 享受回调机制带来的美妙感受。
如果有人认为好用,我想把它上传到 jcenter 仓库,目的也是为了演练怎么上传 Android 模块到开源库。喜欢这篇文章就给我一个赞吧,需要你们的鼓励。哈哈。
自定义View实战--实现一个清新美观的加载按钮的更多相关文章
- 在Qt Quick中一个简单Hello World加载过程
Qt5基本类图: QQmlEngine QQmlEngine类提供了一个QML引擎,用于管理由QML文档定义的对象层次架构,QML提供了一个默认的QML上下文(根上下文,获取函数QQmlEngi ...
- 【SpringBoot基础系列-实战】如何指定 bean 最先加载(应用篇)
[基础系列-实战]如何指定 bean 最先加载(应用篇) 在日常的业务开发中,绝大多数我们都是不关注 bean 的加载顺序,然而如果在某些场景下,当我们希望某个 bean 优于其他的 bean 被实例 ...
- 使用RequireJS并实现一个自己的模块加载器 (一)
RequireJS & SeaJS 在 模块化开发 开发以前,都是直接在页面上引入 script 标签来引用脚本的,当项目变得比较复杂,就会带来很多问题. JS项目中的依赖只有通过引入JS的顺 ...
- 【模块化编程】理解requireJS-实现一个简单的模块加载器
在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...
- 定义了一个UIImageView如何使加载的图片不会失真 UIImageView的Frame值是固定的
定义了一个UIImageView如何使加载的图片不会失真 UIImageView的Frame值是固定的 UIViewContentModeScaleToFill, 缩放内容到合适比例大小 UIVie ...
- 使用RequireJS并实现一个自己的模块加载器 (二)
2017 新年好 ! 新年第一天对我来说真是悲伤 ,早上兴冲冲地爬起来背着书包跑去实验室,结果今天大家都休息 .回宿舍的时候发现书包湿了,原来盒子装的牛奶盖子松了,泼了一书包,电脑风扇口和USB口都进 ...
- ajax一个很好的加载效果
推荐一个常用的jquery加载效果插件: 要引入这个插件的css和js: <link href="<%=path %>/css/showLoading.css" ...
- Android自定义View实战(SlideTab-可滑动的选择器)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/52178553 本文出自:[openXu的博客] 目录: 初步分析重写onDraw绘制 重写o ...
- 自定义View实战
PS:上一篇从0开始学自定义View有博友给我留言说要看实战,今天我特意写了几个例子,供大家参考,所画的图案加上动画看着确实让人舒服,喜欢的博友可以直接拿到自己的项目中去使用,由于我这个写的是demo ...
随机推荐
- Python(面向对象编程4——继承顺序、封装)
继承顺序 ''' 一点需要注意 ''' class Father: def f1(self): print("test func followed ==>") self.te ...
- CSS 一个完整的例子
My first web page What this is A simple page put together using HTML. I said a simple page put toget ...
- 程序包com.sun.istack.internal不存在
添加一下依赖 <!-- https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-impl --><dependency> ...
- 1163: [Baltic2008]Mafia
1163: [Baltic2008]Mafia Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 123 Solved: 70[Submit][Stat ...
- [Android]android Service后台防杀
网上有很多办法,方法一:在JNI里面fork出子进程service在单独的进程中,在service中调用JNI的代码,然后fork出一个进程,然后让我们的service进程和fork出来的子进程一直运 ...
- Maven 的41种骨架功能介绍
1: internal -> appfuse-basic-jsf (创建一个基于Hibernate,Spring和JSF的Web应用程序的原型) 2: internal -> appfu ...
- BZOJ-5424: 烧桥计划(单调队列)
BZOJ-5424: 烧桥计划(单调队列) 题目链接 题解: 先考虑最暴力的\(dp\):设\(f[k][i]\)表示搞掉第\(1\sim i\)段,烧了\(k\)段的最小花费,设\(calc(x,y ...
- xml的servlet配置
内容如下 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="ht ...
- Spring Boot集成Redis实现缓存机制【从零开始学Spring Boot】
转自:https://blog.csdn.net/linxingliang/article/details/52263763 spring boot 自学笔记(三) Redis集成—RedisTemp ...
- Python中的X[:,0]和X[:,1]
https://blog.csdn.net/csj664103736/article/details/72828584 python中 x=x[1:] 是什么意思 将x的第二位到最后一位的内容赋给x. ...