前言

大家好啊!好久没有见到我了吧。为什么呢!当然是由于开学啦,这学期非常多课,身为部长实验室也也非常多活动和一堆小师弟。同一时候还有蓝桥杯和华为软件开发大赛。并且近期在做一个综合性比較高的作品,没错了,就是酷云,一款仿网易云音乐的在线播放器。当然了,如今我还没有完毕这个作品,并且仅仅是刚刚開始写而已,只是也遇到了非常多坑。所以写下这篇文章来记录一下这次开发中各种各样的坑,希望对各位有所帮助。文章会随着我的开发进度而不定期更新,各位有问题也能够给我发私信,我们一起讨论解决。

至于为什么选择网易云音乐我觉得主要是由于它不仅有着 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 displayDisplayMetrics metrics = new DisplayMetrics();
//                Display display = activity.getWindowManager().getDefaultDisplay();// Create the layout for the dialogLinearLayout 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 dialogsplashDialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar);// check to see if the splash screen should be full screenif ((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.ViewPagerandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center"android:gravity="center"android:id="@+id/vp"><android.support.v4.view.PagerTabStripandroid: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={"私信", "评论", "@我", "通知"};@Overrideprotected 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() {@Overridepublic 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 {@Overridepublic int getCount() {return views.size();}@Overridepublic boolean isViewFromObject(View view, Object object) {return view == object;}//重写销毁滑动视图布局(将子视图移出视图存储集合(ViewGroup))@Overridepublic void destroyItem(ViewGroup container, int position, Object object) {container.removeView(views.get(position));}//重写初始化滑动视图布局(从视图集合中取出相应视图,加入到ViewGroup)@Overridepublic Object instantiateItem(ViewGroup container, int position) {View v = views.get(position);container.addView(v);return v;}@Overridepublic 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;}}@Overridepublic ScaleType getScaleType() {return SCALE_TYPE;}@Overridepublic void setScaleType(ScaleType scaleType) {if (scaleType != SCALE_TYPE) {throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));}}@Overridepublic void setAdjustViewBounds(boolean adjustViewBounds) {if (adjustViewBounds) {throw new IllegalArgumentException("adjustViewBounds not supported.");}}@Overrideprotected 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);}}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);setup();}@Overridepublic void setPadding(int left, int top, int right, int bottom) {super.setPadding(left, top, right, bottom);setup();}@Overridepublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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*/@Deprecatedpublic 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();}@Overridepublic void setImageBitmap(Bitmap bm) {super.setImageBitmap(bm);initializeBitmap();}@Overridepublic void setImageDrawable(Drawable drawable) {super.setImageDrawable(drawable);initializeBitmap();}@Overridepublic void setImageResource(@DrawableRes int resId) {super.setImageResource(resId);initializeBitmap();}@Overridepublic void setImageURI(Uri uri) {super.setImageURI(uri);initializeBitmap();}@Overridepublic void setColorFilter(ColorFilter cf) {if (cf == mColorFilter) {return;}mColorFilter = cf;applyColorFilter();invalidate();}@Overridepublic 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;//实现轮播图的ViewPagerprivate TextView mTitle;//标题private LinearLayout mIndicatorLayout; // 指示器private Handler handler;//每几秒后执行下一张的切换private int WHEEL = 100; // 转动private int WHEEL_WAIT = 101; // 等待private List<View> mViews = new ArrayList<>(); //须要轮播的View。数量为轮播图数量+2private ImageView[] mIndicators;    //指示器小圆点private boolean isScrolling = false; // 滚动框是否滚动着private boolean isCycle = true; // 是否循环,默觉得trueprivate boolean isWheel = true; // 是否轮播,默觉得trueprivate 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() {@Overridepublic 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() {@Overridepublic 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 {//仅仅加入相应数量的Viewfor (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 {@Overridepublic int getCount() {return mViews.size();}@Overridepublic boolean isViewFromObject(View arg0, Object arg1) {return arg0 == arg1;}@Overridepublic void destroyItem(ViewGroup container, int position, Object object) {container.removeView((View) object);}@Overridepublic View instantiateItem(ViewGroup container, final int position) {View v = mViews.get(position);if (mImageCycleViewListener != null) {v.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {mImageCycleViewListener.onImageClick(infos.get(mCurrentPosition - 1), mCurrentPosition, v);}});}container.addView(v);return v;}@Overridepublic int getItemPosition(Object object) {return POSITION_NONE;}}@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}@Overridepublic void onPageSelected(int arg0) {int max = mViews.size() - 1;int position = arg0;mCurrentPosition = arg0;if (isCycle) {if (arg0 == 0) {//滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1mCurrentPosition = max - 1;} else if (arg0 == max) {//滚动到mView的最后一个(界面上的第一个)。将mCurrentPosition设置为1mCurrentPosition = 1;}position = mCurrentPosition - 1;}setIndicator(position);}@Overridepublic 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.CycleViewPagerandroid: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;@Overrideprotected 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() {@Overridepublic 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();}};
}

至此广告轮播功能已经实现了,有兴趣的朋友自己码一下。


转载于:https://www.cnblogs.com/gccbuaa/p/7400247.html

Android 实战之酷云(一)相关推荐

  1. Android 实战之酷云(二)

    对于酷云这个项目,想了很久,到底是做到最好,还是能用就好,在我看来,完成这个项目的难度还是有的,加上其他限制,导致进度缓慢,不过我觉得还是要将它呈现出来,所以决定写下酷云开发过程的一系列文章,希望能和 ...

  2. 酷派android最新版本,酷云手机版下载

    软件标签: 酷云酷派 功能介绍 1.手机端和云端数据同步 联系人.日程.记事本.书签.短信.通话记录.图片......一键备份至云端! 2.多层加密,安全无忧 荣获国家信息产业部"最安全云存 ...

  3. 安卓学习专栏——实战项目酷欧天气(4)给天气页面加上背景图片

    步骤 系列文章 前言 实现效果 项目结构 1.获取必应每日一图 1.1修改修改activity_weather.xml 1.2修改WeatherActivity 2.背景图和状态栏效果修改 2.1修改 ...

  4. android平板车载,酷狗发布Android Pad版 抢滩车载及平板应用市场

    继成功登陆Android手机和苹果手机之后,酷狗音乐隆重推出了其首款为Android平板电脑及车载设备所设计的Android Pad 应用软件.据悉,这是一款针对三星.联想等众多知名品牌推出的安卓平板 ...

  5. 安卓学习专栏——实战项目酷欧天气(2)遍历全国省市县数据

    步骤 系列文章 前言 1.实现效果 2.项目结构 util包 util包下新建HttpUtil util包下新建Utility 3.新建choose_area.xml布局 4.新建ChooseArea ...

  6. Android实战项目: 视频资讯APP,源码在文章里免费分享

    文章目录 一.环境搭建 1.资源下载 2.视频地址 3.项目开发情况 二.项目展示 1.首页 2.资讯页面 3.个人中心 4.收藏页面实现 三.配置过程详解 1.导入Mysql数据库 2.Mongdb ...

  7. 视频教程-Ruby on Rails打造企业级RESTful API项目实战我的云音乐-Ruby/Rails

    Ruby on Rails打造企业级RESTful API项目实战我的云音乐 任苹蜻,爱学啊创始人 & CEO,曾就职于某二车手公司担任Android工程师后离职创办爱学啊,我们的宗旨是:人生 ...

  8. Rancher被Gartner评为“四大最酷云基础设施供应商”之一!

    随着越来越多的企业开始采用将本地.非本地.云端相结合的部署方式,正确选择云基础设施供应商变得更加关键.世界领先的信息技术研究和咨询公司Gartner,于本月公布了其对世界范围内各大云基础设施供应商的分 ...

  9. 国内首个面向工业级实战的点云处理课程

    01 背景介绍 计算机视觉的最终体现是三维视觉,而三维视觉的表达方式则是点云,点云处理在整个三维视觉领域占有非常重要的地位,几乎涉及到所有相关领域,例如自动驾驶感知定位.SLAM.三维场景重建.AR/ ...

最新文章

  1. 不甘心只做输入工具,搜狗输入法上线AI助手,提供智能服务
  2. SAP Batch Management 批次主数据中classification视图中GR Date没有被更新?
  3. java.lang.ClassNotFoundException: Didn't find class com.tzutalin.dlibtest.MainActivity_
  4. D3DPOOL(资源池)
  5. IntelliJ IDEA 超实用技巧分享,不能再全了!
  6. 视频分类/动作识别数据库研究现状
  7. android 蓝牙设置平板电脑,java – BlueCove,笔记本电脑和带蓝牙的Android平板电脑
  8. 【NOIP 2017】列队
  9. 大数高精加减乘除(洛谷P1601、P2142、P1303、P1480题题解,Java语言描述)
  10. 造轮子是什么意思_聊聊在阿里工作一年的收获,什么是真正的技术能力?
  11. 数据中心操作人员:艰难地在针对VM构建的基础设施上运行容器
  12. mysql test 映射到实体_第80天:Python 操作 MySQL
  13. 2011华为上机试题-Java
  14. 软件评测师32小时-第一小时 软件测试概论
  15. Java 接入 cachecloud 入门
  16. Swift-字符串和字符
  17. 没有CUE的情况下APE刻录CD
  18. 基于stm32的绘图机器人设计
  19. 高性能Java模板引擎BSL-1.0.1发布
  20. 临近下班又开会,熬夜加班写纪要,语音转写还收费……

热门文章

  1. SetCapture, ReleaseCapture, GetCapture
  2. pytorch实现多项式回归
  3. 在华为MateBook Ego的arm windows 11上安装hyper-V虚拟机
  4. prototype 和 proto 的区别
  5. 实现用户手机流量统计(ReduceTask并行度控制)
  6. [学习]uni-app监听原生标题页面跳转
  7. 对于jsonp的理解
  8. Unity中手动设置纵横比
  9. 数据库安全性与完整性
  10. Android入门教程二之开发环境搭建