百度地图--android接入百度地图导航

当前项目 Debug 签名信息:

  • SHA1: XXXXXX
  • 包名:com.example.baidunavidemo

在百度地图开放平台申请 Android AK 时填写这两个值即可。

申请到 AK 后,在项目根目录 gradle.properties 中添加:

复制代码
BAIDU_MAP_AK=你的百度地图AK

package com.example.baidunavidemo;

import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.widget.AutoCompleteTextView;
import android.widget.Toast;

import com.baidu.mapapi.search.sug.OnGetSuggestionResultListener;
import com.baidu.mapapi.search.sug.SuggestionResult;
import com.baidu.mapapi.search.sug.SuggestionSearch;
import com.baidu.mapapi.search.sug.SuggestionSearchOption;

import java.util.ArrayList;
import java.util.List;

final class AddressSearchController {

    interface CityProvider {
        String getCity();
    }

    interface SelectionListener {
        void onSelectionChanged(SuggestionItem item);
    }

    private final AutoCompleteTextView input;
    private final SuggestionAdapter adapter;
    private final SuggestionSearch search;
    private final CityProvider cityProvider;
    private final SelectionListener listener;
    private final Handler handler = new Handler(Looper.getMainLooper());
    private Runnable pendingSearch;
    private boolean internalTextChange;
    private SuggestionItem selectedItem;

    AddressSearchController(AutoCompleteTextView input,
                            CityProvider cityProvider,
                            SelectionListener listener) {
        this.input = input;
        this.cityProvider = cityProvider;
        this.listener = listener;
        this.adapter = new SuggestionAdapter(input.getContext());
        this.input.setAdapter(adapter);
        this.search = SuggestionSearch.newInstance();
        this.search.setOnGetSuggestionResultListener(createResultListener());
        bindInput();
    }

    SuggestionItem getSelectedItem() {
        return selectedItem;
    }

    private void bindInput() {
        input.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) { }

            @Override
            public void afterTextChanged(Editable editable) {
                if (internalTextChange) {
                    return;
                }
                selectedItem = null;
                listener.onSelectionChanged(null);
                if (pendingSearch != null) {
                    handler.removeCallbacks(pendingSearch);
                }
                String keyword = editable.toString().trim();
                if (keyword.isEmpty()) {
                    adapter.replaceAll(new ArrayList<>());
                    input.dismissDropDown();
                    return;
                }
                pendingSearch = () -> requestSuggestions(keyword);
                handler.postDelayed(pendingSearch, 350L);
            }
        });

        input.setOnItemClickListener((parent, view, position, id) -> {
            SuggestionItem item = adapter.getItem(position);
            if (item.point == null) {
                Toast.makeText(input.getContext(), "该地址缺少坐标,请选择其他结果", Toast.LENGTH_SHORT).show();
                return;
            }
            selectedItem = item;
            internalTextChange = true;
            input.setText(item.title);
            input.setSelection(input.length());
            internalTextChange = false;
            listener.onSelectionChanged(item);
        });
    }

    private void requestSuggestions(String keyword) {
        String city = cityProvider.getCity();
        if (TextUtils.isEmpty(city)) {
            city = "全国";
        }
        search.requestSuggestion(new SuggestionSearchOption()
                .keyword(keyword)
                .city(city)
                .citylimit(false));
    }

    private OnGetSuggestionResultListener createResultListener() {
        return result -> {
            List<SuggestionItem> suggestions = new ArrayList<>();
            if (result != null && result.getAllSuggestions() != null) {
                for (SuggestionResult.SuggestionInfo info : result.getAllSuggestions()) {
                    if (info == null || TextUtils.isEmpty(info.getKey())) {
                        continue;
                    }
                    String subtitle = joinAddress(info.getCity(), info.getDistrict(), info.getAddress());
                    suggestions.add(new SuggestionItem(info.getKey(), subtitle, info.getPt()));
                }
            }
            adapter.replaceAll(suggestions);
            if (!suggestions.isEmpty() && input.hasFocus()) {
                input.showDropDown();
            }
        };
    }

    private String joinAddress(String city, String district, String address) {
        StringBuilder value = new StringBuilder();
        appendPart(value, city);
        appendPart(value, district);
        appendPart(value, address);
        return value.toString();
    }

    private void appendPart(StringBuilder builder, String value) {
        if (TextUtils.isEmpty(value)) {
            return;
        }
        if (builder.length() > 0 && builder.indexOf(value) < 0) {
            builder.append(" · ");
        }
        if (builder.indexOf(value) < 0) {
            builder.append(value);
        }
    }

    void destroy() {
        if (pendingSearch != null) {
            handler.removeCallbacks(pendingSearch);
        }
        search.destroy();
    }
}

package com.example.baidunavidemo;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.view.inputmethod.InputMethodManager;
import android.content.Context;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.baidu.navisdk.adapter.BNRoutePlanNode;
import com.baidu.navisdk.adapter.BaiduNaviManagerFactory;
import com.baidu.navisdk.adapter.IBNRoutePlanManager;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputEditText;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_NAV_PERMISSIONS = 1001;

    private TextInputEditText cityInput;
    private TextView statusText;
    private MaterialButton startButton;
    private AddressSearchController startController;
    private AddressSearchController endController;
    private boolean pendingNavigation;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cityInput = findViewById(R.id.cityInput);
        statusText = findViewById(R.id.statusText);
        startButton = findViewById(R.id.startNavigationButton);
        startButton.setOnClickListener(v -> startNavigationWithPermissionCheck());

        showPrivacyDialog();
    }

    private void showPrivacyDialog() {
        new AlertDialog.Builder(this)
                .setTitle(R.string.privacy_title)
                .setMessage(R.string.privacy_message)
                .setCancelable(false)
                .setNegativeButton(R.string.decline, (dialog, which) -> finish())
                .setPositiveButton(R.string.agree, (dialog, which) -> initializeSdk())
                .show();
    }

    private void initializeSdk() {
        statusText.setText("正在初始化百度地图与导航 SDK...");
        NaviApplication application = (NaviApplication) getApplication();
        application.agreePrivacyAndInitialize((success, message) -> runOnUiThread(() -> {
            statusText.setText(message);
            if (application.isMapReady() && startController == null) {
                bindAddressSearch();
                requestNavigationPermissions();
            }
            updateStartButton();
        }));
    }

    private void bindAddressSearch() {
        AutoCompleteTextView startInput = findViewById(R.id.startInput);
        AutoCompleteTextView endInput = findViewById(R.id.endInput);

        AddressSearchController.CityProvider cityProvider = () -> {
            String value = cityInput.getText() == null ? "" : cityInput.getText().toString().trim();
            return TextUtils.isEmpty(value) ? "全国" : value;
        };
        startController = new AddressSearchController(startInput, cityProvider, item -> updateStartButton());
        endController = new AddressSearchController(endInput, cityProvider, item -> updateStartButton());
    }

    private void updateStartButton() {
        NaviApplication application = (NaviApplication) getApplication();
        boolean hasRoute = startController != null
                && endController != null
                && startController.getSelectedItem() != null
                && endController.getSelectedItem() != null;
        startButton.setEnabled(application.isNaviReady() && hasRoute);
    }

    private void startNavigationWithPermissionCheck() {
        if (!hasRequiredLocationPermissions()) {
            pendingNavigation = true;
            requestNavigationPermissions();
            return;
        }
        planRouteAndOpenGuide();
    }

    private void planRouteAndOpenGuide() {
        NaviApplication application = (NaviApplication) getApplication();
        if (!application.isNaviReady()) {
            Toast.makeText(this, "导航 SDK 尚未初始化完成", Toast.LENGTH_SHORT).show();
            return;
        }

        SuggestionItem start = startController == null ? null : startController.getSelectedItem();
        SuggestionItem end = endController == null ? null : endController.getSelectedItem();
        if (start == null || end == null || start.point == null || end.point == null) {
            Toast.makeText(this, "请从联想结果中分别选择起点和终点", Toast.LENGTH_SHORT).show();
            return;
        }

        hideKeyboard();
        statusText.setText("正在规划驾车路线...");
        startButton.setEnabled(false);

        BNRoutePlanNode startNode = new BNRoutePlanNode.Builder()
                .latitude(start.point.latitude)
                .longitude(start.point.longitude)
                .name(start.title)
                .description(start.subtitle)
                .build();
        BNRoutePlanNode endNode = new BNRoutePlanNode.Builder()
                .latitude(end.point.latitude)
                .longitude(end.point.longitude)
                .name(end.title)
                .description(end.subtitle)
                .build();

        List<BNRoutePlanNode> nodes = new ArrayList<>();
        nodes.add(startNode);
        nodes.add(endNode);

        BaiduNaviManagerFactory.getRoutePlanManager().routePlan(
                nodes,
                IBNRoutePlanManager.RoutePlanPreference.ROUTE_PLAN_PREFERENCE_DEFAULT,
                null,
                routePlanHandler);
    }

    private final Handler routePlanHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message message) {
            switch (message.what) {
                case IBNRoutePlanManager.MSG_NAVI_ROUTE_PLAN_START:
                    statusText.setText("正在计算路线,请稍候...");
                    break;
                case IBNRoutePlanManager.MSG_NAVI_ROUTE_PLAN_SUCCESS:
                    statusText.setText("路线规划成功,准备进入导航...");
                    break;
                case IBNRoutePlanManager.MSG_NAVI_ROUTE_PLAN_TO_NAVI:
                    statusText.setText("导航中");
                    startActivity(new Intent(MainActivity.this, NaviGuideActivity.class));
                    updateStartButton();
                    break;
                case IBNRoutePlanManager.MSG_NAVI_ROUTE_PLAN_FAILED:
                    statusText.setText("路线规划失败,请检查网络、AK 和起终点");
                    Toast.makeText(MainActivity.this, "路线规划失败", Toast.LENGTH_LONG).show();
                    updateStartButton();
                    break;
                default:
                    break;
            }
        }
    };

    private void requestNavigationPermissions() {
        List<String> permissions = new ArrayList<>();
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
        }
        if (Build.VERSION.SDK_INT >= 33
                && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
                != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.POST_NOTIFICATIONS);
        }
        if (!permissions.isEmpty()) {
            ActivityCompat.requestPermissions(this,
                    permissions.toArray(new String[0]),
                    REQUEST_NAV_PERMISSIONS);
        }
    }

    private boolean hasRequiredLocationPermissions() {
        return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode != REQUEST_NAV_PERMISSIONS) {
            return;
        }
        if (hasRequiredLocationPermissions()) {
            if (pendingNavigation) {
                pendingNavigation = false;
                planRouteAndOpenGuide();
            }
        } else {
            pendingNavigation = false;
            Toast.makeText(this, "真实导航需要精确定位权限", Toast.LENGTH_LONG).show();
        }
    }

    private void hideKeyboard() {
        InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        if (manager != null && getCurrentFocus() != null) {
            manager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
        }
    }

    @Override
    protected void onDestroy() {
        if (startController != null) {
            startController.destroy();
        }
        if (endController != null) {
            endController.destroy();
        }
        super.onDestroy();
    }
}

package com.example.baidunavidemo;

import android.app.Application;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;

import com.baidu.mapapi.CoordType;
import com.baidu.mapapi.SDKInitializer;
import com.baidu.navisdk.adapter.BaiduNaviManagerFactory;
import com.baidu.navisdk.adapter.IBNTTSManager;
import com.baidu.navisdk.adapter.IBaiduNaviManager;
import com.baidu.navisdk.adapter.struct.BNaviInitConfig;
import com.baidu.navisdk.comapi.tts.IBNTTSPlayerListener;
import com.baidu.navisdk.comapi.tts.c;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * 统一管理百度 SDK 的隐私合规初始化和导航状态。
 * 注意:任何百度 SDK API 都只能在用户明确同意隐私政策后调用。
 */
public class NaviApplication extends Application {

    public interface InitCallback {
        void onResult(boolean success, String message);
    }

    private final List<InitCallback> pendingCallbacks = new ArrayList<>();
    private boolean initializing;
    private boolean mapReady;
    private boolean naviReady;
    private TextToSpeech textToSpeech;
    private volatile boolean ttsReady;
    private volatile int ttsState = IBNTTSPlayerListener.PLAYER_STATE_NOT_INIT;
    private volatile String currentSpeech = "";
    private volatile String stoppedSpeechId = "";

    public synchronized boolean isMapReady() {
        return mapReady;
    }

    public synchronized boolean isNaviReady() {
        return naviReady;
    }

    public synchronized void agreePrivacyAndInitialize(InitCallback callback) {
        if (naviReady) {
            callback.onResult(true, "导航 SDK 已就绪");
            return;
        }
        pendingCallbacks.add(callback);
        if (initializing) {
            return;
        }
        initializing = true;

        try {
            SDKInitializer.setAgreePrivacy(this, true);
            SDKInitializer.initialize(this);
            SDKInitializer.setCoordType(CoordType.BD09LL);
            mapReady = true;
        } catch (Throwable error) {
            finishInitialization(false, "地图 SDK 初始化失败:" + safeMessage(error));
            return;
        }

        File externalDir = getExternalFilesDir(null);
        String rootPath = externalDir != null
                ? externalDir.getAbsolutePath()
                : getFilesDir().getAbsolutePath();

        BNaviInitConfig config = new BNaviInitConfig.Builder()
                .sdcardRootPath(rootPath)
                .naviInitListener(new IBaiduNaviManager.INaviInitListener() {
                    private boolean authFailed;

                    @Override
                    public void onAuthResult(int status, String message) {
                        if (status != 0) {
                            authFailed = true;
                            finishInitialization(false, "AK 鉴权失败(" + status + "):" + message);
                        }
                    }

                    @Override
                    public void initStart() {
                        // 初始化进度由首页状态文字展示。
                    }

                    @Override
                    public void initSuccess() {
                        if (authFailed) {
                            return;
                        }
                        naviReady = true;
                        initializeAndroidTts();
                        finishInitialization(true, "导航 SDK 已就绪,请选择起点和终点");
                    }

                    @Override
                    public void initFailed(int errorCode) {
                        finishInitialization(false, "导航 SDK 初始化失败,错误码:" + errorCode);
                    }
                })
                .build();

        try {
            BaiduNaviManagerFactory.getBaiduNaviManager().init(this, config);
        } catch (Throwable error) {
            finishInitialization(false, "导航 SDK 初始化异常:" + safeMessage(error));
        }
    }

    private synchronized void finishInitialization(boolean success, String message) {
        if (success) {
            naviReady = true;
        }
        initializing = false;
        List<InitCallback> callbacks = new ArrayList<>(pendingCallbacks);
        pendingCallbacks.clear();
        for (InitCallback callback : callbacks) {
            callback.onResult(success, message);
        }
    }

    /** 使用系统 TextToSpeech 作为百度导航的外置 TTS,避免额外申请语音 AppId。 */
    private void initializeAndroidTts() {
        textToSpeech = new TextToSpeech(this, status -> {
            ttsReady = status == TextToSpeech.SUCCESS;
            ttsState = ttsReady
                    ? IBNTTSPlayerListener.PLAYER_STATE_IDLE
                    : IBNTTSPlayerListener.PLAYER_STATE_ERROR;
            if (ttsReady) {
                textToSpeech.setLanguage(Locale.SIMPLIFIED_CHINESE);
                textToSpeech.setSpeechRate(1.0f);
                textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
                    @Override
                    public void onStart(String utteranceId) {
                        ttsState = IBNTTSPlayerListener.PLAYER_STATE_PLAYING;
                    }

                    @Override
                    public void onDone(String utteranceId) {
                        ttsState = IBNTTSPlayerListener.PLAYER_STATE_IDLE;
                        stoppedSpeechId = utteranceId == null ? "" : utteranceId;
                        currentSpeech = "";
                    }

                    @Override
                    public void onError(String utteranceId) {
                        ttsState = IBNTTSPlayerListener.PLAYER_STATE_ERROR;
                        stoppedSpeechId = utteranceId == null ? "" : utteranceId;
                    }
                });
            }
        });

        BaiduNaviManagerFactory.getTTSManager().initTTS(
                new IBNTTSManager.IBNOuterTTSPlayerCallback() {
                    @Override
                    public int playTTSText(String speech,
                                           String extra,
                                           int preempt,
                                           String speechId) {
                        if (!ttsReady || textToSpeech == null) {
                            return 0;
                        }
                        currentSpeech = speech == null ? "" : speech;
                        int queueMode = preempt == 1
                                ? TextToSpeech.QUEUE_FLUSH
                                : TextToSpeech.QUEUE_ADD;
                        int result = textToSpeech.speak(speech, queueMode, null, speechId);
                        return result == TextToSpeech.SUCCESS ? 1 : 0;
                    }

                    @Override
                    public int getTTSState() {
                        return ttsState;
                    }

                    @Override
                    public String getCurTTSSpeech() {
                        return currentSpeech;
                    }

                    @Override
                    public String getStopTTSSpeechId() {
                        return stoppedSpeechId;
                    }

                    @Override
                    public int playXDTTSText(boolean preempt, c ttsData) {
                        return super.playXDTTSText(ttsData);
                    }
                });
    }

    private String safeMessage(Throwable error) {
        return error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage();
    }

    @Override
    public void onTerminate() {
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
        super.onTerminate();
    }
}

package com.example.baidunavidemo;

import android.os.Bundle;
import android.content.res.Configuration;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.baidu.navisdk.adapter.BNaviCommonParams;
import com.baidu.navisdk.adapter.BaiduNaviManagerFactory;
import com.baidu.navisdk.adapter.struct.BNGuideConfig;

/** 将百度专业导航 View 直接嵌入当前 App,而不是跳转百度地图客户端。 */
public class NaviGuideActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle params = new Bundle();
        params.putBoolean(BNaviCommonParams.ProGuideKey.IS_REALNAVI, true);
        params.putBoolean(BNaviCommonParams.ProGuideKey.IS_SUPPORT_FULL_SCREEN, true);
        BNGuideConfig config = new BNGuideConfig.Builder()
                .params(params)
                .build();
        View guideView = BaiduNaviManagerFactory.getRouteGuideManager().onCreate(this, config);
        setContentView(guideView);
    }

    @Override
    protected void onStart() {
        super.onStart();
        BaiduNaviManagerFactory.getRouteGuideManager().onStart();
    }

    @Override
    protected void onResume() {
        super.onResume();
        BaiduNaviManagerFactory.getRouteGuideManager().onResume();
    }

    @Override
    protected void onPause() {
        BaiduNaviManagerFactory.getRouteGuideManager().onPause();
        super.onPause();
    }

    @Override
    protected void onStop() {
        BaiduNaviManagerFactory.getRouteGuideManager().onStop();
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        BaiduNaviManagerFactory.getRouteGuideManager().onDestroy(false);
        super.onDestroy();
    }

    @Override
    public void onBackPressed() {
        BaiduNaviManagerFactory.getRouteGuideManager().onBackPressed(false);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        BaiduNaviManagerFactory.getRouteGuideManager().onConfigurationChanged(newConfig);
    }
}

package com.example.baidunavidemo;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

final class SuggestionAdapter extends BaseAdapter implements Filterable {
    private final LayoutInflater inflater;
    private final List<SuggestionItem> items = new ArrayList<>();

    SuggestionAdapter(Context context) {
        inflater = LayoutInflater.from(context);
    }

    void replaceAll(List<SuggestionItem> newItems) {
        items.clear();
        items.addAll(newItems);
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return items.size();
    }

    @Override
    public SuggestionItem getItem(int position) {
        return items.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.item_suggestion, parent, false);
            holder = new ViewHolder();
            holder.title = convertView.findViewById(R.id.suggestionTitle);
            holder.subtitle = convertView.findViewById(R.id.suggestionSubtitle);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        SuggestionItem item = getItem(position);
        holder.title.setText(item.title);
        holder.subtitle.setText(item.subtitle);
        holder.subtitle.setVisibility(item.subtitle.isEmpty() ? View.GONE : View.VISIBLE);
        return convertView;
    }

    /** 结果已由百度 SuggestionSearch 过滤,此处禁止 AutoCompleteTextView 二次本地过滤。 */
    @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults results = new FilterResults();
                results.values = items;
                results.count = items.size();
                return results;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                notifyDataSetChanged();
            }
        };
    }

    private static class ViewHolder {
        TextView title;
        TextView subtitle;
    }
}

package com.example.baidunavidemo;

import com.baidu.mapapi.model.LatLng;

final class SuggestionItem {
    final String title;
    final String subtitle;
    final LatLng point;

    SuggestionItem(String title, String subtitle, LatLng point) {
        this.title = title;
        this.subtitle = subtitle;
        this.point = point;
    }

    String displayText() {
        return title + (subtitle.isEmpty() ? "" : " · " + subtitle);
    }

    @Override
    public String toString() {
        return title;
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/surface">

    <View
        android:id="@+id/headerBackground"
        android:layout_width="0dp"
        android:layout_height="240dp"
        android:background="@drawable/bg_header"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:background="@drawable/bg_badge"
        android:text="BAIDU NAVI · JAVA"
        android:textColor="@color/cyan_300"
        android:textSize="11sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="18dp"
        android:text="@string/title"
        android:textColor="@color/white"
        android:textSize="30sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/badge" />

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="7dp"
        android:layout_marginEnd="24dp"
        android:text="@string/subtitle"
        android:textColor="#BFD1E8"
        android:textSize="14sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title" />

    <androidx.cardview.widget.CardView
        android:id="@+id/searchCard"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="18dp"
        android:layout_marginTop="18dp"
        android:layout_marginEnd="18dp"
        app:cardCornerRadius="20dp"
        app:cardElevation="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/subtitle">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="18dp">

            <com.google.android.material.textfield.TextInputLayout
                style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/city_hint"
                app:boxCornerRadiusBottomEnd="12dp"
                app:boxCornerRadiusBottomStart="12dp"
                app:boxCornerRadiusTopEnd="12dp"
                app:boxCornerRadiusTopStart="12dp">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/cityInput"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="text"
                    android:maxLines="1"
                    android:text="全国" />
            </com.google.android.material.textfield.TextInputLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:gravity="center_vertical"
                android:orientation="horizontal">

                <View
                    android:layout_width="12dp"
                    android:layout_height="12dp"
                    android:layout_marginEnd="12dp"
                    android:background="@drawable/bg_dot_start" />

                <AutoCompleteTextView
                    android:id="@+id/startInput"
                    android:layout_width="0dp"
                    android:layout_height="52dp"
                    android:layout_weight="1"
                    android:background="@android:color/transparent"
                    android:completionThreshold="1"
                    android:dropDownHeight="wrap_content"
                    android:hint="@string/start_hint"
                    android:imeOptions="actionNext"
                    android:inputType="text"
                    android:maxLines="1"
                    android:paddingHorizontal="4dp"
                    android:textColor="@color/text_primary"
                    android:textColorHint="@color/text_secondary"
                    android:textSize="16sp" />
            </LinearLayout>

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:layout_marginStart="24dp"
                android:background="@color/divider" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal">

                <View
                    android:layout_width="12dp"
                    android:layout_height="12dp"
                    android:layout_marginEnd="12dp"
                    android:background="@drawable/bg_dot_end" />

                <AutoCompleteTextView
                    android:id="@+id/endInput"
                    android:layout_width="0dp"
                    android:layout_height="52dp"
                    android:layout_weight="1"
                    android:background="@android:color/transparent"
                    android:completionThreshold="1"
                    android:dropDownHeight="wrap_content"
                    android:hint="@string/end_hint"
                    android:imeOptions="actionDone"
                    android:inputType="text"
                    android:maxLines="1"
                    android:paddingHorizontal="4dp"
                    android:textColor="@color/text_primary"
                    android:textColorHint="@color/text_secondary"
                    android:textSize="16sp" />
            </LinearLayout>
        </LinearLayout>
    </androidx.cardview.widget.CardView>

    <TextView
        android:id="@+id/statusText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="28dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="28dp"
        android:gravity="center"
        android:text="等待同意隐私政策"
        android:textColor="@color/text_secondary"
        android:textSize="13sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchCard" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/startNavigationButton"
        android:layout_width="0dp"
        android:layout_height="58dp"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="28dp"
        android:enabled="false"
        android:text="@string/start_nav"
        android:textAllCaps="false"
        android:textSize="17sp"
        android:textStyle="bold"
        app:backgroundTint="@color/blue_600"
        app:cornerRadius="16dp"
        app:icon="@drawable/ic_arrow"
        app:iconGravity="textEnd"
        app:iconPadding="12dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

item_suggestion.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="wrap_content"
    android:background="@color/white"
    android:minHeight="64dp"
    android:orientation="vertical"
    android:paddingStart="16dp"
    android:paddingTop="10dp"
    android:paddingEnd="16dp"
    android:paddingBottom="10dp">

    <TextView
        android:id="@+id/suggestionTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="@color/text_primary"
        android:textSize="15sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/suggestionSubtitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="@color/text_secondary"
        android:textSize="12sp" />
</LinearLayout>
相关推荐
AFinalStone2 小时前
Android 7系统无障碍服务(四)AccessibilityEvent 的产生与投递
android·无障碍服务
智购科技无人售货机工厂2 小时前
2026自动售货机热敏打印模块集成:从串口驱动到小票自动裁切的工程实践~YH
android·stm32·单片机·嵌入式硬件·系统架构
A13345553 小时前
苹果安卓手机播放器推荐:2026无广告4K怎么选
android·智能手机
hunterandroid4 小时前
ContentProvider 跨进程数据共享实战
android·前端
天空之城--5 小时前
Android输入法子系统深度解析
android
天空之城--6 小时前
Android全链路性能优化:从原理到实践的终极指南
android·性能优化
Meteors.6 小时前
Android 性能优化:05.CPU优化
android·性能优化
石头猫灯7 小时前
一次 CMS 被黑实录:完整事件思路梳理
android·学习
zhonyu鱼7 小时前
Escrcpy:用电脑键盘鼠标操作安卓手机的跨平台投屏工具
android·计算机外设·电脑