Android 实战之酷云(一)
前言
大家好啊!好久没有见到我了吧。为什么呢!当然是由于开学啦,这学期非常多课,身为部长实验室也也非常多活动和一堆小师弟。同一时候还有蓝桥杯和华为软件开发大赛。并且近期在做一个综合性比較高的作品,没错了,就是酷云,一款仿网易云音乐的在线播放器。当然了,如今我还没有完毕这个作品,并且仅仅是刚刚開始写而已,只是也遇到了非常多坑。所以写下这篇文章来记录一下这次开发中各种各样的坑,希望对各位有所帮助。文章会随着我的开发进度而不定期更新,各位有问题也能够给我发私信,我们一起讨论解决。
至于为什么选择网易云音乐我觉得主要是由于它不仅有着 93% 的主流音乐版权,能够找到最齐全的主流音乐。出色的用户界面和体验。让用户感觉舒适。还有人性化的体验,如生日推荐音乐。
自己定义 View 控件
不知道各位有没有体验过网易云音乐 APP ,通过我近期的体验。发现它有着非常好的用户界面。对各种操作都有着非常好的响应,大家能够先去体验一下。以下给出几张网易云音乐的 UI。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
接下来放上酷云相相应的页面
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
说实话从開始接触自己定义 View 到如今也不久,学的不多,可是比較喜欢挑战,因此便有了这个项目的開始。好了,相信大家也已经体验过网易云的用户界面了。就算没有,接下来我也会一点点为你解答。
自己定义 SplashScreen 实现启动动画
/**
* Created by JimCharles on 2017/3/7.
*/
import android.app.Activity;
import android.app.Dialog;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import com.androxue.coolcloud.R;
public class SplashScreen {
public final static int SLIDE_LEFT = 1;
public final static int SLIDE_UP = 2;
public final static int FADE_OUT = 3;
private Dialog splashDialog;
private Activity activity;
public SplashScreen(Activity activity) {
this.activity = activity;
}
public void show(final int imageResource, final int animation) {
Runnable runnable = new Runnable() {
public void run() {
// Get reference to display
DisplayMetrics metrics = new DisplayMetrics();
// Display display = activity.getWindowManager().getDefaultDisplay();
// Create the layout for the dialog
LinearLayout root = new LinearLayout(activity);
root.setMinimumHeight(metrics.heightPixels);
root.setMinimumWidth(metrics.widthPixels);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(Color.BLACK);
root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
root.setBackgroundResource(imageResource);
// Create and show the dialog
splashDialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar);
// check to see if the splash screen should be full screen
if ((activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
== WindowManager.LayoutParams.FLAG_FULLSCREEN) {
splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
Window window = splashDialog.getWindow();
switch (animation) {
case SLIDE_LEFT:
window.setWindowAnimations(R.style.dialog_anim_slide_left);
break;
case SLIDE_UP:
window.setWindowAnimations(R.style.dialog_anim_slide_up);
break;
case FADE_OUT:
window.setWindowAnimations(R.style.dialog_anim_fade_out);
break;
}
splashDialog.setContentView(root);
splashDialog.setCancelable(false);
splashDialog.show();
// Set Runnable to remove splash screen just in case
/*final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
removeSplashScreen();
}
}, millis);*/
}
};
activity.runOnUiThread(runnable);
}
public void removeSplashScreen() {
if (splashDialog != null && splashDialog.isShowing()) {
splashDialog.dismiss();
splashDialog = null;
}
}
}
然后在 MianActivity 的onCreate()方法中加入以下两行代码就可以实现应用启动动画:
splashScreen = new SplashScreen(this);
splashScreen.show(R.drawable.art_login_bg,
SplashScreen.SLIDE_LEFT);
ViewPager+PagerTabStrip 实现滑动切屏
进去网易云后,主界面例如以下一个最大的特点就是滑动切屏了。这个效果既方便又帅气,所以网易云音乐中大量应用了这一效果,那么我们首先便来解说一下滑动切屏的实现。
首先构建在 activity_main.xml 中构建 ViewPager 和 PagerTabStrip。用来实现切屏效果,代码例如以下:
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:id="@+id/vp">
<android.support.v4.view.PagerTabStrip
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tap">
</android.support.v4.view.PagerTabStrip>
</android.support.v4.view.ViewPager>
然后构建切换的子 View,为了方便观察,这里仅仅是通过 ImageView 简单的设置显示不同的颜色,main_layout_1.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/view_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#76ff03"/>
</LinearLayout>
然后在 Activity 中进行设置,MainActivity.java
package com.androxue.coolcloud.activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.PagerTabStrip;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.androxue.coolcloud.R;
import java.util.ArrayList;
/**
* Created by JimCharles on 2017/3/11.
*/
public class MainActivity extends AppCompatActivity {
private ViewPager vp;
//声明存储ViewPager下子视图的集合
ArrayList<View> views = new ArrayList<>();
//显示效果中每个视图的标题
String[] titles={"私信", "评论", "@我", "通知"};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_message);
vp = (ViewPager) findViewById(R.id.vp);
initView();//调用初始化视图方法
vp.setAdapter(new MyAdapter());//设置适配器
ImageView back = (ImageView) findViewById(R.id.back);
back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MessageActivity.this, MainActivity.class);
startActivity(intent);
}
});
}
//初始化视图的方法(通过布局填充器获取用于滑动的视图并存入相应的的集合)
private void initView() {
View v1 = getLayoutInflater().inflate(R.layout.main_layout_1, null);
View v2 = getLayoutInflater().inflate(R.layout.main_layout_2, null);
View v3 = getLayoutInflater().inflate(R.layout.main_layout_3, null);
View v4 = getLayoutInflater().inflate(R.layout.main_layout_4, null);
views.add(v1);
views.add(v2);
views.add(v3);
views.add(v4);
PagerTabStrip pagerTabStrip= (PagerTabStrip)findViewById(R.id.tap);
pagerTabStrip.setDrawFullUnderline(false);//取消标题栏子View之间的切割线
pagerTabStrip.setTabIndicatorColor(Color.RED);//改变指示器颜色为红色
pagerTabStrip.setTextColor(Color.RED);//该变字体颜色为红色
}
private class MyAdapter extends PagerAdapter {
@Override
public int getCount() {
return views.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
//重写销毁滑动视图布局(将子视图移出视图存储集合(ViewGroup))
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(views.get(position));
}
//重写初始化滑动视图布局(从视图集合中取出相应视图,加入到ViewGroup)
@Override
public Object instantiateItem(ViewGroup container, int position) {
View v = views.get(position);
container.addView(v);
return v;
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}
}
好了。一个滑动切屏的功能就这样完毕了。大家能够自行执行体验一下。
圆形头像
关于圆形头像的实如今之前Android 自己定义 View 之 draw 原理分析一文已经有讲到,这里简单的解说一下,顺便解说还有一种实现方式。两种方式都是通过自己定义View控件来实现的。
方式一:Shader+onDraw() 实现圆形头像
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.girl);
int radius = bitmap.getWidth()/2;
BitmapShader bitmapShader = new BitmapShader(bitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
paint.setShader(bitmapShader);
canvas.translate(250,430);
canvas.drawCircle(radius, radius, radius, paint);
}
和之前讲到的一样,甚至没有改代码,利用 BitmapShader 并重写 onDraw() 方法来实现圆形头像,只是这里有一点须要注意的是图片的像素大小才干完美的实现圆形头像。能够自行进行尝试。
方式二:自己定义 CircleImageView 实现圆形头像
package com.androxue.coolcloud.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.util.AttributeSet;
import com.androxue.coolcloud.R;
/**
* Created by JimCharles on 2017/3/9.
*/
public class CircleImageView extends android.support.v7.widget.AppCompatImageView {
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int COLORDRAWABLE_DIMENSION = 2;
private static final int DEFAULT_BORDER_WIDTH = 0;
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;
private static final boolean DEFAULT_BORDER_OVERLAY = false;
private final RectF mDrawableRect = new RectF();
private final RectF mBorderRect = new RectF();
private final Matrix mShaderMatrix = new Matrix();
private final Paint mBitmapPaint = new Paint();
private final Paint mBorderPaint = new Paint();
private final Paint mFillPaint = new Paint();
private int mBorderColor = DEFAULT_BORDER_COLOR;
private int mBorderWidth = DEFAULT_BORDER_WIDTH;
private int mFillColor = DEFAULT_FILL_COLOR;
private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private int mBitmapWidth;
private int mBitmapHeight;
private float mDrawableRadius;
private float mBorderRadius;
private ColorFilter mColorFilter;
private boolean mReady;
private boolean mSetupPending;
private boolean mBorderOverlay;
private boolean mDisableCircularTransformation;
public CircleImageView(Context context) {
super(context);
init();
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);
a.recycle();
init();
}
private void init() {
super.setScaleType(SCALE_TYPE);
mReady = true;
if (mSetupPending) {
setup();
mSetupPending = false;
}
}
@Override
public ScaleType getScaleType() {
return SCALE_TYPE;
}
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType != SCALE_TYPE) {
throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
}
}
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (adjustViewBounds) {
throw new IllegalArgumentException("adjustViewBounds not supported.");
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mDisableCircularTransformation) {
super.onDraw(canvas);
return;
}
if (mBitmap == null) {
return;
}
if (mFillColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
}
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
setup();
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
setup();
}
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
super.setPaddingRelative(start, top, end, bottom);
setup();
}
public int getBorderColor() {
return mBorderColor;
}
public void setBorderColor(@ColorInt int borderColor) {
if (borderColor == mBorderColor) {
return;
}
mBorderColor = borderColor;
mBorderPaint.setColor(mBorderColor);
invalidate();
}
/**
* @deprecated Use {@link #setBorderColor(int)} instead
*/
@Deprecated
public void setBorderColorResource(@ColorRes int borderColorRes) {
setBorderColor(getContext().getResources().getColor(borderColorRes));
}
/**
* Return the color drawn behind the circle-shaped drawable.
*
* @return The color drawn behind the drawable
*
* @deprecated Fill color support is going to be removed in the future
*/
@Deprecated
public int getFillColor() {
return mFillColor;
}
/**
* Set a color to be drawn behind the circle-shaped drawable. Note that
* this has no effect if the drawable is opaque or no drawable is set.
*
* @param fillColor The color to be drawn behind the drawable
*
* @deprecated Fill color support is going to be removed in the future
*/
@Deprecated
public void setFillColor(@ColorInt int fillColor) {
if (fillColor == mFillColor) {
return;
}
mFillColor = fillColor;
mFillPaint.setColor(fillColor);
invalidate();
}
/**
* Set a color to be drawn behind the circle-shaped drawable. Note that
* this has no effect if the drawable is opaque or no drawable is set.
*
* @param fillColorRes The color resource to be resolved to a color and
* drawn behind the drawable
*
* @deprecated Fill color support is going to be removed in the future
*/
@Deprecated
public void setFillColorResource(@ColorRes int fillColorRes) {
setFillColor(getContext().getResources().getColor(fillColorRes));
}
public int getBorderWidth() {
return mBorderWidth;
}
public void setBorderWidth(int borderWidth) {
if (borderWidth == mBorderWidth) {
return;
}
mBorderWidth = borderWidth;
setup();
}
public boolean isBorderOverlay() {
return mBorderOverlay;
}
public void setBorderOverlay(boolean borderOverlay) {
if (borderOverlay == mBorderOverlay) {
return;
}
mBorderOverlay = borderOverlay;
setup();
}
public boolean isDisableCircularTransformation() {
return mDisableCircularTransformation;
}
public void setDisableCircularTransformation(boolean disableCircularTransformation) {
if (mDisableCircularTransformation == disableCircularTransformation) {
return;
}
mDisableCircularTransformation = disableCircularTransformation;
initializeBitmap();
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
initializeBitmap();
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
initializeBitmap();
}
@Override
public void setImageResource(@DrawableRes int resId) {
super.setImageResource(resId);
initializeBitmap();
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
initializeBitmap();
}
@Override
public void setColorFilter(ColorFilter cf) {
if (cf == mColorFilter) {
return;
}
mColorFilter = cf;
applyColorFilter();
invalidate();
}
@Override
public ColorFilter getColorFilter() {
return mColorFilter;
}
private void applyColorFilter() {
if (mBitmapPaint != null) {
mBitmapPaint.setColorFilter(mColorFilter);
}
}
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private void initializeBitmap() {
if (mDisableCircularTransformation) {
mBitmap = null;
} else {
mBitmap = getBitmapFromDrawable(getDrawable());
}
setup();
}
private void setup() {
if (!mReady) {
mSetupPending = true;
return;
}
if (getWidth() == 0 && getHeight() == 0) {
return;
}
if (mBitmap == null) {
invalidate();
return;
}
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
mFillPaint.setStyle(Paint.Style.FILL);
mFillPaint.setAntiAlias(true);
mFillPaint.setColor(mFillColor);
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
mBorderRect.set(calculateBounds());
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
mDrawableRect.set(mBorderRect);
if (!mBorderOverlay && mBorderWidth > 0) {
mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);
}
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
applyColorFilter();
updateShaderMatrix();
invalidate();
}
private RectF calculateBounds() {
int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int sideLength = Math.min(availableWidth, availableHeight);
float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
return new RectF(left, top, left + sideLength, top + sideLength);
}
private void updateShaderMatrix() {
float scale;
float dx = 0;
float dy = 0;
mShaderMatrix.set(null);
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
} else {
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
}
mShaderMatrix.setScale(scale, scale);
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
}
这样的方式是通过自己定义 View 来实现圆形头像的。代码非常easy
夜间模式
关于夜间模式的实现是在我开发过程中遇到的一个比較大坑,但我们都知道夜间模式是一款 APP 应该具备的,通过我近期的了解,总结出了四种实现方式。一是定义两套主题,对每个须要实现夜间模式的控件都另外定义一套夜间主题,但这样子project量大。我是一个人开发的。所以并没有採用这样的方式;二是利用 Google 官方提供的 UiModeManger;三是调用 NightModeHelper 实现夜间模式;四是利用 AppCompatDelegate 实现夜间模式。
Light_Style + Night_Style(即 setTheme)实现夜间模式
Light_Style + Night_Style 实现夜间模式的思路在于分别定义日间模式和夜间模式两套主题。然后再分别加入两套模式资源文件,也就是加入两个 module,生成 apk 文件。而模式的实现也就是对这两个 apk 文件进行处理,事实上也就是价格 .apk 后缀更改为 .skin 或者其他后缀,实现难度较大,不建议使用
UiModeManger 实现夜间模式()
UiModeManger 是谷歌官方给我们提供的实现夜间模式的方式,我们来看一下官方文档对它的描写叙述:
This class provides access to the system uimode services. These services allow applications to control UI modes of the device. It provides functionality to disable the car mode and it gives access to the night mode settings.
上面的意思就是:
这类提供对系统uimode服务的訪问。这些服务同意应用程序控制设备的UI模式。它提供了禁用汽车模式的功能,它同意訪问夜间模式设置。
看到这样的解释大家可能会有疑问。不是用来实现夜间模式的吗?怎么跑出了一个车载模式呢?细致看这个类的文档,发现有一个 setNightMode() 方法,这不就是设置夜间模式的意思吗?点进去看到这种方法的解释例如以下:
Sets the night mode. Changes to the night mode are only effective when the car or desk mode is enabled on a device.
意思例如以下:
设置夜间模式。对夜间模式的更改仅在设备上启用汽车或桌面模式时有效。
不得不说坑爹啊!
必须在开启车载模式或者桌面模式的条件下才干设置夜间模式,再细致看一下,发现UiModeManager 仅提供了方法设置车载模式的设置而没有Desk模式的设置方法,所以以下演示通过 UiModeManger 来实现夜间模式的方式。
首先在 res 包下创建 drawable-night-hdpi、drawable-night-mdpi、drawable-night-xhdpi、drawable-night-xxhdpi、drawable-night-xxxhdpi、values-night 目录,然后将相相应的资源放入新创建的相应的包下,并在 values-night 包下新建 colors.xml 资源文件,以下颜色仅供參考
<resources>
<color name="night_mode_color ">#7f7f7f</color>
<color name="night_mode_dark_color ">#d20000</color>
<color name="night_mode_color ">#0a0a0a</color>
</resources>
须要注意的一点是颜色名须要和 vales 包下的 colors.xml 文件里颜色名同样,但颜色值能够不同
然后在 values 包下的 styles.xml 文件里自己定义实现夜间模式的主题
<style name="Theme.Test" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/night_mode_color </item>
<item name="colorPrimaryDark">@color/night_mode_dark_color </item>
<item name="colorAccent">@color/night_mode_color </item>
<item name="android:windowBackground">@color/dark</item>
</style>
好了。这样我们就自己定义了夜间模式的主题了,接下来在须要实现夜间模式的地方加上一下代码就能够实现夜间模式了
UiModeManager uiManager = (UiModeManager) getSystemService(Context.UI_MODE_SERVICE);
if (isNightMode) {
uiManager.enableCarMode(0);
uiManager.setNightMode(UiModeManager.MODE_NIGHT_YES);
} else {
uiManager.disableCarMode(0);
uiManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
}
NightModeHelper 实现夜间模式()
import java.lang.ref.WeakReference;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.preference.PreferenceManager;
public class NightModeHelper {
private static final String PREF_KEY = "nightModeState";
private static int sUiNightMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
private WeakReference<Activity> mActivity;
private SharedPreferences mPrefs;
public NightModeHelper(Activity activity, int theme) {
int currentMode = (activity.getResources().getConfiguration()
.uiMode & Configuration.UI_MODE_NIGHT_MASK);
mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
init(activity, theme, mPrefs.getInt(PREF_KEY, currentMode));
}
public NightModeHelper(Activity activity, int theme, int defaultUiMode) {
init(activity, theme, defaultUiMode);
}
private void init(Activity activity, int theme, int defaultUiMode) {
mActivity = new WeakReference<Activity>(activity);
if(sUiNightMode == Configuration.UI_MODE_NIGHT_UNDEFINED){
sUiNightMode = defaultUiMode;
}
updateConfig(sUiNightMode);
}
private void updateConfig(int uiNightMode) {
Activity activity = mActivity.get();
if(activity == null){
throw new IllegalStateException("Activity went away?
");
}
Configuration newConfig = new Configuration(activity.getResources().getConfiguration());
newConfig.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
newConfig.uiMode |= uiNightMode;
activity.getResources().updateConfiguration(newConfig, null);
sUiNightMode = uiNightMode;
if(mPrefs != null){
mPrefs.edit()
.putInt(PREF_KEY, sUiNightMode)
.apply();
}
}
public static int getUiNightMode() {
return sUiNightMode;
}
public void toggle() {
if(sUiNightMode == Configuration.UI_MODE_NIGHT_YES){
notNight();
}
else{
night();
}
}
public void notNight() {
updateConfig(Configuration.UI_MODE_NIGHT_NO);
mActivity.get().recreate();
}
public void night() {
updateConfig(Configuration.UI_MODE_NIGHT_YES);
mActivity.get().recreate();
}
}
用这样的方式实现夜间模式仅仅须要定义一个NightModeHelper.java类,然后在须要实现夜间模式的Activity的onCreate()方法setContentView()方法hou加上以下一行代码就可以:
mNightModeHelper = new NightModeHelper(this, R.style.AppTheme_Light);
AppCompatDelegate 实现夜间模式
//夜间模式
SharedPreferences sp = this.getSharedPreferences("loonggg", MODE_PRIVATE);
boolean isNight = sp.getBoolean("night", false);
if (isNight) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
sp.edit().putBoolean("night", false).apply();
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
sp.edit().putBoolean("night", true).apply();
}
recreate();
广告轮播的自己定义实现
在大多数应用 APP 的主页面上都会出现广告轮播,或是推广自己的产品。或是为其他公司做广告推销,但不得不说。这样的插入也让整个 UI 界面的风格别有一番风味。以下我们来看一看网易云音乐的广告轮播的实现方式吧。
首先新建一个 Info.java 类用来保存图片的网址、设置的标题等信息并实现 set 和 get 方法,代码例如以下:
public class Info {
private String url;
private String title;
public Info(String title, String url) {
this.url = url;
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
然后自己定义轮播控件的布局。使用 ViewPager 来实现轮播效果,这里为了显示重叠的效果布局方式採用 RelativeLayout 即相对布局,然后嵌套两个 LinearLayout 分别用来存放指示器(在代码中动态加入)和显示标题。
详细代码例如以下:
<?
xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/cycle_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/cycle_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:gravity="center"
android:orientation="horizontal" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/cycle_indicator"
android:orientation="vertical">
<TextView
android:id="@+id/cycle_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>
</RelativeLayout>
上面说到要在代码中动态加入指示器,所以须要创建一个自己定义响应,新建 CycleViewPager.java 类
package com.androxue.coolcloud.widget;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.androxue.coolcloud.R;
import com.androxue.coolcloud.activity.MainActivity;
import com.androxue.coolcloud.info.Info;
import java.util.ArrayList;
import java.util.List;
/**
* Created by JimCharles on 2017/3/19.
*/
public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
private static final String TAG = "CycleViewPager";
private Context mContext;
private ViewPager mViewPager;//实现轮播图的ViewPager
private TextView mTitle;//标题
private LinearLayout mIndicatorLayout; // 指示器
private Handler handler;//每几秒后执行下一张的切换
private int WHEEL = 100; // 转动
private int WHEEL_WAIT = 101; // 等待
private List<View> mViews = new ArrayList<>(); //须要轮播的View。数量为轮播图数量+2
private ImageView[] mIndicators; //指示器小圆点
private boolean isScrolling = false; // 滚动框是否滚动着
private boolean isCycle = true; // 是否循环,默觉得true
private boolean isWheel = true; // 是否轮播,默觉得true
private int delay = 4000; // 默认轮播时间
private int mCurrentPosition = 0; // 轮播当前位置
private long releaseTime = 0; // 手指松开、页面不滚动时间,防止手机松开后短时间进行切换
private ViewPagerAdapter mAdapter;
private ImageCycleViewListener mImageCycleViewListener;
private List<Info> infos;//数据集合
private int mIndicatorSelected;//指示器图片,被选择状态
private int mIndicatorUnselected;//指示器图片,未被选择状态
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (mContext != null && isWheel) {
long now = System.currentTimeMillis();
// 检測上一次滑动时间与本次之间是否有触击(手滑动)操作,有的话等待下次轮播
if (now - releaseTime > delay - 500) {
handler.sendEmptyMessage(WHEEL);
} else {
handler.sendEmptyMessage(WHEEL_WAIT);
}
}
}
};
public CycleViewPager(Context context) {
this(context, null);
}
public CycleViewPager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CycleViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
initView();
}
/**
* 初始化View
*/
private void initView() {
LayoutInflater.from(mContext).inflate(R.layout.layout_circle_view, this, true);
mViewPager = (ViewPager) findViewById(R.id.cycle_view_pager);
mTitle = (TextView) findViewById(R.id.cycle_title);
mIndicatorLayout = (LinearLayout) findViewById(R.id.cycle_indicator);
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == WHEEL && mViews.size() > 0) {
if (!isScrolling) {
//当前为非滚动状态,切换到下一页
int posttion = (mCurrentPosition + 1) % mViews.size();
mViewPager.setCurrentItem(posttion, true);
}
releaseTime = System.currentTimeMillis();
handler.removeCallbacks(runnable);
handler.postDelayed(runnable, delay);
return;
}
if (msg.what == WHEEL_WAIT && mViews.size() > 0) {
handler.removeCallbacks(runnable);
handler.postDelayed(runnable, delay);
}
}
};
}
/**
* 设置指示器图片。在setData之前调用
* @param select 选中时的图片
* @param unselect 未选中时的图片
*/
public void setIndicators(int select, int unselect) {
mIndicatorSelected = select;
mIndicatorUnselected = unselect;
}
public void setData(List<Info> list, ImageCycleViewListener listener) {
setData(list, listener, 0);
}
/**
* 初始化viewpager
* @param list 要显示的数据
* @param showPosition 默认显示位置
*/
public void setData(List<Info> list, ImageCycleViewListener listener, int showPosition) {
if (list == null || list.size() == 0) {
//没有数据时隐藏整个布局
this.setVisibility(View.GONE);
return;
}
mViews.clear();
infos = list;
if (isCycle) {
//加入轮播图View。数量为集合数+2
// 将最后一个View加入进来
mViews.add(getImageView(mContext, infos.get(infos.size() - 1).getUrl()));
for (int i = 0; i < infos.size(); i++) {
mViews.add(getImageView(mContext, infos.get(i).getUrl()));
}
// 将第一个View加入进来
mViews.add(getImageView(mContext, infos.get(0).getUrl()));
} else {
//仅仅加入相应数量的View
for (int i = 0; i < infos.size(); i++) {
mViews.add(getImageView(mContext, infos.get(i).getUrl()));
}
}
if (mViews == null || mViews.size() == 0) {
//没有View时隐藏整个布局
this.setVisibility(View.GONE);
return;
}
mImageCycleViewListener = listener;
int ivSize = mViews.size();
// 设置指示器
mIndicators = new ImageView[ivSize];
if (isCycle)
mIndicators = new ImageView[ivSize - 2];
mIndicatorLayout.removeAllViews();
for (int i = 0; i < mIndicators.length; i++) {
mIndicators[i] = new ImageView(mContext);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
lp.setMargins(10, 0, 10, 0);
mIndicators[i].setLayoutParams(lp);
mIndicatorLayout.addView(mIndicators[i]);
}
mAdapter = new ViewPagerAdapter();
// 默认指向第一项。下方viewPager.setCurrentItem将触发又一次计算指示器指向
setIndicator(0);
mViewPager.setOffscreenPageLimit(3);
mViewPager.setOnPageChangeListener(this);
mViewPager.setAdapter(mAdapter);
if (showPosition < 0 || showPosition >= mViews.size())
showPosition = 0;
if (isCycle) {
showPosition = showPosition + 1;
}
mViewPager.setCurrentItem(showPosition);
setWheel(true);//设置轮播
}
/**
* 获取轮播图View
*/
private View getImageView(Context context, String url) {
return MainActivity.getImageView(context, url);
}
/**
* 设置指示器。和文字内容
* @param selectedPosition 默认指示器位置
*/
private void setIndicator(int selectedPosition) {
setText(mTitle, infos.get(selectedPosition).getTitle());
try {
for (int i = 0; i < mIndicators.length; i++) {
mIndicators[i]
.setBackgroundResource(mIndicatorUnselected);
}
if (mIndicators.length > selectedPosition)
mIndicators[selectedPosition]
.setBackgroundResource(mIndicatorSelected);
} catch (Exception e) {
Log.i(TAG, "指示器路径不对");
}
}
/**
* 页面适配器 返回相应的view
*/
private class ViewPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return mViews.size();
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public View instantiateItem(ViewGroup container, final int position) {
View v = mViews.get(position);
if (mImageCycleViewListener != null) {
v.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mImageCycleViewListener.onImageClick(infos.get(mCurrentPosition - 1), mCurrentPosition, v);
}
});
}
container.addView(v);
return v;
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int arg0) {
int max = mViews.size() - 1;
int position = arg0;
mCurrentPosition = arg0;
if (isCycle) {
if (arg0 == 0) {
//滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1
mCurrentPosition = max - 1;
} else if (arg0 == max) {
//滚动到mView的最后一个(界面上的第一个)。将mCurrentPosition设置为1
mCurrentPosition = 1;
}
position = mCurrentPosition - 1;
}
setIndicator(position);
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == 1) { // viewPager在滚动
isScrolling = true;
return;
} else if (state == 0) { // viewPager滚动结束
releaseTime = System.currentTimeMillis();
//跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化)
mViewPager.setCurrentItem(mCurrentPosition, false);
}
isScrolling = false;
}
/**
* 为textview设置文字
*/
public static void setText(TextView textView, String text) {
if (text != null && textView != null) textView.setText(text);
}
/**
* 为textview设置文字
*/
public static void setText(TextView textView, int text) {
if (textView != null) setText(textView, text + "");
}
/**
* 是否循环,默认开启。必须在setData前调用
*/
public void setCycle(boolean isCycle) {
this.isCycle = isCycle;
}
/**
* 是否处于循环状态
*/
public boolean isCycle() {
return isCycle;
}
/**
* 设置是否轮播。默认轮播,轮播一定是循环的
*/
public void setWheel(boolean isWheel) {
this.isWheel = isWheel;
isCycle = true;
if (isWheel) {
handler.postDelayed(runnable, delay);
}
}
/**
* 刷新数据。当外部视图更新后,通知刷新数据
*/
public void refreshData() {
if (mAdapter != null)
mAdapter.notifyDataSetChanged();
}
/**
* 是否处于轮播状态
*/
public boolean isWheel() {
return isWheel;
}
/**
* 设置轮播暂停时间,单位毫秒(默认4000毫秒)
* @param delay
*/
public void setDelay(int delay) {
this.delay = delay;
}
/**
* 轮播控件的监听事件
*/
public static interface ImageCycleViewListener {
/**
* 单击图片事件
*/
public void onImageClick(Info info, int position, View imageView);
}
}
然后再主页面中UI中引用自己定义的 CycleViewPager 控件并在 Activity 中进行相应操作就可以,详细代码例如以下:
<com.androxue.coolcloud.widget.CycleViewPager
android:id="@+id/cycle_view"
android:layout_width="395dp"
android:layout_height="160dp"
tools:layout_editor_absoluteY="0dp"
tools:layout_editor_absoluteX="8dp" />
public class MainActivity extends AppCompatActivity {
/**
* 模拟请求后得到的数据
*/
List<Info> mList = new ArrayList<>();
/**
* 轮播图
*/
CycleViewPager mCycleViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
/**
* 初始化数据
*/
private void initData() {
mList.add(new Info("标题1",
"http://img2.3lian.com/2014/c7/25/d/40.jpg"));
mList.add(new Info("标题2",
"http://img2.3lian.com/2014/c7/25/d/41.jpg"));
mList.add(new Info("标题3",
"http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg"));
mList.add(new Info("标题4",
"http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg"));
}
/**
* 初始化View
*/
private void initView() {
mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view);
//设置选中和未选中时的图片
mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect);
//设置轮播间隔时间
mCycleViewPager.setDelay(2000);
mCycleViewPager.setData(mList, mAdCycleViewListener);
}
/**
* 轮播图点击监听
*/
private CycleViewPager.ImageCycleViewListener mAdCycleViewListener =
new CycleViewPager.ImageCycleViewListener() {
@Override
public void onImageClick(Info info, int position, View imageView) {
if (mCycleViewPager.isCycle()) {
position = position - 1;
}
Toast.makeText(MainActivity.this, info.getTitle() +
"选择了--" + position, Toast.LENGTH_LONG).show();
}
};
}
至此广告轮播功能已经实现了,有兴趣的朋友自己码一下。
Android 实战之酷云(一)的更多相关文章
- 9套Android实战经典项目资料分享给大家
通过项目学习收获更大. 1.基于Android平台实战爱短信项目 下载地址:http://pan.baidu.com/s/1hr8CEry 2.Android平台实战CRM客户关系管理(AChartE ...
- Android实战技巧:深入解析AsyncTask
AsyncTask的介绍及基本使用方法 关于AsyncTask的介绍和基本使用方法可以参考官方文档和Android实战技巧:多线程AsyncTask这里就不重复. AsyncTask引发的一个问题 上 ...
- 【Android实战开发】3G技术和Android发展简介
随着移动设备的不断普及和发展,相关软件的开发也越来越受到人们的关注,其中要提及的就是Android开发.本系列博客主要为大家介绍Android的开发,可能会有人问:现在互联网上已经有很多的Androi ...
- 《Java并发编程实战》/童云兰译【PDF】下载
<Java并发编程实战>/童云兰译[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062521 内容简介 本书深入浅出地介绍了Jav ...
- 【Android实战】----从Retrofit源代码分析到Java网络编程以及HTTP权威指南想到的
一.简单介绍 接上一篇[Android实战]----基于Retrofit实现多图片/文件.图文上传中曾说非常想搞明确为什么Retrofit那么屌. 近期也看了一些其源代码分析的文章以及亲自查看了源代码 ...
- android实现超酷的腾讯视频首页和垂直水平网格瀑布流一揽子效果
代码地址如下:http://www.demodashi.com/demo/13381.html 先来一波demo截图 实现ListView.GridView.瀑布流 1.导入RecyclerView的 ...
- [置顶]
【Android实战】----从Retrofit源码分析到Java网络编程以及HTTP权威指南想到的
一.简介 接上一篇[Android实战]----基于Retrofit实现多图片/文件.图文上传中曾说非常想搞明白为什么Retrofit那么屌.最近也看了一些其源码分析的文章以及亲自查看了源码,发现其对 ...
- Android实战简易教程-第三十九枪(第三方短信验证平台Mob和验证码自己主动填入功能结合实例)
用户注冊或者找回password时通常会用到短信验证功能.这里我们使用第三方的短信平台进行验证实例. 我们用到第三方短信验证平台是Mob,地址为:http://mob.com/ 一.注冊用户.获取SD ...
- Android实战简易教程-第四十枪(窃听风云之短信监听)
近期在做监听验证码短信自己主动填入的功能,无意间想到了一个短信监听的办法. 免责声明:短信监听本身是一种违法行为,这里仅仅是技术描写叙述.请大家学习技术就可以.(哈哈) 本实例是基于bmob提供的后台 ...
随机推荐
- java util Aarrys 类
toString方法 产生一维数组的二维版本 public static String toString(type[] a) { //type 是传入数据类型 if (a == null) retu ...
- ORACLE分页查询SQL语法——高效的分页
--1:无ORDER BY排序的写法.(效率最高)--(经过测试,此方法成本最低,只嵌套一层,速度最快!即使查询的数据量再大,也几乎不受影响,速度依然!) SELECT * FROM (SELECT ...
- .NetCore下使用Prometheus实现系统监控和警报 (五)进阶自定义收集指标 之 Counter
Prometheus下面定了四种类型的收集方式,下面我们主要来来说下Counter的使用 Nuget导入Prometheus.AspNetCore包 下面先来看下我的Prometheus配置,这里我没 ...
- 【AtCoder】ARC082
C - Together 用一个数组记一下一个数给它本身,左右贡献都是1,看看哪个数的总贡献最大 #include <bits/stdc++.h> #define fi first #de ...
- zprofiler三板斧解决cpu占用率过高问题
zprofiler三板斧解决cpu占用率过高问题 九居 浏览 171 2015-04-08 14:11:58 发表于:JVM性能与调试平台 zprofiler 上周五碰到了一个线上机器cpu ...
- PHP 数组中取出随机取出指定数量子值集
#关键:array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组.#思路:先使用array_rand()随机取出所需数量键名,然后将这些键名指向 ...
- 【Java】 剑指offer(63) 股票的最大利润
本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖交易该 ...
- Nginx的配置详解
人无再少年,花有重开日——风城玫瑰 德里克·罗斯 Nginx是一款轻量级的HTTP服务器,采用事件驱动的异步非阻塞处理方式框架,这让其具有极好的IO性能,时常用于服务端的反向代理和负载均衡. Ngin ...
- go语言学习-接口
Go语言中虽然没有传统面向对象语言中类.集成的概念,不过提供了接口的支持,可以使用接口来使用一些面向对象的特性. 在 go 语言中,的接口有下面几个特点: 可以包含0个或多个方法的签名 只定义方法的签 ...
- Django models字段查询谓词表
谓词 含义 示例 等价SQL语句 exact 精确等于 Comment.objects.filter(id__exact=14) select * from Comment where id=14 i ...