Android 实战之酷云(一)
前言
大家好啊!好久没有见到我了吧。为什么呢!当然是由于开学啦,这学期非常多课,身为部长实验室也也非常多活动和一堆小师弟。同一时候还有蓝桥杯和华为软件开发大赛。并且近期在做一个综合性比較高的作品,没错了,就是酷云,一款仿网易云音乐的在线播放器。当然了,如今我还没有完毕这个作品,并且仅仅是刚刚開始写而已,只是也遇到了非常多坑。所以写下这篇文章来记录一下这次开发中各种各样的坑,希望对各位有所帮助。文章会随着我的开发进度而不定期更新,各位有问题也能够给我发私信,我们一起讨论解决。
至于为什么选择网易云音乐我觉得主要是由于它不仅有着 93% 的主流音乐版权,能够找到最齐全的主流音乐。出色的用户界面和体验。让用户感觉舒适。还有人性化的体验,如生日推荐音乐。
自己定义 View 控件
不知道各位有没有体验过网易云音乐 APP ,通过我近期的体验。发现它有着非常好的用户界面。对各种操作都有着非常好的响应,大家能够先去体验一下。以下给出几张网易云音乐的 UI。
接下来放上酷云相相应的页面
说实话从開始接触自己定义 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();
}
};
}
至此广告轮播功能已经实现了,有兴趣的朋友自己码一下。