基于android的网络音乐播放器-播放控制界面(九)
来源:互联网 发布:java 自定义类加载器 编辑:程序博客网 时间:2024/05/29 02:19
到这里我们的音乐播放器已经有了播放,收藏,搜索网络音乐并下载(包括多线程断点下载)等基本功能,下面将开发播放界面——实现音乐播放的控制-上/下一首,播放/暂停,循环控制,专辑图片的加载,歌词的加载和解析并支持滑动改变进度。这些实现主要在一个类里-PlayMusicActivity.java
package com.sprd.easymusic;import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.io.UnsupportedEncodingException;import java.lang.ref.SoftReference;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.net.URLEncoder;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import com.android.volley.RequestQueue;import com.android.volley.Response;import com.android.volley.VolleyError;import com.android.volley.Request.Method;import com.android.volley.toolbox.StringRequest;import com.android.volley.toolbox.Volley;import com.sprd.easymusic.fragment.NetFragment;import com.sprd.easymusic.myview.LrcView;import com.sprd.easymusic.service.MusicService;import com.sprd.easymusic.util.BitmapUtil;import com.sprd.easymusic.util.DownloadUtil;import com.sprd.easymusic.util.LrcLine;import com.sprd.easymusic.util.StringUtil;import android.app.ActionBar;import android.app.Activity;import android.app.Service;import android.content.BroadcastReceiver;import android.content.ComponentName;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.content.ServiceConnection;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.AsyncTask;import android.os.Bundle;import android.os.Environment;import android.os.Handler;import android.os.IBinder;import android.os.Message;import android.util.Log;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.View.OnClickListener;import android.view.animation.Animation;import android.view.animation.AnimationUtils;import android.widget.ImageView;import android.widget.ProgressBar;import android.widget.SeekBar;import android.widget.SeekBar.OnSeekBarChangeListener;import android.widget.TextView;import android.widget.Toast;public class PlayMusicActivity extends Activity implements MusicService.Watcher { private static final String TAG = "PlayMusicActivity"; /** * 发送音乐播放控制的广播给MianActivity,音乐的控制统一交给MainActivity管理 * 下一首,暂停,上一首,播放,继续播放 */ private static final String ACTION_NEXT_SONG = "action.nextsong"; private static final String ACTION_PAUSE = "action.pause"; private static final String ACTION_PRE_SONG = "action.presong"; private static final String ACTION_PLAY_SONG = "action.playsong"; private static final String ACTION_CONTINUE_PLAYING_SONG = "action.continueplaying"; /** * 接收广播,下载歌词完成,下载专辑图片完成,更新播放状态 */ private static final String ACTION_DOWNLOADLRC_SUCCESS = "action_downloadlrc_success"; private static final String ACTION_DOWNLOADPIC_SUCCESS = "action_downloadpic_success"; private static final String ACTION_UPDATE_PLAYSTATE = "action.update.playstate"; /** * 上一首,播放/暂停,下一首,专辑图片, 循环 */ private ImageView pre, playAndPause, next, albumPic, cycleView; /** * 音乐标题,歌手,时长,已播放时长 */ private TextView title, artist, duration, playedTimeView; //加载歌词 private TextView loadLrc; //歌词View--自定义View private LrcView lrcView; //进度条 private SeekBar seekBar; //表征当前播放状态 private boolean isPlaying; //表征暂停键是否按下,若为true则下次点击播放为继续播放,和isPlaying不冲突 private boolean pause; //音乐播放器后台服务 private MusicService musicService; private Context mContext; //当前播放音乐的标题和歌手和时长 private String currentMusicTitle, currentMusicArtist; private int currentMusicDuration; //播放界面专辑图片的动画-旋转 private Animation playAnimation; //当前播放的进度 private int currentProgress; private Handler myHandler; private static final int UPDATE_PROGRESS = 1; //添加网络请求队列 private RequestQueue mQueue; //搜索歌词的API public static final String lrcApi = "http://geci.me/api/lyric/"; //歌词下载的URL和网络响应 private String lrcUrl; private String lrcResponse; //缓存专辑图片,避免每次从本地或网络加载 private Map<String, SoftReference<Bitmap>> playImageCacheMap = new HashMap<String, SoftReference<Bitmap>>(); //当前歌词是否正在展示 private boolean showLrc = false; //循环控制切换对应的图片资源 private int[] cycleViewResource; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.play); mContext = this; //绑定服务并获得musicService的引用 bindToService(); cycleViewResource = new int[] {R.drawable.cycle_list, R.drawable.cycle_single, R.drawable.cycle_random}; pre = (ImageView) findViewById(R.id.pre); pre.setOnClickListener(musicClickListener); playAndPause = (ImageView) findViewById(R.id.playAndpause); playAndPause.setOnClickListener(musicClickListener); next = (ImageView) findViewById(R.id.next); next.setOnClickListener(musicClickListener); albumPic = (ImageView) findViewById(R.id.lrcpic); title = (TextView) findViewById(R.id.title); artist = (TextView) findViewById(R.id.artist); duration = (TextView) findViewById(R.id.duration); playedTimeView = (TextView) findViewById(R.id.playedtime); lrcView = (LrcView) findViewById(R.id.lrcview); loadLrc = (TextView) findViewById(R.id.loadlrc); loadLrc.setOnClickListener(musicClickListener); cycleView = (ImageView) findViewById(R.id.cycleview); cycleView.setOnClickListener(musicClickListener); mQueue = Volley.newRequestQueue(mContext); playAnimation = AnimationUtils.loadAnimation(mContext, R.anim.rotate); seekBar = (SeekBar) findViewById(R.id.musicProgress); seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) {//手动滑动进度条时的监听 changeProgressFromUser(progress);//通知后台musicService改变播放进度 //歌词信息跳转进度 lrcView.checkLrcTime(StringUtil.formatDuration(progress), progress - currentProgress); } } public void onStartTrackingTouch(SeekBar seekBar) { } public void onStopTrackingTouch(SeekBar seekBar) { } }); myHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case UPDATE_PROGRESS://更新播放进度 int progress = (Integer)msg.obj; seekBar.setProgress(progress); String playedTime = StringUtil.formatDuration(progress); playedTimeView.setText(playedTime); lrcView.checkLrcTime(playedTime, 1); Log.d(TAG, "update progress success!"); break; default: break; } } }; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_DOWNLOADLRC_SUCCESS); filter.addAction(ACTION_DOWNLOADPIC_SUCCESS); filter.addAction(ACTION_UPDATE_PLAYSTATE); mContext.registerReceiver(updateReceiver, filter); } // 绑定服务时的ServiceConnection参数 private ServiceConnection conn = new ServiceConnection() { // 绑定成功后该方法回调,并获得服务端IBinder的引用 public void onServiceConnected(ComponentName name, IBinder service) { // 通过获得的IBinder获取PlayMusicService的引用 musicService = ((MusicService.MusicBinder) service).getService(); musicService.addWatcher(PlayMusicActivity.this); //Toast.makeText(mContext, "onServiceConnected:musicService", Toast.LENGTH_LONG).show(); } @Override public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "onServiceDisconnected:musicService"); } }; // 绑定服务MusicService private void bindToService() { bindService(new Intent(mContext, com.sprd.easymusic.service.MusicService.class), conn, Service.BIND_AUTO_CREATE); } //后台播放服务改变播放进度 public void changeProgressFromUser(int progress) { musicService.changePlayProgress(progress); } protected void onStart() { updataPlayState(); super.onStart(); } protected void onDestroy() { musicService = null; this.unbindService(conn); this.unregisterReceiver(updateReceiver); super.onDestroy(); } //更新播放状态 private void updataPlayState() { Log.d(TAG, "updataPlayState"); currentMusicTitle = MainActivity.currentMusicTitle; currentMusicArtist = MainActivity.currentMusicArtist; currentMusicDuration = (int) MainActivity.currentMusicDuration; isPlaying = MainActivity.isPlaying; pause = MainActivity.pause; seekBar.setMax((int)currentMusicDuration); title.setText(currentMusicTitle); artist.setText(currentMusicArtist); duration.setText(StringUtil.formatDuration(currentMusicDuration)); if (isPlaying) { playAndPause.setImageResource(android.R.drawable.ic_media_pause); albumPic.startAnimation(playAnimation); } else { playAndPause.setImageResource(android.R.drawable.ic_media_play); albumPic.clearAnimation(); } showAlbumPic(); showLrc(); } //控制音乐的播放转交给MianActivity private void changeMusicState(Intent intent) { Log.d(TAG, "changeMusicState:action = " + intent.getAction()); this.sendBroadcast(intent); } private OnClickListener musicClickListener = new OnClickListener() { public void onClick(View v) { if (v == pre) {//上一首 isPlaying = true; pause = false; showLrc = false; Intent intent = new Intent(ACTION_PRE_SONG); changeMusicState(intent); } else if (v == playAndPause) {//播放或者暂停 if (pause) {//如果点击过暂停,下一次点击播放应当是继续播放 isPlaying = true; pause = false; Intent intent = new Intent(ACTION_CONTINUE_PLAYING_SONG); changeMusicState(intent); return; } if (isPlaying) {//当前播放状态点击暂停 isPlaying = false; pause = true; Intent intent = new Intent(ACTION_PAUSE); changeMusicState(intent); } else {//首次点击播放键,此时isPlaying和pause均为false isPlaying = true; Intent intent = new Intent(ACTION_PLAY_SONG); changeMusicState(intent); } } else if (v == next) {//下一首 isPlaying = true; pause = false; showLrc = false; Intent intent = new Intent(ACTION_NEXT_SONG); changeMusicState(intent); } else if (v == loadLrc) {//加载歌词 showLrc = true; String localUrl = getLocalPath(0); File file = new File( localUrl); if (file.exists()) { showLrc(); return; } searchLrc(currentMusicTitle, currentMusicArtist); } else if (v == cycleView) {//切换循环模式 MainActivity.cycle += 1; if (MainActivity.cycle > 3) MainActivity.cycle = 1; cycleView.setImageResource(cycleViewResource[MainActivity.cycle-1]); } } }; //搜索歌词 private void searchLrc(String title, String artist) { String searchTitle = null; String searchArtist = null; Log.d(TAG, currentMusicTitle + currentMusicArtist); try { searchTitle = URLEncoder.encode(StringUtil.removeReg(currentMusicTitle, null), "UTF-8"); searchArtist = URLEncoder.encode(StringUtil.removeReg(currentMusicArtist, null), "UTF-8"); Log.d(TAG, searchTitle + searchArtist); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String url = lrcApi + searchTitle + "/" + searchArtist; if (searchTitle.indexOf("+")>=0 || searchArtist.indexOf("+")>=0) { url = url.replace('+', ' '); } Log.d(TAG, "url = " + url ); StringRequest stringRequest = new StringRequest(Method.GET,url, new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d(TAG, "response = " + response); //搜索歌词得到的响应 lrcResponse = response; analysisLrcUrl(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e(TAG, error.getMessage(), error); Toast.makeText(PlayMusicActivity.this, "加载失败!", 300).show(); } }); mQueue.add(stringRequest); } //搜索专辑图片 private void searchAlbumPic(String title, String artist) { Toast.makeText(mContext, "搜索专辑图片...", 300).show(); //有些歌名和歌手信息中可能会携带些非原始的特殊符号需要去掉,比如《》.,之类的 String ti = StringUtil.removeReg(title, null); String ar = StringUtil.removeReg(artist, null); Log.d(TAG, "searchAlbumPic:ti = " + ti + " ar = " + ar); //启动查询歌词的异步任务 new NetFragment().new SearchMusicTask(ti, ar, mContext).execute(NetFragment.getRealUrl(title)); } //从响应中分离出歌词下载的URL private void analysisLrcUrl() { try { JSONObject jo = new JSONObject(lrcResponse); JSONArray result = jo.getJSONArray("result"); JSONObject firstResult = result.getJSONObject(0); String lrc = firstResult.getString("lrc"); lrcUrl = lrc; downloadLrc(lrcUrl); Log.d(TAG, "lrc = " + lrc); } catch (JSONException e) { e.printStackTrace(); Toast.makeText(PlayMusicActivity.this, "抱歉,未搜索到歌词!", 300).show(); } } //下载歌词 private void downloadLrc(String lrcUrl) { final String musicLrc = lrcUrl; Toast.makeText(mContext, "正在下载歌词,请稍候...", 300).show(); String localFile = getLocalPath(0); File file = new File(localFile); //开启一个线程下载歌词 DownloadUtil lrcUtil = new DownloadUtil(musicLrc, localFile, mContext, 1); musicService.downloadMusic(lrcUtil); } //下载专辑图片 public void downloadAlbumPic(String albumPicUrl) { if (albumPicUrl == null) { Toast.makeText(mContext, "未搜索到匹配图片", 300).show(); albumPic.setImageResource(R.drawable.rotate); return; } final String musicAlbumPic = albumPicUrl; //Toast.makeText(mContext, "正在下载专辑图片,请稍候...", 300).show(); String localFile = getLocalPath(1); File file = new File(localFile); //开启两个线程下载专辑图片 DownloadUtil albumPicUtil = new DownloadUtil(musicAlbumPic, localFile, mContext, 2); musicService.downloadMusic(albumPicUtil); } //显示歌词 protected void showLrc() { if (!showLrc) { lrcView.setLrcList(null); lrcView.setCurrentLrcIndex(0); lrcView.setVisibility(View.GONE); loadLrc.setVisibility(View.VISIBLE); return; } String localFile = getLocalPath(0); File file = new File(localFile); if (!file.exists()) return; //歌词的解析只需传进去一个歌词文件路径,自定义view-lrcView内部已经封装好 lrcView.loadLrc(localFile); loadLrc.setVisibility(View.INVISIBLE); } //显示专辑图片 private void showAlbumPic() { //首先从缓存中查询是否已加载有该专辑图片 if (playImageCacheMap != null) { Log.d(TAG, "playImageCacheMap.size = " + playImageCacheMap.size()); SoftReference<Bitmap> sBitmap = playImageCacheMap.get(currentMusicTitle+currentMusicArtist); if (sBitmap != null) { Bitmap bitmap = sBitmap.get(); if (bitmap != null) { albumPic.setImageBitmap(bitmap); Log.d(TAG, "get image from ImageCacheMap"); return; } else { playImageCacheMap.remove(currentMusicTitle+currentMusicArtist); } } } //如果缓存中还未存入该专辑图片或者已经被回收,从本地加载并添加到缓存 final String localFile = getLocalPath(1); File file = new File(localFile); if (file.exists()) { myHandler.post(new Runnable() { public void run() { Bitmap bitmap = BitmapFactory.decodeFile(localFile); //int size = Math.min(lrcPic.getWidth(), lrcPic.getHeight()); //Bitmap bitmap = BitmapUtil.getScropBitmap(localFile, size, size); playImageCacheMap.put(currentMusicTitle + currentMusicArtist, new SoftReference<Bitmap>(bitmap)); Log.d(TAG, "add bitmap to ImageCacheMap, bytes = " + bitmap.getByteCount()); albumPic.setImageBitmap(bitmap); } }); return; } //如果缓存和本地均没有专辑图片则从网络中搜索并加载 searchAlbumPic(currentMusicTitle, currentMusicArtist); } //获取本地文件路径,type为0表示歌词路径,为1表示专辑图片路径 private String getLocalPath(int type) { String result = null; MainActivity.createFileDir(); if (type == 0) { result = MainActivity.downloadedPath + "/lrc/"+ currentMusicTitle + "-" + currentMusicArtist + ".lrc"; } else { result = MainActivity.downloadedPath + "/album/"+ currentMusicTitle + "-" + currentMusicArtist + ".jpg"; } return result; } @Override public void update(int currentProgress) { this.currentProgress = currentProgress; myHandler.sendMessage(myHandler.obtainMessage(UPDATE_PROGRESS, currentProgress)); } private BroadcastReceiver updateReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ACTION_DOWNLOADLRC_SUCCESS)) { Toast.makeText(mContext, "下载歌词成功!", 100).show(); showLrc(); } else if (intent.getAction().equals(ACTION_DOWNLOADPIC_SUCCESS)) { Toast.makeText(mContext, "下载专辑图片成功!", 100).show(); showAlbumPic(); } else if (intent.getAction().equals(ACTION_UPDATE_PLAYSTATE)) { boolean autoChange = intent.getBooleanExtra("autoChange", false); if (autoChange) showLrc = !autoChange; updataPlayState(); } } };}
其中StringUtil是字符串处理工具类:
package com.sprd.easymusic.util;import android.media.MediaPlayer;public class StringUtil { // 将音乐时长-毫秒转换为00:00格式 public static String formatDuration(long dur) { long totalSecond = dur / 1000; String minute = totalSecond / 60 + ""; if (minute.length() < 2) minute = "0" + minute; String second = totalSecond % 60 + ""; if (second.length() < 2) second = "0" + second; return minute + ":" + second; } public static String removeReg(String source, String reg) { if (reg!= null) { return source.replaceAll(reg, ""); } //保留中文和英文字符 return source.replaceAll("[^a-zA-Z \u4e00-\u9fa5]", ""); } public static String getMusicDuration(String url) { int duration = 0; MediaPlayer mp = null; try { mp = new MediaPlayer(); mp.reset(); mp.setDataSource(url); mp.prepare(); duration = mp.getDuration(); } catch (Exception e) { e.printStackTrace(); } finally { mp.release(); } return formatDuration(duration); } public static long getMusicLongDuration(String url) { long duration = 0; MediaPlayer mp = null; try { mp = new MediaPlayer(); mp.reset(); mp.setDataSource(url); mp.prepare(); duration = mp.getDuration(); } catch (Exception e) { e.printStackTrace(); } finally { mp.release(); } return duration; }}
搜索专辑图片的异步任务与网络音乐搜索的异步任务是同一个,在前面的章节中可以看到搜索歌曲时有保存专辑图片,只是没有拿来使用。
PlayMusicActivity.java的布局很简单,如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/playlayout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/myshape" android:gravity="top" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="2" android:gravity="center" android:orientation="vertical" > <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="30dp" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#ff0000" /> <TextView android:id="@+id/artist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="#00ff00" /> </LinearLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="20" > <!--<ImageView android:id="@+id/lrcpic" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="20dp" android:src="@drawable/rotate" /> --> <com.sprd.easymusic.myview.CircleImageView android:id="@+id/lrcpic" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="20dp" android:alpha="0.6" android:src="@drawable/rotate"/> <com.sprd.easymusic.myview.LrcView android:id="@+id/lrcview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="20dp" android:alpha="1.0" android:visibility="gone"/> </RelativeLayout> <TextView android:id="@+id/loadlrc" android:layout_width="match_parent" android:layout_height="0dp" android:layout_gravity="center" android:layout_marginBottom="10dp" android:layout_weight="4" android:gravity="center" android:text="查看歌词" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="#ff00ff" /> <SeekBar android:id="@+id/musicProgress" android:layout_width="match_parent" android:layout_height="30dp" android:layout_marginBottom="10dp" /> <LinearLayout android:id="@+id/seekbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:orientation="horizontal" > <TextView android:id="@+id/playedtime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dp" android:layout_weight="4" android:text="00:00" android:textColor="#ffffff" /> <TextView android:id="@+id/duration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="4" android:gravity="right" android:textColor="#ffffff" /> </LinearLayout> <LinearLayout android:id="@+id/controlarea" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="30dp" android:layout_weight="5" android:gravity="center" android:orientation="horizontal" > <ImageView android:id="@+id/pre" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginLeft="30dp" android:src="@drawable/pre" /> <ImageView android:id="@+id/playAndpause" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginLeft="50dp" android:background="#ffffff" android:src="@drawable/play" /> <ImageView android:id="@+id/next" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginLeft="50dp" android:src="@drawable/next" /> <ImageView android:id="@+id/cycleview" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginLeft="50dp" android:src="@drawable/cycle_list" /> </LinearLayout></LinearLayout>
这是一个线性布局,从各个view的id就可以看出其作用了,这里也不多做解释了。
大概界面预览图如下(专辑图片eclipse显示有误):
音乐播放器已完成,下载地址:
Android音乐播放器
0 0
- 基于android的网络音乐播放器-播放控制界面(九)
- 基于android的网络音乐播放器-通知栏控制(RemoteViews)(十)
- 基于android的网络音乐播放器-回调实现音乐播放及音乐收藏的实现(三)
- 基于android的网络音乐播放器-播放音乐及收藏音乐的效果展示(四)
- 基于android的网络音乐播放器-网络音乐的搜索和展示(五)
- 基于android的网络音乐播放器-网络音乐的多线程下载(六)
- 基于android的网络音乐播放器-添加viewpager和fragment实现滑动切换多个界面(二)
- 基于android的网络音乐播放器-下载完成后下拉音乐列表刷新(八)
- Android本地及网络音乐播放器-播放界面显示(二)
- 基于Android的音乐播放器项目
- 基于android的音乐播放器
- 基于Android的音乐播放器
- Android开发本地及网络Mp3音乐播放器(六)实现独立音乐播放界面
- android-----音乐播放器的音量控制功能(开发)
- 播放网络音乐的播放器
- Android音乐播放器---实现Notification控制音乐播放
- Android音乐播放器---实现Notification控制音乐播放
- Android音乐播放器---实现Notification控制音乐播放
- 一周热议新闻20170508
- 图形界面必知道的一些东西1
- java 字符串判断是否是float或者int型
- vue-cli开发环境跨域问题解决方案
- python调用texturepacker命令行处理图片
- 基于android的网络音乐播放器-播放控制界面(九)
- ubuntu14.04 elastic stack部署
- docker学习笔记04:Centos7使用阿里云镜像加速
- markdownpad2注册码
- iOS开发中集成Udesk的问题
- Socket 详解
- ggplot2画中国地图
- 机器学习笔记7——核
- 最小距离法